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

This commit is contained in:
XianYuDaXian committed 2026-03-14 09:16:07 +08:00
1 parent 3a6661c8df
commit 63a0f9188e
3 files changed
+459 -19

No files matched your search

+227
View File
@@ -824,3 +824,230 @@ html[data-theme='dark'] .candidate-meta {
grid-column: auto;
}
}
/* ============================
NSFW 工作台 移动端双行 Sticky Bar(<=900px)
============================ */
@media (max-width: 900px) {
body {
padding-bottom: 108px !important;
}
.nsfw-sticky-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 200;
background: #fff;
border-top: 1px solid var(--border);
padding: 8px 14px;
padding-bottom: calc(8px + env(safe-area-inset-bottom, 0px));
box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.07);
display: flex;
flex-direction: column;
gap: 0;
}
.nsfw-sticky-row {
display: flex;
align-items: center;
gap: 7px;
padding: 5px 0;
}
.nsfw-sticky-row-sep {
height: 1px;
background: var(--border);
margin: 0 -2px;
opacity: 0.6;
}
.nsfw-icon-divider {
width: 1px;
height: 22px;
background: var(--border);
flex-shrink: 0;
}
.nsfw-icon-group {
display: flex;
align-items: center;
gap: 4px;
flex: 1;
justify-content: flex-start;
}
.nsfw-icon-btn {
width: 36px;
height: 36px;
border: 1px solid var(--border);
border-radius: 9px;
background: #fff;
color: var(--fg);
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
flex-shrink: 0;
transition: background .15s ease;
padding: 0;
position: relative;
}
.nsfw-icon-btn:active {
background: var(--accents-1);
}
.nsfw-icon-btn--danger {
color: #ef4444;
border-color: #fecaca;
}
.nsfw-icon-btn--danger:active {
background: #fef2f2;
}
.nsfw-main-btn {
height: 36px;
padding: 0 14px;
font-size: 13px;
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 5px;
flex-shrink: 0;
border-radius: 9px;
min-width: 84px;
justify-content: center;
margin-left: auto;
}
.nsfw-main-btn.is-stop {
background: #fee2e2 !important;
color: #ef4444 !important;
border: 1px solid #fecaca !important;
}
/* 视频行 */
.nsfw-sticky-row--video {
opacity: 0.46;
pointer-events: none;
transition: opacity .2s ease;
}
.nsfw-sticky-row--video.has-selected {
opacity: 1;
pointer-events: auto;
}
.nsfw-selected-label {
flex: 1;
font-size: 11px;
color: var(--accents-5);
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.nsfw-sticky-row--video.has-selected .nsfw-selected-label {
color: #2563eb;
font-weight: 500;
}
.nsfw-video-actions {
display: flex;
align-items: center;
gap: 5px;
flex-shrink: 0;
}
/* Settings Overlay */
.nsfw-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);
}
.nsfw-settings-overlay.active {
display: block;
}
/* Settings Bottom Sheet */
.nsfw-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: 80vh;
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%);
}
.nsfw-settings-sheet.open {
transform: translateY(0);
}
.nsfw-settings-sheet-handle {
display: flex;
justify-content: center;
padding: 12px 0 6px;
cursor: pointer;
}
.nsfw-settings-sheet-handle::after {
content: '';
width: 36px;
height: 4px;
border-radius: 999px;
background: var(--border);
}
.nsfw-settings-sheet-title {
font-size: 14px;
font-weight: 600;
color: var(--accents-7);
margin-bottom: 14px;
text-align: center;
}
}
/* 暗色主题 */
html[data-theme='dark'] .nsfw-sticky-bar {
background: #111821;
border-top-color: #2b3440;
box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.3);
}
html[data-theme='dark'] .nsfw-icon-btn {
background: #1a2330;
border-color: #3b4654;
color: #c9d6e5;
}
html[data-theme='dark'] .nsfw-icon-btn:active {
background: #243244;
}
html[data-theme='dark'] .nsfw-main-btn.is-stop {
background: #2d1414 !important;
color: #f87171 !important;
border-color: #7f1d1d !important;
}
html[data-theme='dark'] .nsfw-settings-sheet {
background: #111821;
color: #e2e8f0;
}
+150 -1
View File
@@ -1,4 +1,4 @@
(() => {
(() => {
const imagePromptInput = document.getElementById('imagePromptInput');
const videoPromptInput = document.getElementById('videoPromptInput');
const ratioSelect = document.getElementById('ratioSelect');
@@ -1818,4 +1818,153 @@
updateCounters();
updateImageButtons();
updateVideoButtons();
/**
* NSFW 工作台 双行 Sticky Bar 初始化
*/
function initNsfwStickyBar() {
/* --- 代理映射 --- */
const proxyMap = {
imClearImagesBtn: 'clearImagesBtn',
imNextBatchBtn: 'nextBatchBtn',
imClearVideosBtn: 'clearVideosBtn',
};
Object.entries(proxyMap).forEach(([sId, oId]) => {
const sEl = document.getElementById(sId);
const oEl = document.getElementById(oId);
if (sEl && oEl) sEl.addEventListener('click', () => oEl.click());
});
/* --- 候选图主按钮:生成6张/停止候选 --- */
const imGenBtn = document.getElementById('imGenerateBtn');
const genPlayIcon = imGenBtn && imGenBtn.querySelector('.nsfw-btn-play');
const genStopIcon = imGenBtn && imGenBtn.querySelector('.nsfw-btn-stop');
const genLabel = imGenBtn && imGenBtn.querySelector('.nsfw-btn-label');
function syncGenBtn() {
if (!imGenBtn) return;
const isRunning = generateBatchBtn && generateBatchBtn.dataset.running === '1';
imGenBtn.classList.toggle('is-stop', isRunning);
if (genPlayIcon) genPlayIcon.style.display = isRunning ? 'none' : '';
if (genStopIcon) genStopIcon.style.display = isRunning ? '' : 'none';
if (genLabel) genLabel.textContent = isRunning ? '停止候选' : '生成 6 张';
}
if (imGenBtn) {
imGenBtn.addEventListener('click', () => {
const isRunning = generateBatchBtn && generateBatchBtn.dataset.running === '1';
(isRunning ? stopBatchBtn : generateBatchBtn) && (isRunning ? stopBatchBtn : generateBatchBtn).click();
});
}
/* 观察 generateBatchBtn 的 data-running 属性和 disabled 状态 */
if (generateBatchBtn) {
new MutationObserver(syncGenBtn).observe(generateBatchBtn, { attributes: true, attributeFilter: ['data-running', 'disabled'] });
}
syncGenBtn();
/* --- 视频主按钮:生成视频/中断视频 --- */
const imVideoBtn = document.getElementById('imStartVideoBtn');
const vidPlayIcon = imVideoBtn && imVideoBtn.querySelector('.nsfw-btn-play');
const vidStopIcon = imVideoBtn && imVideoBtn.querySelector('.nsfw-btn-stop');
const vidLabel = imVideoBtn && imVideoBtn.querySelector('.nsfw-btn-label');
function syncVideoBtn() {
if (!imVideoBtn) return;
const isRunning = startVideoBtn && startVideoBtn.dataset.running === '1';
const hasSelected = startVideoBtn && !startVideoBtn.disabled;
imVideoBtn.classList.toggle('is-stop', isRunning);
imVideoBtn.disabled = !isRunning && !hasSelected;
if (vidPlayIcon) vidPlayIcon.style.display = isRunning ? 'none' : '';
if (vidStopIcon) vidStopIcon.style.display = isRunning ? '' : 'none';
if (vidLabel) vidLabel.textContent = isRunning ? '中断视频' : '生成视频';
}
if (imVideoBtn) {
imVideoBtn.addEventListener('click', () => {
const isRunning = startVideoBtn && startVideoBtn.dataset.running === '1';
(isRunning ? stopVideoBtn : startVideoBtn) && (isRunning ? stopVideoBtn : startVideoBtn).click();
});
}
if (startVideoBtn) {
new MutationObserver(syncVideoBtn).observe(startVideoBtn, { attributes: true, attributeFilter: ['data-running', 'disabled'] });
}
syncVideoBtn();
/* --- 候选图已选状态同步 → 第二行高亮 --- */
const videoRow = document.getElementById('nsfwVideoRow');
const selectedLabel = document.getElementById('nsfwSelectedLabel');
function syncSelected() {
if (!selectedMeta || !videoRow || !selectedLabel) return;
const text = String(selectedMeta.textContent || '').trim();
const hasSelected = !text.includes('未选择');
videoRow.classList.toggle('has-selected', hasSelected);
// 截短显示
if (hasSelected) {
selectedLabel.textContent = text.length > 22 ? text.slice(0, 22) + '…' : text;
} else {
selectedLabel.textContent = '未选择候选图';
}
syncVideoBtn();
}
if (selectedMeta) {
new MutationObserver(syncSelected).observe(selectedMeta, { characterData: true, childList: true, subtree: true });
syncSelected();
}
/* --- 设置 Bottom Sheet(画面比例、并发、分辨率、时长、NSFW) --- */
const settingsBtn = document.getElementById('nsfwSettingsBtn');
const settingsOverlay = document.getElementById('nsfwSettingsOverlay');
let settingsSheet = document.getElementById('nsfwSettingsSheet');
if (!settingsSheet) {
settingsSheet = document.createElement('div');
settingsSheet.id = 'nsfwSettingsSheet';
settingsSheet.className = 'nsfw-settings-sheet';
const handle = document.createElement('div');
handle.className = 'nsfw-settings-sheet-handle';
settingsSheet.appendChild(handle);
const titleEl = document.createElement('div');
titleEl.className = 'nsfw-settings-sheet-title';
titleEl.textContent = '生成参数';
settingsSheet.appendChild(titleEl);
// 将参数表单(画面比例、并发、分辨率、时长、NSFW 这些 field,不含 textarea)移入 sheet
const formGrid = document.querySelector('.form-grid');
if (formGrid) {
const nonWideFields = formGrid.querySelectorAll('.field:not(.field-wide)');
const sheetGrid = document.createElement('div');
sheetGrid.className = 'form-grid';
sheetGrid.style.cssText = 'grid-template-columns: 1fr 1fr; gap: 10px 14px;';
nonWideFields.forEach(f => sheetGrid.appendChild(f));
settingsSheet.appendChild(sheetGrid);
}
document.body.appendChild(settingsSheet);
}
const openSettings = () => {
if (!settingsSheet || !settingsOverlay) return;
settingsOverlay.classList.add('active');
requestAnimationFrame(() => settingsSheet.classList.add('open'));
};
const 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('.nsfw-settings-sheet-handle');
if (sheetHandle) sheetHandle.addEventListener('click', closeSettings);
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && settingsSheet && settingsSheet.classList.contains('open')) closeSettings();
});
}
initNsfwStickyBar();
})();
+82 -18
View File
@@ -19,7 +19,7 @@
<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/common/css/toast.css" rel="stylesheet">
<link href="/static/public/css/nsfw.css?v=0.3.6" rel="stylesheet">
<link href="/static/public/css/nsfw.css?v=0.3.7" rel="stylesheet">
</head>
<body class="public-page min-h-screen flex flex-col" style="background-color: var(--bg);">
@@ -132,23 +132,88 @@
</div>
</main>
<div class="mobile-fab" data-mobile-fab>
<div class="mobile-fab-panel">
<div class="mobile-fab-actions">
<button class="geist-button" data-proxy-click="#generateBatchBtn" type="button">生成6张</button>
<button class="geist-button-outline" data-proxy-click="#stopBatchBtn" type="button">停候选</button>
<button class="geist-button-outline" data-proxy-click="#nextBatchBtn" type="button">再来一批</button>
<button class="geist-button-outline" data-proxy-click="#clearImagesBtn" type="button">清图片</button>
<button class="geist-button" data-proxy-click="#startVideoBtn" type="button">开始视频</button>
<button class="geist-button-outline" data-proxy-click="#stopVideoBtn" type="button">停视频</button>
<button class="geist-button-outline" data-proxy-click="#clearVideosBtn" type="button">清视频</button>
<!-- 移动端设置遮罩(Bottom Sheet) -->
<div id="nsfwSettingsOverlay" class="nsfw-settings-overlay"></div>
<!-- 移动端双行 Sticky Bar -->
<div id="nsfwStickyBar" class="nsfw-sticky-bar">
<!-- 第一行:候选图操作 -->
<div class="nsfw-sticky-row nsfw-sticky-row--image">
<!-- 左:设置 -->
<button class="nsfw-icon-btn" id="nsfwSettingsBtn" 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">
<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="nsfw-icon-divider"></div>
<!-- 中:图标操作 -->
<div class="nsfw-icon-group">
<!-- 清空图片 -->
<button class="nsfw-icon-btn nsfw-icon-btn--danger" id="imClearImagesBtn" type="button" title="清空图片">
<svg width="16" height="16" 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="nsfw-icon-btn" id="imNextBatchBtn" type="button" title="再来一批">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<polyline points="1 4 1 10 7 10"/>
<path d="M3.51 15a9 9 0 1 0 .49-3.5"/>
</svg>
</button>
</div>
<!-- 右:主按钮(生成6张/停止候选) -->
<button id="imGenerateBtn" class="geist-button nsfw-main-btn" type="button">
<svg class="nsfw-btn-play" width="12" height="12" 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="nsfw-btn-stop" width="11" height="11" 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="nsfw-btn-label">生成 6 张</span>
</button>
</div>
<!-- 行分隔线 -->
<div class="nsfw-sticky-row-sep"></div>
<!-- 第二行:视频操作(候选图已选时高亮) -->
<div class="nsfw-sticky-row nsfw-sticky-row--video" id="nsfwVideoRow">
<!-- 左:已选图摘要 -->
<div class="nsfw-selected-label" id="nsfwSelectedLabel">未选择候选图</div>
<!-- 右侧视频操作 -->
<div class="nsfw-video-actions">
<!-- 清空视频 -->
<button class="nsfw-icon-btn nsfw-icon-btn--danger" id="imClearVideosBtn" type="button" title="清空视频结果">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="2" width="20" height="20" rx="2.18"/><line x1="7" y1="2" x2="7" y2="22"/>
<line x1="17" y1="2" x2="17" y2="22"/><line x1="2" y1="12" x2="22" y2="12"/>
<line x1="2" y1="7" x2="7" y2="7"/><line x1="2" y1="17" x2="7" y2="17"/>
<line x1="17" y1="17" x2="22" y2="17"/><line x1="17" y1="7" x2="22" y2="7"/>
</svg>
</button>
<!-- 生成视频/中断视频 主按钮 -->
<button id="imStartVideoBtn" class="geist-button nsfw-main-btn nsfw-main-btn--video" type="button" disabled>
<svg class="nsfw-btn-play" width="12" height="12" 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="nsfw-btn-stop" width="11" height="11" 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="nsfw-btn-label">生成视频</span>
</button>
</div>
</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="lightbox" class="image-lightbox">
@@ -199,11 +264,10 @@
<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="/static/public/js/nsfw.js?v=0.3.5"></script>
<script src="/static/public/js/nsfw.js?v=0.3.6"></script>
</body>
</html>