feat(video): 移动端布局全面优化,引入粘性导航条和底部参数组件
This commit is contained in:
1 parent
1a4cc52ed8
commit
254c6b9c53
3 files changed
+434
-4
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
})();
|
||||
|
||||
})();
|
||||
@@ -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>
|
||||
Reference in new issue
Block a user