feat(workbench): 三工序图片区视觉对齐 + 任务中心聚合 + 工具台头部筛选

- model-photo / platform-cover · 头部 toolbar 落地: 时间 / 模特(平台) chip 下拉 + 折叠搜索
- model-photo / platform-cover · 图片卡片样式同步图片创作 (.io-cell): bg / hover / .gen 脉冲 / .err 红框
- model-photo / platform-cover · 单图 hover overlay: 再次生成 + 下载 + 更多(加入资产库/删除)
- model-photo / platform-cover · 批次底栏: 再次生成图标统一 + 更多 menu(全部加入资产库/删除该批)
- model-photo · 修 TDZ bug: renderModelMini 调用挪到 MODELS 声明后, 解决整页崩溃
- model-photo · 去掉冗余 pv-summary, 商品自动选最近编辑, task 写入 name 字段
- image-optimize · 单图右上加再次生成图标, 加入 fs-image-tasks-image 与任务中心打通
- image-optimize · 输入区拆 3 行: + 在顶 / textarea 满宽 / 发送在底栏右; 参考图缩略与加号同 64×64
- asset-factory · 任务中心加时间 chip + image 类型 + 跳转表; 删冗余类型列
- pipeline · stage2 商品卡换商品库风格 + AI 生成三视图主 CTA + .tri-missing-badge[hidden] CSS 修复
This commit is contained in:
iye
2026-05-22 19:35:36 +08:00
parent f420af2069
commit 04335f3269
25 changed files with 13042 additions and 2700 deletions
+144 -10
View File
@@ -250,6 +250,58 @@
@media (max-width: 900px) {
#${DRAWER_ID} .pf-upload-row { grid-template-columns: 1fr; }
}
/* 放弃填写 · 确认弹窗 · 覆盖在 drawer 上 */
#npd-confirm-bg {
position: fixed; inset: 0; z-index: 1200;
background: rgba(21, 20, 15, .42);
display: grid; place-items: center;
padding: 16px;
}
#npd-confirm-bg[hidden] { display: none; }
.npd-confirm-modal {
width: min(420px, 92vw);
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
box-shadow: 0 16px 48px rgba(21, 20, 15, .18);
overflow: hidden;
}
.npd-confirm-modal .dh {
display: flex; align-items: center; gap: 12px;
padding: 18px 20px 14px;
}
.npd-confirm-modal .dh .ic {
width: 36px; height: 36px; flex-shrink: 0;
border-radius: var(--r-md);
background: rgba(180, 30, 30, .08); color: var(--accent-crimson);
display: grid; place-items: center;
}
.npd-confirm-modal .dh .ic svg { width: 18px; height: 18px; }
.npd-confirm-modal .dh .ti { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.npd-confirm-modal .dh .ti strong { font-size: 14.5px; color: var(--accent-black); font-weight: 600; }
.npd-confirm-modal .dh .ti .mono { font-family: var(--font-mono); font-size: 11.5px; color: var(--black-alpha-48); letter-spacing: .02em; line-height: 1.5; }
.npd-confirm-modal .df {
display: flex; gap: 8px;
padding: 0 20px 18px;
justify-content: flex-end;
}
.npd-confirm-modal .df button {
height: 32px; padding: 0 14px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
font-size: 12.5px; color: var(--accent-black);
font-family: inherit; cursor: pointer;
transition: border-color var(--t-base), background var(--t-base), color var(--t-base);
}
.npd-confirm-modal .df button:hover { border-color: var(--heat-20); background: var(--heat-12); color: var(--heat); }
.npd-confirm-modal .df button.danger { color: var(--accent-crimson); }
.npd-confirm-modal .df button.danger:hover {
background: rgba(180, 30, 30, .08);
border-color: rgba(180, 30, 30, .35);
color: var(--accent-crimson);
}
`;
const HTML = `
@@ -341,11 +393,33 @@
</button>
</div>
</aside>
<!-- 放弃填写 · 确认弹窗(覆盖在 drawer 之上) -->
<div id="npd-confirm-bg" hidden>
<div class="npd-confirm-modal" role="dialog" aria-label="放弃填写">
<div class="dh">
<div class="ic">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><path d="M12 9v4M12 17h.01"/></svg>
</div>
<div class="ti">
<strong>放弃当前填写?</strong>
<span class="mono">// 已填写的商品名、卖点、图片等内容将不会保留</span>
</div>
</div>
<div class="df">
<button type="button" data-act="keep">继续填写</button>
<button type="button" class="danger" data-act="discard">放弃退出</button>
</div>
</div>
</div>
`;
/* ---------- DOM refs (populated by ensureInjected) ---------- */
let injected = false;
let bg, drawer, $f, $grid, $bullets, $blInput;
let confirmBg;
let _forceClose = false; // 保存成功后绕过脏检查
let _initialCat = ''; // 打开时类目下拉的初始值,用来判断是否被改过
let currentOpts = {};
const PF_MAX = 5;
let pfFiles = []; // { id, dataUrl, name }
@@ -368,8 +442,9 @@
wrap.innerHTML = HTML;
while (wrap.firstChild) document.body.appendChild(wrap.firstChild);
bg = document.getElementById(DRAWER_BG_ID);
drawer = document.getElementById(DRAWER_ID);
bg = document.getElementById(DRAWER_BG_ID);
drawer = document.getElementById(DRAWER_ID);
confirmBg = document.getElementById('npd-confirm-bg');
$f = {
name: drawer.querySelector('[data-f="name"]'),
cat: drawer.querySelector('[data-f="cat"]'),
@@ -549,6 +624,7 @@
if (drawer.classList.contains('show')) return; // 已开则不重复锁
currentOpts = opts || {};
resetForm();
_initialCat = $f.cat.value; // 快照「打开时类目」用于脏检查
bg.classList.add('show');
drawer.classList.add('show');
drawer.setAttribute('aria-hidden', 'false');
@@ -556,9 +632,49 @@
setTimeout(() => $f.name.focus(), 280);
}
function close() {
/* ---------- 脏检查 + 退出确认 ---------- */
function isDirty() {
if (!injected) return false;
if (($f.name.value || '').trim()) return true;
if (($f.target.value || '').trim()) return true;
if (($blInput.value || '').trim()) return true;
if (pfFiles.length > 0) return true;
if ($bullets.querySelectorAll('.bl-item').length > 0) return true;
if ($f.cat.value !== _initialCat) return true;
return false;
}
function confirmDiscard() {
return new Promise(resolve => {
confirmBg.hidden = false;
const buttons = confirmBg.querySelectorAll('button[data-act]');
function done(choice) {
confirmBg.hidden = true;
buttons.forEach(b => b.onclick = null);
confirmBg.onclick = null;
document.removeEventListener('keydown', escHandler, true);
resolve(choice === 'discard');
}
function escHandler(e) {
if (e.key === 'Escape') {
e.stopPropagation();
done('keep');
}
}
buttons.forEach(b => b.onclick = () => done(b.dataset.act));
confirmBg.onclick = e => { if (e.target === confirmBg) done('keep'); };
// 用 capture · 截在外层 Esc 监听器之前
document.addEventListener('keydown', escHandler, true);
});
}
async function close() {
if (!injected) return;
if (!drawer.classList.contains('show')) return; // 已关则不重复解锁
if (!_forceClose && isDirty()) {
const ok = await confirmDiscard();
if (!ok) return;
}
bg.classList.remove('show');
drawer.classList.remove('show');
drawer.setAttribute('aria-hidden', 'true');
@@ -566,7 +682,20 @@
if (typeof currentOpts.onClose === 'function') currentOpts.onClose();
}
function closeForce() {
_forceClose = true;
try { close(); } finally { _forceClose = false; }
}
function save() {
// 兜底:用户在卖点输入框敲了字但没回车/没点「添加卖点」就直接点创建,
// 这里自动提交一次,避免静默丢失最后一条卖点(常见误操作)
if ($blInput && ($blInput.value || '').trim()) {
blAdd($blInput.value);
$blInput.value = '';
updateBlAddBtn();
}
const name = ($f.name.value || '').trim();
const cat = $f.cat.value;
const target = ($f.target.value || '').trim();
@@ -595,15 +724,14 @@
images,
imgs: images.length,
};
// 持久化到 localStorage('fs-extra-products'),让 products.html 下次加载时
// 自动从 storage 读出并 prepend 到 grid(否则用户在工作台创建后 → 跳详情 →
// 回商品库会看不到刚创建的商品)
// 持久化到 sessionStorage('fs-extra-products') · 仅当前标签页生命周期内有效
// 关闭标签页/浏览器后自动清空,不会跨会话累积演示残留
try {
const KEY = 'fs-extra-products';
const list = JSON.parse(localStorage.getItem(KEY) || '[]');
const list = JSON.parse(sessionStorage.getItem(KEY) || '[]');
list.push({
id: product.id,
name, cat,
name, cat, target,
tags: '',
assets: 0,
videos: 0,
@@ -611,13 +739,19 @@
date: new Date().toISOString().slice(0, 10),
createdAt: Date.now(),
});
localStorage.setItem(KEY, JSON.stringify(list));
sessionStorage.setItem(KEY, JSON.stringify(list));
} catch (e) { /* storage 不可用降级到只跳转 */ }
// 短期 sessionStorage 缓存完整 product(含图片 dataUrl),供 detail 页 ?id=new
// 读出后渲染。读完即清除,避免污染后续操作。
try {
sessionStorage.setItem('npd-last-created', JSON.stringify(product));
} catch (e) { /* dataUrl 可能过大 → 退化为不带图,detail 仍能渲染文本字段 */ }
if (typeof currentOpts.onSave === 'function') {
toast('商品已创建', '+ ' + name);
currentOpts.onSave(product);
close();
closeForce();
return;
}
// 默认行为: 不 close drawer · 跳转期间 drawer 仍覆盖 host 页面 → 视觉上彻底
+128 -8
View File
@@ -431,6 +431,15 @@ main { position: relative; background: var(--background-base); min-width: 0; }
}
.page-head .sub .mono { font-family: var(--font-mono); font-size: 11.5px; color: var(--black-alpha-48); letter-spacing: .04em; }
.page-head .actions { display: flex; gap: 10px; align-items: center; }
/* page-head 右上角主操作按钮 · 统一尺寸(对齐 .btn-lg),覆盖各页里 btn / btn-sm / btn-lg 混用 */
.page-head .actions > .btn,
.page-head .actions > a.btn,
.page-head .actions > button.btn {
height: 40px;
padding: 0 20px;
font-size: 13.5px;
}
.page-head .actions > .btn svg { width: 14px; height: 14px; }
.section-h { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 14px; }
.section-h h2 { font-size: 16px; font-weight: 600; letter-spacing: -.01em; color: var(--accent-black); }
@@ -1415,11 +1424,22 @@ table.t tbody tr:hover { background: var(--black-alpha-4); }
background: var(--background-lighter);
}
/* ─── Scrollbar ─── */
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--black-alpha-12); border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: var(--black-alpha-24); }
/* ─── Scrollbar · 全局隐藏可视滚动条,保留滚动能力
用 !important 覆盖各页 inline <style> 里残留的 scrollbar-width:thin / 自定义 webkit 滚动条 ─── */
* { scrollbar-width: none !important; -ms-overflow-style: none !important; }
*::-webkit-scrollbar {
width: 0 !important;
height: 0 !important;
display: none !important;
-webkit-appearance: none !important;
}
*::-webkit-scrollbar-track,
*::-webkit-scrollbar-thumb,
*::-webkit-scrollbar-thumb:hover,
*::-webkit-scrollbar-corner {
background: transparent !important;
display: none !important;
}
/* ─── Responsive ─── */
@media (max-width: 1100px) {
@@ -1456,10 +1476,29 @@ table.t tbody tr:hover { background: var(--black-alpha-4); }
letter-spacing: .03em;
border-radius: var(--r-sm);
box-shadow: 0 1px 2px rgba(0,0,0,.12);
pointer-events: none;
cursor: help;
user-select: none;
transition: background var(--t-base), box-shadow var(--t-base), transform var(--t-base);
}
.tri-missing-badge::before {
.tri-missing-badge[hidden] { display: none; }
.tri-missing-badge:hover {
box-shadow: 0 3px 8px rgba(0,0,0,.18);
transform: translateY(-1px);
}
.tri-missing-badge .ico {
width: 12px; height: 12px;
border: 1px solid currentColor;
border-radius: 50%;
display: grid; place-items: center;
font-size: 9px; font-weight: 700;
line-height: 1;
font-family: var(--font-mono);
}
.tri-missing-badge .ico::before { content: '!'; }
.tri-missing-badge .lbl-mono { font-family: var(--font-mono); letter-spacing: .04em; }
/* 兼容旧 markup(没有 .ico 子元素的也保留 ! 圆点) */
.tri-missing-badge:not(:has(.ico))::before {
content: '!';
width: 12px; height: 12px;
border: 1px solid currentColor;
@@ -1468,4 +1507,85 @@ table.t tbody tr:hover { background: var(--black-alpha-4); }
font-size: 9px; font-weight: 700;
line-height: 1;
}
.tri-missing-badge .lbl-mono { font-family: var(--font-mono); letter-spacing: .04em; }
/* hover popover · V2.1 dark tooltip with arrow */
.tri-missing-pop {
position: absolute;
top: calc(100% + 9px);
left: 0;
width: 252px;
padding: 11px 13px 12px;
background: var(--accent-black);
color: var(--accent-white);
border-radius: var(--r-md);
box-shadow: 0 8px 24px rgba(0,0,0,.20), 0 2px 6px rgba(0,0,0,.10);
opacity: 0; visibility: hidden;
transform: translateY(-4px);
transition: opacity .15s ease, visibility .15s ease, transform .15s ease;
pointer-events: none;
z-index: 30;
font-family: var(--font-sans);
font-weight: 400;
letter-spacing: 0;
text-transform: none;
text-align: left;
cursor: default;
}
.tri-missing-pop::before {
content: '';
position: absolute;
top: -5px; left: 14px;
width: 10px; height: 10px;
background: var(--accent-black);
transform: rotate(45deg);
border-radius: 2px;
}
.tri-missing-pop .pop-h {
display: flex; align-items: center; gap: 6px;
font-family: var(--font-mono);
font-size: 11px;
letter-spacing: .04em;
color: var(--heat);
margin-bottom: 6px;
}
.tri-missing-pop .pop-h svg { display: block; }
.tri-missing-pop .pop-body {
font-size: 12px;
line-height: 1.6;
color: rgba(255,255,255,.88);
}
.tri-missing-pop .pop-tip {
display: block;
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid rgba(255,255,255,.12);
font-family: var(--font-mono);
font-size: 11px;
line-height: 1.55;
letter-spacing: .02em;
color: rgba(255,255,255,.78);
}
.tri-missing-pop .pop-tip b {
color: var(--accent-white);
font-weight: 600;
letter-spacing: .03em;
}
.tri-missing-badge:hover .tri-missing-pop,
.tri-missing-badge:focus-within .tri-missing-pop {
opacity: 1; visibility: visible; transform: translateY(0);
transition-delay: .12s;
}
/* 让 popover 能溢出 thumb / 卡片 容器(否则被 overflow:hidden 裁切) */
.placeholder:has(.tri-missing-badge:hover),
.placeholder:has(.tri-missing-badge:focus-within) {
overflow: visible;
z-index: 4;
}
.product-card:has(.tri-missing-badge:hover),
.product-card:has(.tri-missing-badge:focus-within),
.asset-card:has(.tri-missing-badge:hover),
.asset-card:has(.tri-missing-badge:focus-within) {
overflow: visible;
z-index: 5;
}
+1 -1
View File
@@ -21,7 +21,7 @@ const NAV = [
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="4" width="18" height="16"/><path d="M7 4v16M16 4v16M3 9h18M3 15h18"/></svg>'
},
{
id: 'asset-factory', label: '图片生成', href: 'asset-factory.html', badge: '12',
id: 'asset-factory', label: '图片生成', href: 'asset-factory.html',
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l1.7 4.6L18 9l-4.3 1.4L12 15l-1.7-4.6L6 9l4.3-1.4L12 3z"/><path d="M19 16l.85 2.3L22 19l-2.15.7L19 22l-.85-2.3L16 19l2.15-.7L19 16z"/></svg>'
},
{