feat(imagine): 使用粘性导航条和底部设置表单替换悬浮按钮

This commit is contained in:
XianYuDaXian committed 2026-03-14 09:16:07 +08:00
1 parent b6eefe5cff
commit 5bf8a03ef6
3 files changed
+367 -32

No files matched your search

+202
View File
@@ -1252,3 +1252,205 @@ html[data-theme='dark'] .waterfall-item.selected .image-checkbox::after {
grid-template-columns: 1fr;
}
}
/* ============================
移动端 Sticky Bar im-sticky-bar(<=900px)
============================ */
@media (max-width: 900px) {
body {
padding-bottom: 72px !important;
}
.im-sticky-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 200;
background: #fff;
border-top: 1px solid var(--border);
padding: 10px 16px;
padding-bottom: calc(10px + env(safe-area-inset-bottom, 0px));
box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.07);
}
.im-sticky-bar-inner {
display: flex;
align-items: center;
gap: 8px;
max-width: 700px;
margin: 0 auto;
}
.im-icon-divider {
width: 1px;
height: 24px;
background: var(--border);
flex-shrink: 0;
}
.im-icon-group {
display: flex;
align-items: center;
gap: 4px;
flex: 1;
justify-content: center;
}
.im-icon-btn {
width: 40px;
height: 40px;
border: 1px solid var(--border);
border-radius: 10px;
background: #fff;
color: var(--fg);
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
flex-shrink: 0;
transition: background .15s ease, border-color .15s ease;
position: relative;
padding: 0;
}
.im-icon-btn:active {
background: var(--accents-1);
}
.im-icon-btn--danger {
color: #ef4444;
border-color: #fecaca;
}
.im-icon-btn--danger:active {
background: #fef2f2;
}
.im-selected-badge {
position: absolute;
top: -4px;
right: -4px;
min-width: 16px;
height: 16px;
padding: 0 4px;
border-radius: 999px;
background: #ef4444;
color: #fff;
font-size: 9px;
font-weight: 700;
line-height: 16px;
text-align: center;
display: none;
pointer-events: none;
}
.im-selected-badge.visible {
display: block;
}
.im-main-btn {
height: 40px;
padding: 0 18px;
font-size: 14px;
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
border-radius: 10px;
min-width: 88px;
justify-content: center;
}
.im-main-btn.is-stop {
background: #fee2e2 !important;
color: #ef4444 !important;
border: 1px solid #fecaca !important;
}
.im-settings-overlay {
display: none;
position: fixed;
inset: 0;
z-index: 300;
background: rgba(0, 0, 0, 0.38);
backdrop-filter: blur(2px);
-webkit-backdrop-filter: blur(2px);
}
.im-settings-overlay.active {
display: block;
}
.im-settings-sheet {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 301;
background: #fff;
border-radius: 20px 20px 0 0;
padding: 0 16px 24px;
padding-bottom: calc(24px + env(safe-area-inset-bottom, 0px));
max-height: 88vh;
overflow-y: auto;
box-shadow: 0 -8px 40px rgba(0, 0, 0, 0.18);
transition: transform .28s cubic-bezier(.2, 0, 0, 1);
transform: translateY(100%);
}
.im-settings-sheet.open {
transform: translateY(0);
}
.im-settings-sheet-handle {
display: flex;
justify-content: center;
padding: 12px 0 8px;
cursor: pointer;
}
.im-settings-sheet-handle::after {
content: '';
width: 36px;
height: 4px;
border-radius: 999px;
background: var(--border);
}
.im-settings-sheet-title {
font-size: 14px;
font-weight: 600;
color: var(--accents-7);
margin-bottom: 14px;
text-align: center;
}
}
html[data-theme='dark'] .im-sticky-bar {
background: #111821;
border-top-color: #2b3440;
box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.3);
}
html[data-theme='dark'] .im-icon-btn {
background: #1a2330;
border-color: #3b4654;
color: #c9d6e5;
}
html[data-theme='dark'] .im-icon-btn:active {
background: #243244;
}
html[data-theme='dark'] .im-main-btn.is-stop {
background: #2d1414 !important;
color: #f87171 !important;
border-color: #7f1d1d !important;
}
html[data-theme='dark'] .im-settings-sheet {
background: #111821;
color: #e2e8f0;
}
+102
View File
@@ -2303,4 +2303,106 @@
}
});
}
/**
* 移动端 Sticky Bar 初始化
*/
function initStickyBar() {
/* 图标按钮代理 */
const proxyMap = {
imClearBtn: 'clearBtn',
imBatchBtn: 'batchDownloadBtn',
imSelectAllBtn: 'toggleSelectAllBtn',
imDownloadSelectedBtn: 'downloadSelectedBtn',
};
Object.entries(proxyMap).forEach(([stickyId, origId]) => {
const stickyEl = document.getElementById(stickyId);
const origEl = document.getElementById(origId);
if (stickyEl && origEl) stickyEl.addEventListener('click', () => origEl.click());
});
/* 主按钮(开始/停止)同步 */
const imBtn = document.getElementById('imStartStopBtn');
const playIcon = imBtn && imBtn.querySelector('.im-btn-icon-play');
const stopIcon = imBtn && imBtn.querySelector('.im-btn-icon-stop');
const btnLabel = imBtn && imBtn.querySelector('.im-btn-label');
function syncMainBtn() {
if (!imBtn) return;
const isStopping = startBtn && startBtn.classList.contains('hidden');
imBtn.classList.toggle('is-stop', !!isStopping);
if (playIcon) playIcon.style.display = isStopping ? 'none' : '';
if (stopIcon) stopIcon.style.display = isStopping ? '' : 'none';
if (btnLabel) btnLabel.textContent = isStopping ? '停止' : '开始';
}
if (imBtn) {
imBtn.addEventListener('click', () => {
const isStopping = startBtn && startBtn.classList.contains('hidden');
(isStopping ? stopBtn : startBtn) && (isStopping ? stopBtn : startBtn).click();
});
}
if (startBtn) new MutationObserver(syncMainBtn).observe(startBtn, { attributes: true, attributeFilter: ['class'] });
syncMainBtn();
/* 选中数字角标同步 */
const origBadge = document.getElementById('selectedCount');
const imBadge = document.getElementById('imSelectedBadge');
function syncBadge() {
if (!origBadge || !imBadge) return;
const count = parseInt(origBadge.textContent || '0', 10);
imBadge.textContent = String(count);
imBadge.classList.toggle('visible', count > 0);
}
if (origBadge) {
new MutationObserver(syncBadge).observe(origBadge, { characterData: true, childList: true, subtree: true });
syncBadge();
}
/* 生成设置 Bottom Sheet */
const settingsBtn = document.getElementById('imSettingsBtn');
const settingsOverlay = document.getElementById('imSettingsOverlay');
const settingsCard = document.querySelector('.settings-card');
let settingsSheet = document.getElementById('imSettingsSheet');
if (!settingsSheet && settingsCard) {
settingsSheet = document.createElement('div');
settingsSheet.id = 'imSettingsSheet';
settingsSheet.className = 'im-settings-sheet';
const handle = document.createElement('div');
handle.className = 'im-settings-sheet-handle';
settingsSheet.appendChild(handle);
const titleEl = document.createElement('div');
titleEl.className = 'im-settings-sheet-title';
titleEl.textContent = '生成设置';
settingsSheet.appendChild(titleEl);
const cardContent = settingsCard.querySelector('.card-content');
if (cardContent) settingsSheet.appendChild(cardContent);
document.body.appendChild(settingsSheet);
}
function openSettings() {
if (!settingsSheet || !settingsOverlay) return;
settingsOverlay.classList.add('active');
requestAnimationFrame(() => settingsSheet.classList.add('open'));
}
function closeSettings() {
if (!settingsSheet || !settingsOverlay) return;
settingsSheet.classList.remove('open');
settingsOverlay.classList.remove('active');
}
if (settingsBtn) settingsBtn.addEventListener('click', openSettings);
if (settingsOverlay) settingsOverlay.addEventListener('click', closeSettings);
const sheetHandle = settingsSheet && settingsSheet.querySelector('.im-settings-sheet-handle');
if (sheetHandle) sheetHandle.addEventListener('click', closeSettings);
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && settingsSheet && settingsSheet.classList.contains('open')) closeSettings();
});
}
initStickyBar();
})();
+63 -32
View File
@@ -257,39 +257,71 @@
清空
</button>
<span class="toolbar-sep"></span>
<button id="batchDownloadBtn"
class="batch-download-btn geist-button-outline text-xs px-3 gap-1 border-0 hover:bg-gray-100" title="批量下载">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
<span>批量</span>
</button>
<div id="selectionToolbar" class="selection-toolbar hidden">
<button id="toggleSelectAllBtn"
class="geist-button-outline text-xs px-3 gap-1 border-0 hover:bg-gray-100">全选</button>
<button id="downloadSelectedBtn" class="geist-button-outline text-xs px-3 gap-1 border-0 hover:bg-gray-100">下载
<span id="selectedCount" class="selected-count">0</span></button>
</div>
</div>
<!-- 移动端生成设置遮罩(Bottom Sheet) -->
<div id="imSettingsOverlay" class="im-settings-overlay"></div>
<div class="mobile-fab" data-mobile-fab>
<div class="mobile-fab-panel">
<div class="mobile-fab-actions">
<button class="geist-button" data-proxy-click="#startBtn" type="button">开始</button>
<button class="geist-button-outline" data-proxy-click="#stopBtn" type="button">停止</button>
<button class="geist-button-outline" data-proxy-click="#clearBtn" type="button">清空</button>
<button class="geist-button-outline" data-proxy-click="#batchDownloadBtn" type="button">批量</button>
<button class="geist-button-outline" data-proxy-click="#toggleSelectAllBtn" type="button">全选</button>
<button class="geist-button-outline" data-proxy-click="#downloadSelectedBtn" type="button">下载</button>
<!-- 移动端底部 Sticky 操作栏 -->
<div id="imStickyBar" class="im-sticky-bar">
<div class="im-sticky-bar-inner">
<!-- 左侧:齿轮(生成设置) -->
<button class="im-icon-btn" id="imSettingsBtn" type="button" title="生成设置" 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="im-icon-divider"></div>
<!-- 图标区 -->
<div class="im-icon-group">
<!-- 清空 -->
<button class="im-icon-btn im-icon-btn--danger" id="imClearBtn" type="button" title="清空图片" aria-label="清空">
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/>
<path d="M10 11v6M14 11v6"/><path d="M9 6V4h6v2"/>
</svg>
</button>
<!-- 批量下载 -->
<button class="im-icon-btn" id="imBatchBtn" type="button" title="批量下载" aria-label="批量下载">
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>
</svg>
</button>
<!-- 全选 -->
<button class="im-icon-btn" id="imSelectAllBtn" type="button" title="全选" aria-label="全选">
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<polyline points="9 11 12 14 22 4"/>
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/>
</svg>
</button>
<!-- 下载选中(带数字角标) -->
<button class="im-icon-btn im-download-selected-btn" id="imDownloadSelectedBtn" type="button"
title="下载选中" aria-label="下载选中">
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>
</svg>
<span id="imSelectedBadge" class="im-selected-badge">0</span>
</button>
</div>
<!-- 右侧:主按钮(开始/停止) -->
<button id="imStartStopBtn" class="geist-button im-main-btn" type="button">
<svg class="im-btn-icon-play" width="13" height="13" 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>
<svg class="im-btn-icon-stop" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none">
<rect x="5" y="5" width="14" height="14"/>
</svg>
<span class="im-btn-label">开始</span>
</button>
</div>
<button class="mobile-fab-toggle" data-fab-toggle type="button" aria-label="打开操作菜单">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 5v14M5 12h14" />
</svg>
</button>
</div>
<div id="app-footer"></div>
@@ -298,12 +330,11 @@
<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>
<script src="/static/common/js/footer.js?v=0.3.0"></script>
<script src="/static/common/js/mobile-fab.js?v=0.1.1"></script>
<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="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
<script src="/static/public/js/imagine.js?v=0.3.8"></script>
<script src="/static/public/js/imagine.js?v=0.3.9"></script>
</body>
</html>