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:
+161
-190
@@ -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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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>
|
||||
|
||||
Reference in New Issue
Block a user