feat(workbench): 统一立绘详情页参考布局 · 三视图全 16:9 · 工作台批次追加

详情页 (pipeline / library / model-photo)
- 统一参考布局:大立绘+缩略 strip+查看大图,右栏 三视图+简介(标签 chip)+3 列属性表
- 底部仅留「下载」+「使用该资产」,去除收藏 / 关闭

三视图固定单张 16:9
- pipeline / library / model-photo / asset-factory / product-studio 全部同步
- 移除原 actor 3 列 3:4 拆图,改为单容器 16:9

图片工作台 (model-photo / platform-cover)
- 立即生成 + 全部重跑 + 单张重跑 均追加新批次到下方,旧批次保留
- 批量按钮下沉到每批次下方,与图片网格左对齐
- hover 重跑/采用 icon 缩小至 26px,右下角横向,无遮罩层
- 立即生成后不再自动新增「编辑中」草稿卡

新建商品 drawer
- 无 onSave 回调时默认跳转 product-detail
- 卖点新增 「+ 添加卖点」按钮(输入框下方独立行,左对齐)

product-detail
- 视频项目卡片状态 pill 改为 4 态(已完成/视频生成 4/6/已归档/故事板失败)
- 移除视频卡个体「通过/不通过/归档」状态切换
- 去掉冗余「通过」status 筛选;过滤逻辑兼容缺失按钮

sidebar (shell.js)
- 图片生成补 badge 12,团队去 badge

