diff --git a/app/static/public/css/video.css b/app/static/public/css/video.css index 1a6b2c3..18bf8dc 100644 --- a/app/static/public/css/video.css +++ b/app/static/public/css/video.css @@ -18,18 +18,47 @@ body { /* 移动端强制约束 */ @media (max-width: 768px) { + /* 主体内容底部留出 sticky bar 的高度 */ + body { + padding-bottom: 72px; + } + .video-main-grid { display: flex; flex-direction: column; width: 100%; min-width: 0; + gap: 16px; } - .video-main-left, - .video-main-right { + /* video-main-right 整体提前(运行状态 + 参数一览紧跟标题) */ + .video-main-left { + order: 1; width: 100%; min-width: 0; max-width: 100%; + display: flex; + flex-direction: column; + gap: 16px; + } + + .video-main-right { + order: 0; + display: flex; + flex-direction: column; + gap: 16px; + width: 100%; + min-width: 0; + max-width: 100%; + } + + /* video-main-right 内部:生成参数先,运行状态次,历史最后 */ + .video-settings-card { + order: -1; + } + + .history-panel { + order: 1; } .video-card { @@ -46,8 +75,207 @@ body { min-width: 0 !important; overflow-x: hidden; } + + /* 生成参数:2 列横向网格,减少纵向高度 */ + .video-settings-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; + } + + /* 再次确保原始按钮在移动端彻底隐藏 */ + .settings-actions-side { + display: none !important; + } + + .splice-action-row { + display: none !important; + } } +/* ─── 移动端底部 Sticky 操作栏 ─── */ +.mobile-action-bar { + display: none; /* 桌面端始终隐藏 */ +} + +/* 遮罩:全局放置,默认不可见 */ +.mobile-settings-overlay { + display: none; + position: fixed; + inset: 0; + z-index: 250; + background: rgba(0, 0, 0, 0.52); + opacity: 0; + pointer-events: none; + transition: opacity 0.22s ease; +} + +@media (max-width: 768px) { + /* ── Sticky Bar 本体 ── */ + .mobile-action-bar { + display: block; + position: fixed; + left: 0; + right: 0; + bottom: 0; + z-index: 200; + padding-bottom: env(safe-area-inset-bottom, 0px); + background: var(--video-surface); + border-top: 1px solid var(--video-outline); + box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.08); + } + + html[data-theme='dark'] .mobile-action-bar { + background: #131a24; + border-top-color: #2b3440; + box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.35); + } + + /* ── Bar 内部:左齿轮 + 右操作区 ── */ + .mobile-action-bar-inner { + display: flex; + align-items: center; + gap: 10px; + height: 56px; + padding: 0 12px; + } + + /* 齿轮按钮 */ + .mobile-settings-btn { + flex-shrink: 0; + width: 44px; + height: 44px; + border-radius: 10px; + border: 1px solid var(--video-outline); + background: transparent; + color: var(--video-ink); + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: background 0.15s ease; + } + + .mobile-settings-btn:active { + background: var(--video-muted); + } + + /* 齿轮激活态(sheet 打开时) */ + body.mobile-settings-open .mobile-settings-btn { + background: var(--video-muted); + border-color: var(--accents-4); + } + + html[data-theme='dark'] .mobile-settings-btn { + border-color: #2b3440; + color: #e6edf3; + } + + html[data-theme='dark'] body.mobile-settings-open .mobile-settings-btn { + background: #1b2432; + } + + /* 右侧操作区容器(滑动切换区) */ + .mobile-action-ops { + flex: 1; + position: relative; + height: 44px; + min-width: 0; + overflow: hidden; + border-radius: 8px; + } + + /* 两个插槽:绝对定位叠放,transform 滑动切换 */ + .mobile-action-slot { + position: absolute; + inset: 0; + display: flex; + align-items: center; + gap: 8px; + transform: translateX(100%); + transition: transform 0.22s cubic-bezier(0.4, 0, 0.2, 1); + pointer-events: none; + } + + .mobile-action-slot--active { + transform: translateX(0); + pointer-events: auto; + } + + /* 操作按钮 */ + .mobile-action-btn { + flex: 1; + height: 44px; + font-size: 14px; + font-weight: 500; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 8px; + min-width: 0; + } + + /* 停止按钮不撑满 */ + #mobileStopBtn { + flex: 0 0 auto; + padding: 0 16px; + } + + /* ── 遮罩激活态 ── */ + .mobile-settings-overlay { + display: block; + } + + body.mobile-settings-open .mobile-settings-overlay { + opacity: 1; + pointer-events: auto; + } +} + +/* ── + video-settings-card 作为 Bottom Sheet 的样式 + ★ 通过 JS 将节点移到 直接子元素,避免祖先层叠上下文干扰 fixed 定位 + 选择器用 body > .video-settings-card 确保高特异性、精准命中 +── */ +body > .video-settings-card { + position: fixed; + left: 0; + right: 0; + bottom: 56px; /* sticky bar 高度 */ + top: auto; + z-index: 310; + margin: 0; + border-radius: 16px 16px 0 0; + transform: translateY(calc(100% + 56px)); + transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); + max-height: 72vh; + overflow-y: auto; + padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 8px); + pointer-events: none; + box-shadow: 0 -4px 32px rgba(0, 0, 0, 0.18); +} + +body.mobile-settings-open > .video-settings-card { + transform: translateY(0); + pointer-events: auto; +} + +html[data-theme='dark'] body > .video-settings-card { + box-shadow: 0 -8px 40px rgba(0, 0, 0, 0.55); +} + +/* sheet 标题行样式 */ +body > .video-settings-card > .card-title { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; + padding-bottom: 12px; + border-bottom: 1px solid var(--video-outline); + font-size: 14px; +} + + .video-header-row { display: flex; align-items: center; @@ -212,6 +440,13 @@ body { .video-main-grid { grid-template-columns: 1fr; } + + /* 820px ~ 769px 区间:grid 单列,right 保持 flex column 内部排列 */ + .video-main-right { + display: flex; + flex-direction: column; + gap: 20px; + } } .video-card { diff --git a/app/static/public/js/video.js b/app/static/public/js/video.js index 992e7f4..5b7cda4 100644 --- a/app/static/public/js/video.js +++ b/app/static/public/js/video.js @@ -3129,4 +3129,152 @@ clearReferencePreview(); } + // ─── 移动端底部 Sticky 操作栏逻辑 ─────────────────────────────────────── + (function initMobileActionBar() { + const bar = document.getElementById('mobileActionBar'); + if (!bar) return; + + const slotGenerate = document.getElementById('mobileBarGenerate'); + const slotSplice = document.getElementById('mobileBarSplice'); + const mobileStart = document.getElementById('mobileStartBtn'); + const mobileStop = document.getElementById('mobileStopBtn'); + const mobileSplice = document.getElementById('mobileSpliceBtn'); + const settingsBtn = document.getElementById('mobileSettingsBtn'); + const settingsOverlay = document.getElementById('mobileSettingsOverlay'); + // settings-card:作为 bottom sheet 使用 + const settingsCard = document.querySelector('.video-settings-card'); + let scOriginalParent = null; + let scOriginalNextSibling = null; + + function isMobile() { + return window.matchMedia('(max-width: 768px)').matches; + } + + // ── 将 settings-card 移到 body 直接子,避免祖先层叠上下文干扰 position:fixed ── + function moveCardToBody() { + if (!settingsCard || settingsCard.parentElement === document.body) return; + scOriginalParent = settingsCard.parentElement; + scOriginalNextSibling = settingsCard.nextElementSibling; + document.body.appendChild(settingsCard); + } + + function moveCardBack() { + if (!settingsCard || settingsCard.parentElement !== document.body) return; + if (scOriginalParent) { + scOriginalNextSibling + ? scOriginalParent.insertBefore(settingsCard, scOriginalNextSibling) + : scOriginalParent.appendChild(settingsCard); + } + } + + // ── 齿轮:生成参数 Bottom Sheet 开关 ── + function openSettingsSheet() { + document.body.classList.add('mobile-settings-open'); + if (settingsBtn) settingsBtn.setAttribute('aria-expanded', 'true'); + } + + function closeSettingsSheet() { + document.body.classList.remove('mobile-settings-open'); + if (settingsBtn) settingsBtn.setAttribute('aria-expanded', 'false'); + } + + if (settingsBtn) { + settingsBtn.addEventListener('click', () => { + document.body.classList.contains('mobile-settings-open') + ? closeSettingsSheet() + : openSettingsSheet(); + }); + } + + // 点击遮罩关闭 + if (settingsOverlay) { + settingsOverlay.addEventListener('click', closeSettingsSheet); + } + + // Esc 键关闭 + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && document.body.classList.contains('mobile-settings-open')) { + closeSettingsSheet(); + } + }); + + // resize:移动端时把节点移到 body,桌面端时关闭 sheet 并移回原位 + window.addEventListener('resize', () => { + if (isMobile()) { + moveCardToBody(); + } else { + closeSettingsSheet(); + moveCardBack(); + } + }); + + // ── 按钮代理:点击 sticky bar 按钮 = 点击原始按钮 ── + if (mobileStart) { + mobileStart.addEventListener('click', () => { + if (startBtn && !startBtn.disabled) startBtn.click(); + }); + } + if (mobileStop) { + mobileStop.addEventListener('click', () => { + if (stopBtn && !stopBtn.disabled) stopBtn.click(); + }); + } + if (mobileSplice) { + mobileSplice.addEventListener('click', () => { + if (spliceBtn && !spliceBtn.disabled) spliceBtn.click(); + }); + } + + // ── 同步生成按钮状态(running / idle)到 sticky bar ── + function syncGenerateSlotState() { + if (!slotGenerate || !mobileStart || !mobileStop) return; + const running = stopBtn && !stopBtn.classList.contains('hidden'); + mobileStop.classList.toggle('hidden', !running); + mobileStart.classList.toggle('hidden', running); + slotGenerate.classList.toggle('is-running', running); + if (mobileStart) mobileStart.disabled = Boolean(startBtn && startBtn.disabled); + } + + if (startBtn || stopBtn) { + const syncObserver = new MutationObserver(syncGenerateSlotState); + [startBtn, stopBtn].filter(Boolean).forEach((el) => { + syncObserver.observe(el, { attributes: true, attributeFilter: ['class', 'disabled'] }); + }); + } + + // ── IntersectionObserver:监听 #editPanel 可见性,切换操作插槽 ── + const targetPanel = document.getElementById('editPanel'); + if (targetPanel && 'IntersectionObserver' in window) { + let spliceVisible = false; + const io = new IntersectionObserver( + (entries) => { + if (!isMobile()) return; + entries.forEach((entry) => { spliceVisible = entry.isIntersecting; }); + if (slotGenerate) slotGenerate.classList.toggle('mobile-action-slot--active', !spliceVisible); + if (slotSplice) slotSplice.classList.toggle('mobile-action-slot--active', spliceVisible); + }, + { threshold: 0.15 } + ); + io.observe(targetPanel); + } + + // ── 延长视频 sticky 按钮文本 & 状态同步 ── + function syncSpliceBtnState() { + if (!mobileSplice || !spliceBtn) return; + mobileSplice.disabled = spliceBtn.disabled; + const span = spliceBtn.querySelector('span'); + const mSpan = mobileSplice.querySelector('span'); + if (span && mSpan) mSpan.textContent = span.textContent; + } + if (spliceBtn) { + new MutationObserver(syncSpliceBtnState) + .observe(spliceBtn, { subtree: true, childList: true, attributes: true, attributeFilter: ['disabled'] }); + } + + // 初始化状态 + if (isMobile()) moveCardToBody(); // 移到 body 直接子,确保 position:fixed 相对视口 + syncGenerateSlotState(); + syncSpliceBtnState(); + })(); + })(); diff --git a/app/static/public/pages/video.html b/app/static/public/pages/video.html index dc17026..9864c36 100644 --- a/app/static/public/pages/video.html +++ b/app/static/public/pages/video.html @@ -19,7 +19,7 @@ - + @@ -300,6 +300,53 @@ + +
+
+ + + +
+ +
+ + +
+ +
+ +
+
+
+
+ +
+ @@ -307,7 +354,7 @@ - +