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 页面 → 视觉上彻底