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:
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>'
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user