feat(video): 移动端布局全面优化,引入粘性导航条和底部参数组件

This commit is contained in:
XianYuDaXian committed 2026-03-14 09:16:06 +08:00
1 parent 1a4cc52ed8
commit 254c6b9c53
3 files changed
+434 -4

No files matched your search

+237 -2
View File
@@ -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 将节点移到 <body> 直接子元素,避免祖先层叠上下文干扰 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 {
+148
View File
@@ -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();
})();
})();
+49 -2
View File
@@ -19,7 +19,7 @@
<link href="/static/vendor/geist/geist-mono.css" rel="stylesheet">
<script src="/static/common/js/theme.js?v=0.1.0"></script>
<link href="/static/common/css/common.css?v=0.3.7" rel="stylesheet">
<link href="/static/public/css/video.css?v=0.4.16" rel="stylesheet">
<link href="/static/public/css/video.css?v=0.4.21" rel="stylesheet">
<link href="/static/common/css/toast.css" rel="stylesheet">
</head>
@@ -300,6 +300,53 @@
<div id="app-footer"></div>
<!-- 移动端底部悬浮操作栏 -->
<div id="mobileActionBar" class="mobile-action-bar">
<div class="mobile-action-bar-inner">
<!-- 左:齿轮按钮,点击弹出生成参数 -->
<button id="mobileSettingsBtn" class="mobile-settings-btn" type="button" aria-label="生成参数">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="3"/>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
</svg>
</button>
<!-- 右:操作区,根据上下文滑动切换 -->
<div class="mobile-action-ops">
<!-- 生成模式(默认) -->
<div id="mobileBarGenerate" class="mobile-action-slot mobile-action-slot--active">
<button id="mobileStartBtn" class="geist-button mobile-action-btn gap-2" type="button">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<polygon points="5 3 19 12 5 21 5 3" />
</svg>
开始生成
</button>
<button id="mobileStopBtn" class="geist-button-outline mobile-action-btn gap-2 hidden" type="button">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<rect x="5" y="5" width="14" height="14" />
</svg>
停止
</button>
</div>
<!-- 延长模式(滚到延长区时) -->
<div id="mobileBarSplice" class="mobile-action-slot">
<button id="mobileSpliceBtn" class="geist-button mobile-action-btn gap-2" type="button">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<path d="M5 12h14" />
<path d="M12 5v14" />
</svg>
延长视频
</button>
</div>
</div>
</div>
</div>
<!-- 生成参数 Sheet 遮罩 -->
<div id="mobileSettingsOverlay" class="mobile-settings-overlay"></div>
<script src="/static/common/js/admin-auth.js?v=0.3.0"></script>
<script src="/static/common/js/parentpost-memory.js?v=0.1.0"></script>
<script src="/static/common/js/public-header.js?v=0.3.3"></script>
@@ -307,7 +354,7 @@
<script src="/static/common/js/toast.js"></script>
<script src="/static/common/js/prompt-enhancer.js?v=0.1.15"></script>
<script src="/static/common/js/pwa.js?v=0.1.1"></script>
<script src="/static/public/js/video.js?v=0.6.0"></script>
<script src="/static/public/js/video.js?v=0.6.3"></script>
</body>
</html>