清理
- 删除 v2/ 历史镜像目录(与 电商AI平台/ 重复,Dockerfile build context 不依赖)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
iye
2026-05-21 21:12:03 +08:00
co-authored by Claude Opus 4.7
parent 2a3ae88dc5
commit 8a783ca36f
34 changed files with 2147 additions and 29201 deletions
+161 -190
View File
@@ -118,8 +118,9 @@
.model-visual .stack .placeholder { aspect-ratio: 3 / 4; }
.kit-visual { grid-template-columns: repeat(4, 1fr); }
.kit-visual .placeholder { aspect-ratio: 1 / 1; }
.tri-visual { grid-template-columns: repeat(3, 1fr); }
.tri-visual .placeholder { aspect-ratio: 1 / 1; }
.tri-visual { display: block; }
.tri-visual .placeholder { aspect-ratio: 16 / 9; }
.tri-visual .placeholder + .placeholder { display: none; }
/* ─── 任务中心 · section header ─── */
.section-h { display: flex; align-items: center; gap: 12px; margin-top: 24px; margin-bottom: 14px; }
@@ -432,9 +433,7 @@
</div>
<div class="factory-visual tri-visual">
<div class="placeholder"><span class="ph-frame"></span></div>
<div class="placeholder"><span class="ph-frame">侧面</span></div>
<div class="placeholder"><span class="ph-frame">背面</span></div>
<div class="placeholder"><span class="ph-frame"> / 侧 / 背 · 三视图</span></div>
</div>
</div>
</div>
@@ -501,117 +500,13 @@
</table>
</div>
<!-- ============= GRID VIEW ============= -->
<div class="history-grid" id="task-grid" hidden>
<div class="task-card history-card" data-status="ok" data-type="model" data-name="补水面膜 × Ava">
<button class="card-del-btn" type="button" title="删除任务" onclick="event.stopPropagation();" data-action="delete-task"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
<div class="placeholder"><span class="ph-frame">Ava · 1:1</span></div>
<div class="history-body">
<div class="history-name">补水面膜 × Ava</div>
<div class="history-type">模特上身图 · 4 张</div>
<div class="history-foot">
<span class="mono">// 05.19 · 14:30</span>
<span class="pill ok"><span class="dot"></span>已完成</span>
</div>
</div>
</div>
<div class="task-card history-card" data-status="ok" data-type="platform" data-name="精华液 × 平台套图">
<button class="card-del-btn" type="button" title="删除任务" onclick="event.stopPropagation();"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
<div class="placeholder"><span class="ph-frame">TB / XHS · 1:1</span></div>
<div class="history-body">
<div class="history-name">精华液 × 平台套图</div>
<div class="history-type">平台套图 · 4 张</div>
<div class="history-foot">
<span class="mono">// 05.19 · 14:25</span>
<span class="pill ok"><span class="dot"></span>已完成</span>
</div>
</div>
</div>
<div class="task-card history-card" data-status="gen" data-type="model" data-name="防晒霜 × Luna">
<button class="card-del-btn" type="button" title="删除任务" onclick="event.stopPropagation();"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
<div class="placeholder"><span class="ph-frame">Luna · 1:1</span></div>
<div class="history-body">
<div class="history-name">防晒霜 × Luna</div>
<div class="history-type">模特上身图 · 4 张</div>
<div class="history-foot">
<span class="mono">// 05.19 · 14:20</span>
<span class="pill info"><span class="dot"></span>生成中</span>
</div>
<div class="history-prog">
<div class="bar"><span style="width:65%"></span></div>
<span class="pct">65%</span>
</div>
</div>
</div>
<div class="task-card history-card" data-status="err" data-type="platform" data-name="面霜 × 平台套图">
<button class="card-del-btn" type="button" title="删除任务" onclick="event.stopPropagation();"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
<div class="placeholder"><span class="ph-frame">DY / PDD · 1:1</span></div>
<div class="history-body">
<div class="history-name">面霜 × 平台套图</div>
<div class="history-type">平台套图 · 4 张</div>
<div class="history-foot">
<span class="mono">// 05.19 · 14:15</span>
<span class="pill err"><span class="dot"></span>失败</span>
</div>
</div>
</div>
<div class="task-card history-card" data-status="ok" data-type="model" data-name="口红 × Mia">
<button class="card-del-btn" type="button" title="删除任务" onclick="event.stopPropagation();"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
<div class="placeholder"><span class="ph-frame">Mia · 1:1</span></div>
<div class="history-body">
<div class="history-name">口红 × Mia</div>
<div class="history-type">模特上身图 · 4 张</div>
<div class="history-foot">
<span class="mono">// 05.18 · 21:08</span>
<span class="pill ok"><span class="dot"></span>已完成</span>
</div>
</div>
</div>
<div class="task-card history-card" data-status="ok" data-type="platform" data-name="眼霜 × 平台套图">
<button class="card-del-btn" type="button" title="删除任务" onclick="event.stopPropagation();"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
<div class="placeholder"><span class="ph-frame">TB / DY · 1:1</span></div>
<div class="history-body">
<div class="history-name">眼霜 × 平台套图</div>
<div class="history-type">平台套图 · 8 张</div>
<div class="history-foot">
<span class="mono">// 05.18 · 18:42</span>
<span class="pill ok"><span class="dot"></span>已完成</span>
</div>
</div>
</div>
<div class="task-card history-card" data-status="ok" data-type="model" data-name="瑜伽裤 × Zoe">
<button class="card-del-btn" type="button" title="删除任务" onclick="event.stopPropagation();"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
<div class="placeholder"><span class="ph-frame">Zoe · 1:1</span></div>
<div class="history-body">
<div class="history-name">瑜伽裤 × Zoe</div>
<div class="history-type">模特上身图 · 12 张</div>
<div class="history-foot">
<span class="mono">// 05.18 · 11:20</span>
<span class="pill ok"><span class="dot"></span>已完成</span>
</div>
</div>
</div>
<div class="task-card history-card" data-status="ok" data-type="platform" data-name="咖啡粉 × 平台套图">
<button class="card-del-btn" type="button" title="删除任务" onclick="event.stopPropagation();"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
<div class="placeholder"><span class="ph-frame">XHS / AMZ · 1:1</span></div>
<div class="history-body">
<div class="history-name">咖啡粉 × 平台套图</div>
<div class="history-type">平台套图 · 4 张</div>
<div class="history-foot">
<span class="mono">// 05.17 · 16:50</span>
<span class="pill ok"><span class="dot"></span>已完成</span>
</div>
</div>
</div>
<!-- ============= GRID VIEW (JS 从 localStorage 动态渲染) ============= -->
<div class="history-grid" id="task-grid" hidden></div>
<!-- 空态 -->
<div id="tc-empty" hidden style="padding:60px 20px;text-align:center;color:var(--black-alpha-48);font-size:13px;line-height:1.6">
<div style="font-family:var(--font-mono);font-size:11px;letter-spacing:.04em;margin-bottom:6px;">// NO TASKS YET</div>
<div id="tc-empty-text">还没有任务,去上方选一个工序开始生成吧</div>
</div>
</div>
@@ -647,68 +542,112 @@ Shell.render({
});
/* ============================================================
任务中心 · projects.html 风格 (tabs + toolbar + 双视图)
任务中心 · 从 localStorage 读取(与 model-photo / platform-cover 共享)
============================================================ */
(function () {
'use strict';
const TYPE_LABEL = { model: '模特上身图', platform: '平台套图' };
const TYPE_LABEL = { model: '模特上身图', platform: '平台套图' };
const STATUS_LABEL = { ok: '已完成', gen: '生成中', err: '失败' };
const STATUS_PILL = { ok: 'ok', gen: 'info', err: 'err' };
const STATUS_PILL = { ok: 'ok', gen: 'info', err: 'err' };
const KEY_BY_TYPE = { model: 'fs-image-tasks-model', platform: 'fs-image-tasks-platform' };
const taskGrid = document.getElementById('task-grid');
const listTbody = document.getElementById('task-list-tbody');
const gridView = document.getElementById('task-grid');
const gridView = taskGrid;
const listView = document.getElementById('task-list-view');
const cards = [...taskGrid.querySelectorAll('.task-card')];
let cards = []; // 动态生成的 .task-card 元素数组(顺序与任务时间倒序一致)
const state = { filter: 'all', type: 'all', search: '', view: 'list' };
function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c])); }
// 任务行点击 → 跳转到对应工作台,携带商品名(任务名一般是「商品 × 模特/平台」格式)
function goToWorkbench(type, name) {
const productName = (name || '').split(/\s[×x]\s/)[0].trim();
const q = '?t=' + Date.now() + (productName ? '&product=' + encodeURIComponent(productName) : '');
const url = (type === 'model') ? 'model-photo.html' + q
: (type === 'platform') ? 'platform-cover.html' + q
: 'projects-new.html' + q;
/* ---------- localStorage 读写 ---------- */
function loadType(type) {
try { return JSON.parse(localStorage.getItem(KEY_BY_TYPE[type]) || '[]'); } catch (e) { return []; }
}
function saveType(type, arr) {
try { localStorage.setItem(KEY_BY_TYPE[type], JSON.stringify(arr)); } catch (e) {}
}
function loadAllTasks() {
const all = [];
Object.keys(KEY_BY_TYPE).forEach(type => {
loadType(type).forEach(t => all.push({ ...t, type })); // type 兜底
});
all.sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));
return all;
}
/* ---------- task → 渲染辅助 ---------- */
function subTextOf(t) {
const lbl = TYPE_LABEL[t.type] || t.type;
const count = (t.snap && t.snap.count) || 4;
return `${lbl} · ${count}`;
}
function thumbLabelOf(t) {
// 取「商品 × 模特/平台」中右半作为缩略图占位文字
const parts = (t.name || '').split(/\s[×x]\s/);
return (parts[1] || parts[0] || '—').slice(0, 8);
}
/* ---------- 点击行 / 卡片 → 跳转工作台(携带 taskId) ---------- */
function goToWorkbench(t) {
const url = (t.type === 'model' ? 'model-photo.html' : 'platform-cover.html')
+ '?taskId=' + encodeURIComponent(t.id);
location.href = url;
}
/* ---------- 1. 从卡片生成 list 表行 (单数据源) ---------- */
function rowFor(card) {
const name = card.dataset.name;
const type = card.dataset.type;
const status = card.dataset.status;
const subText = (card.querySelector('.history-type')?.textContent || '').trim();
const timeText = (card.querySelector('.history-foot .mono')?.textContent || '').replace(/^\/\/\s*/, '');
const pct = card.querySelector('.history-prog .pct')?.textContent || '';
const pillClass = STATUS_PILL[status] || 'info';
const pillLabel = STATUS_LABEL[status] || status;
/* ---------- 1. 从 task 数据生成卡片 + list 行 ---------- */
function cardFor(t) {
const card = document.createElement('div');
card.className = 'task-card history-card';
card.dataset.status = t.status;
card.dataset.type = t.type;
card.dataset.name = t.name;
card.dataset.taskId = t.id;
card.style.cursor = 'pointer';
card.innerHTML = `
<button class="card-del-btn" type="button" title="删除任务">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg>
</button>
<div class="placeholder"><span class="ph-frame">${esc(thumbLabelOf(t))}</span></div>
<div class="history-body">
<div class="history-name">${esc(t.name)}</div>
<div class="history-type">${esc(subTextOf(t))}</div>
<div class="history-foot">
<span class="mono">// ${esc(t.time || '')}</span>
<span class="pill ${STATUS_PILL[t.status] || 'info'}"><span class="dot"></span>${esc(STATUS_LABEL[t.status] || t.status)}</span>
</div>
</div>
`;
return card;
}
function rowFor(t) {
const tr = document.createElement('tr');
tr.dataset.taskRow = '1';
tr.dataset.name = name;
tr.dataset.type = type;
tr.dataset.status = status;
tr.addEventListener('click', () => goToWorkbench(type, name));
tr.dataset.name = t.name;
tr.dataset.type = t.type;
tr.dataset.status = t.status;
tr.dataset.taskId = t.id;
tr.addEventListener('click', () => goToWorkbench(t));
tr.innerHTML = `
<td>
<div class="task-name-cell">
<div class="placeholder task-thumb"><span class="ph-frame">${esc(name.split(' ')[0] || '')}</span></div>
<div class="placeholder task-thumb"><span class="ph-frame">${esc(thumbLabelOf(t))}</span></div>
<div>
<div class="task-name">${esc(name)}</div>
<div class="task-sub">${esc(subText)}</div>
<div class="task-name">${esc(t.name)}</div>
<div class="task-sub">${esc(subTextOf(t))}</div>
</div>
</div>
</td>
<td><span class="muted">${esc(TYPE_LABEL[type] || type)}</span></td>
<td>${status === 'gen'
? `<div class="task-list-prog"><div class="bar"><span style="width:${pct || '60%'}"></span></div><span class="pct">${esc(pct || '60%')}</span></div>`
: (status === 'ok' ? '<span class="muted-2 mono" style="font-size:11px;">已完成</span>' : '<span class="muted-2 mono" style="font-size:11px;">—</span>')}</td>
<td><span class="pill ${pillClass}"><span class="dot"></span>${esc(pillLabel)}</span></td>
<td class="muted-2">${esc(timeText)}</td>
<td><span class="muted">${esc(TYPE_LABEL[t.type] || t.type)}</span></td>
<td>${t.status === 'gen'
? `<div class="task-list-prog"><div class="bar"><span style="width:60%"></span></div><span class="pct">60%</span></div>`
: (t.status === 'ok' ? '<span class="muted-2 mono" style="font-size:11px;">已完成</span>' : '<span class="muted-2 mono" style="font-size:11px;">—</span>')}</td>
<td><span class="pill ${STATUS_PILL[t.status] || 'info'}"><span class="dot"></span>${esc(STATUS_LABEL[t.status] || t.status)}</span></td>
<td class="muted-2">${esc(t.time || '')}</td>
<td>
<div class="row-action">
<span class="row-more"><svg width="14" height="14" viewBox="0 0 16 16"><circle cx="3" cy="8" r="1.2" fill="currentColor"/><circle cx="8" cy="8" r="1.2" fill="currentColor"/><circle cx="13" cy="8" r="1.2" fill="currentColor"/></svg>
@@ -717,24 +656,54 @@ Shell.render({
</div>
</td>
`;
// 阻止 row-more 区域冒泡触发整行点击
tr.querySelector('.row-more').addEventListener('click', e => e.stopPropagation());
return tr;
}
cards.forEach(card => {
const tr = rowFor(card);
listTbody.appendChild(tr);
card._listRow = tr;
tr._card = card;
});
/* ---------- 2. 全量重渲染(load → render → filter) ---------- */
function renderAll() {
// 清空 grid / list
taskGrid.innerHTML = '';
listTbody.innerHTML = '';
cards = [];
/* ---------- 2. 构建类型 chip 菜单 ---------- */
const tasks = loadAllTasks();
tasks.forEach(t => {
const card = cardFor(t);
const row = rowFor(t);
taskGrid.appendChild(card);
listTbody.appendChild(row);
card._listRow = row;
card._task = t;
row._card = card;
cards.push(card);
// 卡片点击 → 跳工作台
card.addEventListener('click', e => {
if (e.target.closest('.card-del-btn')) return;
goToWorkbench(t);
});
// 卡片删除按钮
card.querySelector('.card-del-btn').addEventListener('click', e => {
e.stopPropagation();
openDelConfirm(card);
});
});
// 类型 chip 菜单(基于现有 task 的 type 集合,动态)
rebuildTypeMenu();
applyFilter();
}
/* ---------- 3. 构建类型 chip 菜单(基于当前 cards 的 type 集合) ---------- */
const checkSvg = '<svg class="mi-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg>';
const typeMenu = document.querySelector('.chip-wrap[data-key="type"] .chip-menu');
const typeOptions = [...new Set(cards.map(c => c.dataset.type))];
typeMenu.innerHTML = `<div class="mi selected" data-value="all">${checkSvg}<span>全部任务类型</span></div><div class="mi-sep"></div>`
+ typeOptions.map(v => `<div class="mi" data-value="${esc(v)}">${checkSvg}<span>${esc(TYPE_LABEL[v] || v)}</span></div>`).join('');
function rebuildTypeMenu() {
const typeOptions = [...new Set(cards.map(c => c.dataset.type))];
typeMenu.innerHTML = `<div class="mi selected" data-value="all">${checkSvg}<span>全部任务类型</span></div><div class="mi-sep"></div>`
+ typeOptions.map(v => `<div class="mi" data-value="${esc(v)}">${checkSvg}<span>${esc(TYPE_LABEL[v] || v)}</span></div>`).join('');
syncTypeChip();
}
function syncTypeChip() {
const wrap = document.querySelector('.chip-wrap[data-key="type"]');
@@ -778,6 +747,24 @@ Shell.render({
document.getElementById('tc-result-meta').innerHTML = `// 显示 <span class="count">${visible}</span> / ${cards.length} 个任务`;
document.getElementById('tc-clear').hidden = !(state.search || state.type !== 'all');
// 空态
const emptyEl = document.getElementById('tc-empty');
const emptyText = document.getElementById('tc-empty-text');
if (cards.length === 0) {
emptyEl.hidden = false;
emptyText.textContent = '还没有任务,去上方选一个工序开始生成吧';
listView.hidden = true; gridView.hidden = true;
} else if (visible === 0) {
emptyEl.hidden = false;
emptyText.textContent = '没有符合筛选条件的任务';
// 视图本身保留,空表头也保留
} else {
emptyEl.hidden = true;
// 恢复 view 显示(可能在 length===0 分支被隐藏)
if (state.view === 'list') { listView.hidden = false; gridView.hidden = true; }
else { listView.hidden = true; gridView.hidden = false; }
}
}
/* ---------- 4. 事件绑定 ---------- */
@@ -835,7 +822,7 @@ Shell.render({
});
});
/* ---------- 5. 删除 modal + 配对删除 (复用 projects.html 模式) ---------- */
/* ---------- 6. 删除 modal + 同步 localStorage ---------- */
const delBg = document.getElementById('tc-del-bg');
const delBody = document.getElementById('tc-del-body');
const delCancel = document.getElementById('tc-del-cancel');
@@ -853,37 +840,21 @@ Shell.render({
delBg.addEventListener('click', e => { if (e.target === delBg) closeDelConfirm(); });
delOk.addEventListener('click', () => {
if (!_delTarget) return;
const card = _delTarget._card || _delTarget; // 可能传 row (with ._card) 或 card (with ._listRow)
const row = card._listRow;
const card = _delTarget._card || _delTarget; // 可能传 row 或 card
const taskId = card.dataset.taskId;
const taskType = card.dataset.type;
const name = card.dataset.name;
card.remove();
if (row) row.remove();
// 同步 cards 数组
const idx = cards.indexOf(card);
if (idx >= 0) cards.splice(idx, 1);
// 从 localStorage 移除对应任务
if (taskType && KEY_BY_TYPE[taskType]) {
const arr = loadType(taskType).filter(t => t.id !== taskId);
saveType(taskType, arr);
}
closeDelConfirm();
Shell.toast('已删除', name);
applyFilter();
renderAll();
});
// 绑定网格卡 删除按钮
document.querySelectorAll('.task-card .card-del-btn').forEach(btn => {
btn.addEventListener('click', e => {
e.stopPropagation();
const card = btn.closest('.task-card');
if (card) openDelConfirm(card);
});
});
// 绑定网格卡片点击 → 跳工作台
cards.forEach(card => {
card.style.cursor = 'pointer';
card.addEventListener('click', e => {
if (e.target.closest('.card-del-btn')) return;
goToWorkbench(card.dataset.type, card.dataset.name);
});
});
// 绑定列表行 删除按钮 (事件委托, 因为是动态生成)
// 列表行 删除按钮(事件委托,因为是动态生成)
listTbody.addEventListener('click', e => {
const btn = e.target.closest('.mi-del-task');
if (!btn) return;
@@ -892,8 +863,8 @@ Shell.render({
if (tr) openDelConfirm(tr);
});
/* ---------- 6. 初始化 ---------- */
applyFilter();
/* ---------- 7. 初始化 ---------- */
renderAll();
})();
</script>
</body>