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>
+76 -4
View File
@@ -204,6 +204,25 @@
font-size: 13.5px;
}
#${DRAWER_ID} .bullet-list .bl-add { background: transparent; border-style: dashed; }
#${DRAWER_ID} .bullet-list .bl-add-row {
margin: 8px 0 0;
display: none;
padding: 0;
background: transparent;
border: 0;
}
#${DRAWER_ID} .bullet-list .bl-add-row.show { display: flex; justify-content: flex-start; }
#${DRAWER_ID} .bl-add-btn {
display: inline-flex; align-items: center; gap: 6px;
padding: 7px 14px;
background: var(--heat); color: #fff;
border: 0; border-radius: var(--r-md);
font-size: 13px; font-weight: 600;
cursor: pointer; font-family: inherit;
transition: filter var(--t-base);
}
#${DRAWER_ID} .bl-add-btn:hover { filter: brightness(.94); }
#${DRAWER_ID} .bl-add-btn svg { width: 12px; height: 12px; }
#${DRAWER_ID} .bullet-list .num {
width: 22px; height: 22px;
background: var(--surface);
@@ -298,7 +317,13 @@
<div class="field" style="margin-bottom: 0;">
<label class="field-label">核心卖点<span class="req">*</span></label>
<ul class="bullet-list" data-f="bullets">
<li class="bl-add"><span class="num">+</span><input class="bl-input" placeholder="添加新卖点 · 回车确认"></li>
<li class="bl-add"><span class="num">+</span><input class="bl-input" placeholder="添加新卖点 · 回车或点击下方按钮"></li>
<li class="bl-add-row" data-f="add-row">
<button type="button" class="bl-add-btn" data-act="bl-add">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"><path d="M8 3v10M3 8h10"/></svg>
添加卖点
</button>
</li>
</ul>
</div>
</div>
@@ -371,6 +396,13 @@
if (act === 'guide') return toast('使用指南', '点击查看完整填写指南');
if (act === 'save') return save();
if (act === 'upload-zone') return openFilePicker();
if (act === 'bl-add') {
blAdd($blInput.value);
$blInput.value = '';
$blInput.focus();
updateBlAddBtn();
return;
}
});
document.addEventListener('keydown', e => {
if (e.key === 'Escape' && drawer.classList.contains('show')) close();
@@ -388,8 +420,20 @@
// 卖点 bullet-list
$blInput.addEventListener('keydown', e => {
if (e.key === 'Enter') { e.preventDefault(); blAdd($blInput.value); $blInput.value = ''; }
if (e.key === 'Enter') {
e.preventDefault();
blAdd($blInput.value);
$blInput.value = '';
updateBlAddBtn();
}
});
$blInput.addEventListener('input', updateBlAddBtn);
}
function updateBlAddBtn() {
const row = $bullets && $bullets.querySelector('[data-f="add-row"]');
if (!row) return;
row.classList.toggle('show', !!($blInput.value || '').trim());
}
function openFilePicker() { if (pfFiles.length < PF_MAX) $f.file.click(); }
@@ -465,6 +509,7 @@
pfRender();
[...$bullets.querySelectorAll('.bl-item')].forEach(li => li.remove());
$blInput.value = '';
updateBlAddBtn();
}
function lockBody() {
@@ -550,9 +595,36 @@
images,
imgs: images.length,
};
// 持久化到 localStorage('fs-extra-products'),让 products.html 下次加载时
// 自动从 storage 读出并 prepend 到 grid(否则用户在工作台创建后 → 跳详情 →
// 回商品库会看不到刚创建的商品)
try {
const KEY = 'fs-extra-products';
const list = JSON.parse(localStorage.getItem(KEY) || '[]');
list.push({
id: product.id,
name, cat,
tags: '',
assets: 0,
videos: 0,
bullets: points,
date: new Date().toISOString().slice(0, 10),
createdAt: Date.now(),
});
localStorage.setItem(KEY, JSON.stringify(list));
} catch (e) { /* storage 不可用降级到只跳转 */ }
if (typeof currentOpts.onSave === 'function') {
toast('商品已创建', '+ ' + name);
currentOpts.onSave(product);
close();
return;
}
// 默认行为: 不 close drawer · 跳转期间 drawer 仍覆盖 host 页面 → 视觉上彻底
// 消除"闪 host"(浏览器导航开始后,整页被新页面替换,drawer 自然消失)
toast('商品已创建', '+ ' + name);
if (typeof currentOpts.onSave === 'function') currentOpts.onSave(product);
close();
const url = 'product-detail.html?product=' + encodeURIComponent(name) + '&id=new';
location.href = url;
}
window.NewProductDrawer = { open, close };
+13 -3
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',
id: 'asset-factory', label: '图片生成', href: 'asset-factory.html', badge: '12',
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>'
},
{
@@ -29,11 +29,11 @@ const NAV = [
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="4" width="6" height="16"/><rect x="11" y="4" width="4" height="16"/><rect x="17" y="6" width="4" height="14"/></svg>'
},
{
id: 'team', label: '团队', href: 'team.html', badge: '5',
id: 'team', label: '团队', href: 'team.html',
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="9" cy="8" r="3"/><circle cx="17" cy="9" r="2.5"/><path d="M3 19c0-3 2.7-5 6-5s6 2 6 5M14 19c.5-2.4 2.4-4 5-4 .8 0 1.5.2 2 .5"/></svg>'
},
{
id: 'account', label: '账户', href: 'account.html',
id: 'account', label: '消费', href: 'account.html',
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="6" width="18" height="13" rx="2"/><path d="M3 10h18M16 14h2"/></svg>'
},
{
@@ -574,8 +574,18 @@ window.Shell = {
openModal(id) {
const el = document.getElementById(id);
if (!el || el.classList.contains('show')) return;
// 把 modal-bg 挪到 body 直接子节点,绕开 .content 的层叠上下文(z-index:1)困住模态、让 topbar 漏出来的问题
if (el.parentNode !== document.body) document.body.appendChild(el);
el.classList.add('show');
this.lockScroll();
if (!this._modalEsc) {
this._modalEsc = (e) => {
if (e.key !== 'Escape') return;
const open = document.querySelector('.modal-bg.show');
if (open) this.closeModal(open.id);
};
document.addEventListener('keydown', this._modalEsc);
}
},
closeModal(id) {
const el = document.getElementById(id);
+244 -73
View File
@@ -4,7 +4,7 @@
<meta charset="utf-8">
<title>资产库 · 流·Studio</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=202605211643">
<link rel="stylesheet" href="assets/restraint.css?v=202605211800">
<style>
.asset-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 14px; }
.asset-grid.video-grid { grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); }
@@ -92,6 +92,25 @@
}
.asset-card { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); cursor: pointer; transition: background .15s; position: relative; }
.asset-card:hover { background: var(--background-lighter); border-color: var(--black-alpha-48); }
/* 下载按钮 · hover 卡片显示,与 card-del-btn 并列 · PRD §6.5 中间产物可下载 */
.asset-card .card-dl-btn {
position: absolute;
top: 8px; right: 48px;
width: 32px; height: 32px;
background: rgba(255,255,255,.95);
border: 1px solid var(--black-alpha-12);
border-radius: var(--r-md);
display: grid; place-items: center;
color: var(--black-alpha-56);
cursor: pointer;
opacity: 0;
transition: opacity var(--t-base), background var(--t-base), color var(--t-base);
z-index: 4;
}
.asset-card .card-dl-btn svg { width: 14px; height: 14px; }
.asset-card:hover .card-dl-btn { opacity: 1; }
.asset-card .card-dl-btn:hover { background: var(--heat); color: var(--accent-white); border-color: var(--heat); }
body.edit-mode .asset-card .card-dl-btn { opacity: 0 !important; pointer-events: none !important; }
/* 编辑模式 checkbox */
.asset-card .card-check {
position: absolute; top: 10px; left: 10px;
@@ -376,10 +395,13 @@
<div class="asset-meta">男 · 青年 · 都市白领 · 用过 2 次</div>
</div>
</div>
<div class="asset-card" data-name="妈妈 · 王姐" data-gender="女" data-age="中年" data-role="居家" data-source="手动上传" data-used="1" data-added="20260415" onclick="Shell.toast('查看资产', '王姐')">
<div class="asset-card" data-name="妈妈 · 王姐" data-gender="女" data-age="中年" data-role="居家" data-source="手动上传" data-triview="0" data-used="1" data-added="20260415" onclick="Shell.toast('查看资产', '王姐')">
<span class="card-check" aria-hidden="true"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg></span>
<button class="card-del-btn" type="button" title="删除资产" onclick="event.stopPropagation();" data-action="delete-asset"><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 asset-thumb"><span class="ph-frame">妈妈 · 居家</span></div>
<div class="placeholder asset-thumb">
<span class="tri-missing-badge" title="手动上传的人物未生成三视图,前往图片生成可补齐"><span class="lbl-mono">缺三视图</span></span>
<span class="ph-frame">妈妈 · 居家</span>
</div>
<div class="asset-body">
<div class="asset-name">妈妈 · 王姐</div>
<div class="asset-meta">女 · 中年 · 居家 · 用过 1 次</div>
@@ -430,10 +452,13 @@
<div class="asset-meta">男 · 少年 · 学生 · 用过 1 次</div>
</div>
</div>
<div class="asset-card" data-name="李爷爷" data-gender="男" data-age="老年" data-role="居家" data-source="手动上传" data-used="1" data-added="20260418" onclick="Shell.toast('查看资产', '李爷爷')">
<div class="asset-card" data-name="李爷爷" data-gender="男" data-age="老年" data-role="居家" data-source="手动上传" data-triview="0" data-used="1" data-added="20260418" onclick="Shell.toast('查看资产', '李爷爷')">
<span class="card-check" aria-hidden="true"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg></span>
<button class="card-del-btn" type="button" title="删除资产" onclick="event.stopPropagation();" data-action="delete-asset"><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 asset-thumb"><span class="ph-frame">李爷爷 · 居家</span></div>
<div class="placeholder asset-thumb">
<span class="tri-missing-badge" title="手动上传的人物未生成三视图,前往图片生成可补齐"><span class="lbl-mono">缺三视图</span></span>
<span class="ph-frame">李爷爷 · 居家</span>
</div>
<div class="asset-body">
<div class="asset-name">李爷爷</div>
<div class="asset-meta">男 · 老年 · 居家 · 用过 1 次</div>
@@ -848,6 +873,29 @@
<script>
Shell.render({ active: 'library', crumbs: [{ label: '工作台', href: 'index.html' }, { label: '资产库' }] });
/* ─── 给所有资产卡注入下载按钮 · PRD §6.5 所有中间产物可下载 ─── */
(function injectDownloadBtns() {
const dlSvg = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>';
document.querySelectorAll('.asset-card').forEach(card => {
if (card.querySelector('.card-dl-btn')) return;
const btn = document.createElement('button');
btn.className = 'card-dl-btn';
btn.type = 'button';
btn.title = '下载资产';
btn.innerHTML = dlSvg;
btn.addEventListener('click', (e) => {
e.stopPropagation();
const name = card.dataset.name || '资产';
// 推测卡片类型用作 mono 后缀
const grid = card.closest('.asset-grid');
const kind = grid ? grid.dataset.tab : '';
const kindLabel = { people: '人物 · PNG', scenes: '场景 · PNG', products: '商品 · PNG', finals: '成片 · MP4 1080p', uploads: '原始素材' }[kind] || '资产';
Shell.toast('下载中', name + ' · ' + kindLabel);
});
card.appendChild(btn);
});
})();
// ============== State ==============
const TAB_KEYS = ['people', 'scenes', 'products', 'finals', 'uploads'];
const PAGE_SIZES = [12, 24, 48, 96];
@@ -1671,43 +1719,124 @@ document.querySelectorAll('.asset-card').forEach(card => {
});
/* ============================================================
资产详情 modal · 替代旧 Shell.toast 点击
资产详情 modal · 与 pipeline.html 共用参考布局 v2
============================================================ */
(function () {
// 注入 modal HTML
// 注入 modal CSS (与 pipeline.html 保持一致的 .asset-* 命名)
const css = `
.asset-modal-bg { position: fixed; inset: 0; background: rgba(0,0,0,.4); z-index: 1000; display: none; align-items: center; justify-content: center; padding: 40px; }
.asset-modal-bg.show { display: flex; }
.asset-modal { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); width: min(1040px, 100%); max-height: calc(100vh - 80px); overflow: hidden; display: flex; flex-direction: column; box-shadow: 0 16px 48px rgba(0,0,0,.18); }
.asset-modal-h { display: flex; align-items: center; gap: 10px; padding: 14px 20px; border-bottom: 1px solid var(--border-faint); }
.asset-modal-h h2 { font-size: 15px; font-weight: 600; }
.asset-modal-h .ad-tag { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .02em; }
.asset-modal-h .x { width: 30px; height: 30px; display: grid; place-items: center; background: transparent; border: 0; cursor: pointer; color: var(--black-alpha-56); border-radius: var(--r-sm); margin-left: auto; }
.asset-modal-h .x:hover { background: var(--black-alpha-8); color: var(--accent-black); }
.asset-modal-body { padding: 20px 24px 24px; overflow-y: auto; flex: 1; }
.asset-detail-grid { display: grid; grid-template-columns: 340px 1fr; gap: 24px; }
.asset-detail-lead { display: flex; flex-direction: column; gap: 10px; }
.asset-detail-lead .ad-lead-wrap { position: relative; }
.asset-detail-lead .placeholder.ad-lead-img { aspect-ratio: 3/4; border-radius: var(--r-md); }
.asset-detail-lead .ad-zoom-btn { position: absolute; right: 10px; bottom: 10px; height: 28px; padding: 0 12px; background: rgba(21,20,15,.7); color: #fff; border: 0; border-radius: var(--r-pill); display: inline-flex; align-items: center; gap: 4px; font-size: 11.5px; font-family: inherit; cursor: pointer; }
.asset-detail-lead .ad-zoom-btn:hover { background: rgba(21,20,15,.9); }
.asset-detail-lead .ad-zoom-btn svg { width: 12px; height: 12px; }
.asset-detail-lead .ad-thumbs { display: flex; gap: 8px; }
.asset-detail-lead .ad-thumbs .thumb { flex: 0 0 64px; aspect-ratio: 3/4; border-radius: var(--r-sm); border: 1px solid var(--border-faint); cursor: pointer; overflow: hidden; }
.asset-detail-lead .ad-thumbs .thumb:hover { border-color: var(--heat-40); }
.asset-detail-lead .ad-thumbs .thumb.active { border-color: var(--heat); border-width: 2px; }
.asset-detail-right .ad-section + .ad-section { margin-top: 18px; }
.asset-detail-section-h { display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 600; color: var(--accent-black); margin-bottom: 10px; }
.asset-detail-section-h .ic { width: 14px; height: 14px; color: var(--heat); display: grid; place-items: center; }
.asset-detail-section-h .ic svg { width: 14px; height: 14px; }
.asset-detail-section-h .ad-ratio-chip { margin-left: auto; font-family: var(--font-mono); font-size: 10.5px; padding: 2px 8px; border-radius: var(--r-sm); background: var(--background-lighter); border: 1px solid var(--border-faint); color: var(--black-alpha-56); }
.asset-detail-section-h .ad-icon-btn { width: 28px; height: 28px; display: grid; place-items: center; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-sm); color: var(--black-alpha-56); cursor: pointer; }
.asset-detail-section-h .ad-icon-btn:hover { color: var(--heat); border-color: var(--heat-40); }
.asset-detail-section-h .ad-icon-btn svg { width: 12px; height: 12px; }
.asset-detail-tri-row .placeholder { aspect-ratio: 16/9; border-radius: var(--r-md); }
.asset-detail-tri-row .placeholder.missing { display: grid; place-items: center; border: 1px dashed var(--border-faint); background: var(--background-lighter); color: var(--black-alpha-48); font-family: var(--font-mono); font-size: 12px; padding: 12px; text-align: center; cursor: pointer; gap: 8px; }
.asset-detail-tri-row .placeholder.missing:hover { border-color: var(--heat); color: var(--heat); }
.ad-intro { font-size: 13px; line-height: 1.65; color: var(--black-alpha-72); margin: 0 0 12px; }
.ad-tags { display: flex; flex-wrap: wrap; gap: 8px; }
.ad-tags .ad-tag-chip { height: 26px; padding: 0 12px; display: inline-flex; align-items: center; background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-sm); font-size: 12px; color: var(--accent-black); }
.ad-tags .ad-tag-add { width: 26px; height: 26px; display: grid; place-items: center; background: var(--background-lighter); border: 1px dashed var(--black-alpha-24); border-radius: var(--r-sm); color: var(--black-alpha-56); cursor: pointer; }
.ad-tags .ad-tag-add:hover { border-color: var(--heat); color: var(--heat); }
.ad-tags .ad-tag-add svg { width: 12px; height: 12px; }
.ad-props { margin-top: 18px; display: grid; grid-template-columns: repeat(3, 1fr); column-gap: 24px; row-gap: 0; border-top: 1px solid var(--border-faint); padding-top: 16px; }
.ad-props .ad-prop { display: flex; align-items: baseline; padding: 10px 0; border-bottom: 1px solid var(--border-faint); font-size: 12.5px; min-height: 38px; }
.ad-props .ad-prop:nth-last-child(-n+3) { border-bottom: 0; }
.ad-props .ad-prop .k { flex: 0 0 64px; color: var(--black-alpha-56); font-family: var(--font-mono); font-size: 11px; }
.ad-props .ad-prop .v { color: var(--accent-black); font-weight: 500; word-break: break-all; }
.asset-detail-tip { margin-top: 10px; padding: 10px 12px; background: var(--heat-12); border: 1px solid var(--heat-20); border-radius: var(--r-sm); font-size: 12px; color: var(--accent-black); display: flex; align-items: center; gap: 8px; line-height: 1.5; }
.asset-detail-tip svg { width: 14px; height: 14px; color: var(--heat); flex-shrink: 0; }
.asset-detail-tip .ai-gen-btn { margin-left: auto; height: 26px; padding: 0 10px; background: var(--heat); color: #fff; border: 1px solid var(--heat); border-radius: var(--r-sm); font-size: 11.5px; cursor: pointer; font-family: inherit; flex-shrink: 0; }
.asset-modal-f { padding: 14px 20px; border-top: 1px solid var(--border-faint); display: flex; align-items: center; gap: 8px; }
.asset-modal-f .ad-foot-stats { display: flex; gap: 6px; margin-right: auto; }
.asset-modal-f .ad-stat-btn { height: 32px; padding: 0 12px; display: inline-flex; align-items: center; gap: 6px; background: transparent; border: 1px solid var(--border-faint); border-radius: var(--r-sm); color: var(--black-alpha-72); font-size: 12.5px; font-family: inherit; cursor: pointer; }
.asset-modal-f .ad-stat-btn:hover { background: var(--background-lighter); color: var(--accent-black); border-color: var(--black-alpha-24); }
.asset-modal-f .ad-stat-btn svg { width: 13px; height: 13px; }
.asset-modal-f .ad-stat-btn b { color: var(--accent-black); font-weight: 600; }
`;
const style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
const modalHTML = `
<div class="lib-detail-bg" id="lib-detail-bg" style="position:fixed; inset:0; background:rgba(0,0,0,.4); z-index:1000; display:none; align-items:center; justify-content:center; padding:40px;">
<div class="lib-detail" style="background:var(--surface); border:1px solid var(--border-faint); border-radius:var(--r-md); width:min(880px,100%); max-height:calc(100vh - 80px); overflow:hidden; display:flex; flex-direction:column; box-shadow:0 16px 48px rgba(0,0,0,.18);">
<div style="display:flex; align-items:center; gap:10px; padding:14px 20px; border-bottom:1px solid var(--border-faint);">
<h2 id="lib-detail-title" style="font-size:15px; font-weight:600;">资产详情</h2>
<span id="lib-detail-kind" style="font-family:var(--font-mono); font-size:11px; color:var(--black-alpha-48);">// kind</span>
<button id="lib-detail-x" type="button" aria-label="关闭" style="width:30px; height:30px; display:grid; place-items:center; background:transparent; border:0; cursor:pointer; color:var(--black-alpha-56); border-radius:var(--r-sm); margin-left:auto;"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 6l12 12M6 18L18 6"/></svg></button>
<div class="asset-modal-bg" id="lib-detail-bg">
<div class="asset-modal">
<div class="asset-modal-h">
<h2 id="lib-detail-title">资产详情</h2>
<span class="ad-tag" id="lib-detail-kind">/ kind</span>
<button class="x" id="lib-detail-x" type="button" aria-label="关闭"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 6l12 12M6 18L18 6"/></svg></button>
</div>
<div style="padding:22px 24px; overflow-y:auto; flex:1;">
<div style="display:grid; grid-template-columns:1fr 1fr; gap:22px;">
<div>
<div class="placeholder" style="aspect-ratio:3/4;" id="lib-detail-lead-img"><span class="ph-frame">立绘 / 主图</span></div>
<div style="font-family:var(--font-mono); font-size:10.5px; color:var(--black-alpha-48); letter-spacing:.06em; text-transform:uppercase; margin:14px 0 8px;">// 三视图</div>
<div id="lib-detail-tri" style="display:grid; grid-template-columns:repeat(3,1fr); gap:8px;">
<div class="placeholder" style="aspect-ratio:1;"><span class="ph-frame">正面</span></div>
<div class="placeholder" style="aspect-ratio:1;"><span class="ph-frame">侧面</span></div>
<div class="placeholder" style="aspect-ratio:1;"><span class="ph-frame">背面</span></div>
<div class="asset-modal-body">
<div class="asset-detail-grid">
<div class="asset-detail-lead">
<div class="ad-lead-wrap">
<div class="placeholder ad-lead-img" id="lib-detail-lead-img"><span class="ph-frame">立绘 / 主图</span></div>
<button class="ad-zoom-btn" type="button">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 8V3h5M16 3h5v5M21 16v5h-5M8 21H3v-5"/></svg>
查看大图
</button>
</div>
<div class="ad-thumbs" id="lib-detail-thumbs"></div>
</div>
<div>
<div style="font-family:var(--font-mono); font-size:10.5px; color:var(--black-alpha-48); letter-spacing:.06em; text-transform:uppercase; margin-bottom:8px;">// 基础信息</div>
<div id="lib-detail-info"></div>
<div id="lib-detail-tip" style="display:none; margin-top:10px; padding:10px 12px; background:var(--heat-12); border:1px solid var(--heat-20); border-radius:var(--r-sm); font-size:12px; color:var(--accent-black); align-items:center; gap:8px; line-height:1.5;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" style="color:var(--heat); flex-shrink:0;"><circle cx="12" cy="12" r="9"/><path d="M12 8v4M12 16h.01"/></svg>
<span>暂无三视图,建议用 AI 生成以保证多角度一致性</span>
<button id="lib-detail-aigen" type="button" style="margin-left:auto; height:26px; padding:0 10px; background:var(--heat); color:var(--accent-white); border:1px solid var(--heat); border-radius:var(--r-sm); font-size:11.5px; cursor:pointer; flex-shrink:0;">AI 生成三视图</button>
<div class="asset-detail-right">
<div class="ad-section" id="lib-detail-tri-section">
<div class="asset-detail-section-h">
<span class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg></span>
<span class="t">三视图</span>
<span class="ad-ratio-chip" id="lib-detail-ratio">16:9</span>
<button class="ad-icon-btn" type="button" title="下载"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg></button>
</div>
<div class="asset-detail-tri-row" id="lib-detail-tri">
<div class="placeholder"><span class="ph-frame">正 / 侧 / 背 · 三视图</span></div>
</div>
<div class="asset-detail-tip" id="lib-detail-tip" style="display:none;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 8v4M12 16h.01"/></svg>
<span>暂无三视图,建议用 AI 生成以保证多角度一致性</span>
<button class="ai-gen-btn" type="button" id="lib-detail-aigen">AI 生成三视图</button>
</div>
</div>
<div class="ad-section">
<div class="asset-detail-section-h">
<span class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6h16M4 12h16M4 18h10"/></svg></span>
<span class="t">简介</span>
</div>
<p class="ad-intro" id="lib-detail-intro"></p>
<div class="ad-tags" id="lib-detail-tags"></div>
</div>
<div class="ad-props" id="lib-detail-props"></div>
</div>
</div>
</div>
<div style="padding:14px 20px; border-top:1px solid var(--border-faint); display:flex; justify-content:flex-end; gap:8px;">
<button class="btn btn-ghost" type="button" id="lib-detail-close">关闭</button>
<button class="btn btn-primary" type="button" id="lib-detail-apply">应用到当前项目</button>
<div class="asset-modal-f">
<div class="ad-foot-stats">
<button class="ad-stat-btn" type="button">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg>
下载
</button>
</div>
<button class="btn btn-primary" type="button" id="lib-detail-apply">使用该资产</button>
</div>
</div>
</div>`;
@@ -1717,69 +1846,111 @@ document.querySelectorAll('.asset-card').forEach(card => {
const titleEl = document.getElementById('lib-detail-title');
const kindEl = document.getElementById('lib-detail-kind');
const leadImg = document.getElementById('lib-detail-lead-img');
const thumbsEl = document.getElementById('lib-detail-thumbs');
const triSection = document.getElementById('lib-detail-tri-section');
const triEl = document.getElementById('lib-detail-tri');
const infoEl = document.getElementById('lib-detail-info');
const ratioChip = document.getElementById('lib-detail-ratio');
const introEl = document.getElementById('lib-detail-intro');
const tagsEl = document.getElementById('lib-detail-tags');
const propsEl = document.getElementById('lib-detail-props');
const tipEl = document.getElementById('lib-detail-tip');
function _hash(s) { let h = 0; for (let i = 0; i < s.length; i++) h = ((h << 5) - h + s.charCodeAt(i)) | 0; return Math.abs(h); }
function _fmtAssetId(name, k) { return 'ASSET-20240520-' + (k === 'person' ? 'M' : k === 'scene' ? 'S' : 'P') + String(_hash(name) % 1000).padStart(3, '0'); }
function _fmtSize(name) { return (4 + (_hash(name) % 100) / 10).toFixed(1) + 'MB'; }
function _fmtFav(name) { return String(8 + _hash(name) % 80); }
function _fmtDl(name) { const n = 200 + _hash(name) % 1800; return n >= 1000 ? (n / 1000).toFixed(1) + 'k' : String(n); }
function open(card) {
const name = card.dataset.name || '资产';
let kind = 'asset', info = [];
if (card.dataset.gender) { // 人物
kind = '人物 · 模特';
info = [
['类别', '人物'],
['性别', card.dataset.gender || '—'],
['年龄段', card.dataset.age || '—'],
['角色', card.dataset.role || '—'],
['来源', card.dataset.source || '—'],
['使用次数', (card.dataset.used || '0') + ' 次'],
const used = card.dataset.used || '0';
const source = card.dataset.source || '平台预设';
let tagText = 'AI 素材', intro = '', tags = [], props = [], hasTri = false, isActor = false;
if (card.dataset.gender) {
tagText = '人物 · 模特';
isActor = true; hasTri = true;
intro = (card.dataset.role || '人物模特') + ' · 可用于本项目人物资产生成';
tags = [card.dataset.gender, card.dataset.age, card.dataset.role].filter(Boolean);
props = [
['性别', card.dataset.gender || '-'], ['种族', '东亚'], ['作品ID', _fmtAssetId(name, 'person')],
['年龄段', card.dataset.age || '-'], ['角色', card.dataset.role || '-'], ['创作人', '流·Studio'],
['身高', '中等'], ['来源', source], ['文件大小', _fmtSize(name)],
['使用次数', used + ' 次'], ['授权', '商用'], ['发布时间', '2024-05-20'],
];
} else if (card.dataset.sceneType) {
kind = '场景';
info = [
['类别', '场景'],
['场景类型', card.dataset.sceneType],
['来源', card.dataset.source || '—'],
['使用次数', (card.dataset.used || '0') + ' 次'],
tagText = '场景 · ' + card.dataset.sceneType;
isActor = false; hasTri = false;
intro = card.dataset.sceneType + ' 场景资产';
tags = [card.dataset.sceneType, source].filter(Boolean);
props = [
['场景类型', card.dataset.sceneType], ['来源', source], ['作品ID', _fmtAssetId(name, 'scene')],
['镜头', '通用'], ['光线', '自然光'], ['创作人', '流·Studio'],
['用途', '本项目场景资产'], ['使用次数', used + ' 次'], ['文件大小', _fmtSize(name)],
];
} else if (card.dataset.product) {
kind = '商品资产';
info = [
['类别', '商品'],
['关联商品', card.dataset.product],
['来源', card.dataset.source || '—'],
['使用次数', (card.dataset.used || '0') + ' 次'],
tagText = '商品资产';
isActor = false;
intro = '关联商品: ' + card.dataset.product;
tags = ['商品', source].filter(Boolean);
props = [
['关联商品', card.dataset.product], ['来源', source], ['作品ID', _fmtAssetId(name, 'product')],
['用途', '商品资产'], ['使用次数', used + ' 次'], ['创作人', '流·Studio'],
['授权', '商用'], ['文件大小', _fmtSize(name)], ['发布时间', '2024-05-20'],
];
} else {
kind = 'AI 素材';
info = [
['名称', name],
['来源', card.dataset.source || '—'],
tagText = 'AI 素材';
isActor = false;
intro = name;
tags = [source].filter(Boolean);
props = [
['名称', name], ['来源', source], ['作品ID', _fmtAssetId(name, 'asset')],
['创作人', '流·Studio'], ['文件大小', _fmtSize(name)], ['发布时间', '2024-05-20'],
];
}
titleEl.textContent = name;
kindEl.textContent = '// ' + kind;
kindEl.textContent = '/ ' + tagText;
leadImg.innerHTML = '<span class="ph-frame">' + name + '</span>';
// 三视图:人物显示,其他默认提示
if (card.dataset.gender) {
triEl.style.display = 'grid';
thumbsEl.innerHTML = ['v1','v2','v3'].map((t, i) => `<div class="thumb placeholder${i === 0 ? ' active' : ''}"><span class="ph-frame">${t}</span></div>`).join('');
thumbsEl.querySelectorAll('.thumb').forEach(t => t.addEventListener('click', () => {
thumbsEl.querySelectorAll('.thumb').forEach(x => x.classList.remove('active'));
t.classList.add('active');
}));
if (card.dataset.sceneType) {
triSection.style.display = 'none';
} else if (isActor) {
triSection.style.display = '';
triEl.classList.remove('actor');
triEl.innerHTML = '<div class="placeholder"><span class="ph-frame">' + name + ' · 三视图 (正/侧/背)</span></div>';
ratioChip.textContent = '16:9';
tipEl.style.display = 'none';
} else if (card.dataset.product) {
// 商品:若未上传则提示 AI 生成
triEl.style.display = 'grid';
tipEl.style.display = 'flex';
} else {
triEl.style.display = 'none';
tipEl.style.display = 'none';
triSection.style.display = '';
triEl.classList.remove('actor');
ratioChip.textContent = '16:9';
if (hasTri) {
triEl.innerHTML = '<div class="placeholder"><span class="ph-frame">' + name + ' · 三视图</span></div>';
tipEl.style.display = 'none';
} else {
triEl.innerHTML = '<div class="placeholder missing" data-tri="0"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 8v4M12 16h.01"/></svg><span>暂未生成三视图(16:9 单图)</span></div>';
tipEl.style.display = 'flex';
}
}
infoEl.innerHTML = info.map(([k, v]) => '<div style="display:flex; justify-content:space-between; align-items:baseline; padding:8px 0; border-bottom:1px solid var(--border-faint); font-size:12.5px;"><span style="color:var(--black-alpha-56); font-family:var(--font-mono); font-size:11px;">' + k + '</span><span style="color:var(--accent-black);">' + v + '</span></div>').join('');
bg.style.display = 'flex';
introEl.textContent = intro || '暂无简介';
tagsEl.innerHTML = tags.map(t => '<span class="ad-tag-chip">' + t + '</span>').join('') +
'<button class="ad-tag-add" type="button" title="添加标签"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M8 3v10M3 8h10"/></svg></button>';
propsEl.innerHTML = props.map(([k, v]) => '<div class="ad-prop"><span class="k">' + k + '</span><span class="v">' + v + '</span></div>').join('');
bg.classList.add('show');
}
function close() { bg.style.display = 'none'; }
function close() { bg.classList.remove('show'); }
bg.addEventListener('click', e => { if (e.target === bg) close(); });
document.getElementById('lib-detail-x').addEventListener('click', close);
document.getElementById('lib-detail-close').addEventListener('click', close);
document.getElementById('lib-detail-apply').addEventListener('click', () => {
Shell.toast('已应用「' + titleEl.textContent + '」', '已加入当前项目');
close();
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+408 -68
View File
@@ -167,6 +167,18 @@
}
.prod-row .x:hover { background: var(--black-alpha-8); color: var(--accent-crimson); }
.prod-row .x svg { width: 12px; height: 12px; }
.prod-row .swap {
width: 22px; height: 22px;
display: grid; place-items: center;
background: transparent; border: 0;
border-radius: var(--r-sm);
cursor: pointer;
color: var(--black-alpha-48);
flex-shrink: 0;
transition: background var(--t-base), color var(--t-base);
}
.prod-row .swap:hover { background: var(--heat-12); color: var(--heat); }
.prod-row .swap svg { width: 13px; height: 13px; }
.prod-empty {
padding: 14px 10px;
text-align: center;
@@ -197,6 +209,7 @@
}
.prod-add:hover { background: var(--heat-12); }
.prod-add svg { width: 12px; height: 12px; }
.prod-add[hidden] { display: none; }
/* 商品库全屏弹窗 */
.pl-modal-bg {
@@ -505,14 +518,51 @@
display: flex; flex-direction: column;
overflow-y: auto;
}
/* prompt-style summary 卡片 (引号 icon + 灰底 + 右上 meta) */
.pc-pv-h {
display: flex; flex-direction: column; gap: 8px;
padding-bottom: 14px; margin-bottom: 16px;
border-bottom: 1px solid var(--border-faint);
position: relative;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
padding: 14px 18px 14px 44px;
margin-bottom: 14px;
}
.pc-pv-h .row { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--black-alpha-72); }
.pc-pv-h .row .k { font-family: var(--font-mono); font-size: 11.5px; color: var(--black-alpha-48); letter-spacing: .02em; }
.pc-pv-h .row .v { color: var(--accent-black); font-weight: 500; }
.pc-pv-h .quote-icon {
position: absolute;
top: 13px; left: 16px;
width: 18px; height: 18px;
color: var(--black-alpha-24);
}
.pc-pv-h .pv-meta {
float: right;
font-family: var(--font-mono);
font-size: 11px;
color: var(--black-alpha-48);
letter-spacing: .04em;
line-height: 1.5;
}
.pc-pv-h .pv-meta b { color: var(--accent-black); font-weight: 600; }
.pc-pv-h .pv-line {
font-size: 13px;
color: var(--accent-black);
line-height: 1.6;
display: flex; align-items: center;
}
.pc-pv-h .pv-line .k {
font-family: var(--font-mono);
font-size: 11px;
color: var(--black-alpha-48);
letter-spacing: .04em;
margin-right: 8px;
min-width: 36px;
}
.pc-pv-h .pv-line .v { font-weight: 500; }
.pc-pv-h .pv-line .swap {
margin-left: 10px;
font-size: 11.5px; color: var(--heat);
cursor: pointer;
}
.pc-pv-h .pv-line .swap:hover { text-decoration: underline; }
.pv-platform-section { margin-bottom: 24px; }
.pv-platform-section:last-child { margin-bottom: 0; }
@@ -525,10 +575,20 @@
margin-left: auto; font-family: var(--font-mono); font-size: 11px;
color: var(--black-alpha-48); letter-spacing: .02em; font-weight: 400;
}
.pv-platform-section .ps-h .batch-lab {
font-family: var(--font-mono); font-size: 10.5px; font-weight: 600;
padding: 2px 8px; border-radius: var(--r-pill); letter-spacing: .04em;
}
.pv-platform-section .ps-h .batch-lab.gen { background: var(--background-lighter); color: var(--black-alpha-56); }
.pv-platform-section .ps-h .batch-lab.rerun { background: var(--heat-12); color: var(--heat); }
.pv-platform-section .ps-h .sep { color: var(--black-alpha-32); font-weight: 400; }
.pv-platform-section .ps-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 8px;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
}
@media (max-width: 1400px) {
.pv-platform-section .ps-grid { gap: 10px; }
}
.pv-platform-section .ps-grid .mp-result {
position: relative;
@@ -539,30 +599,117 @@
border: 1px solid var(--border-faint);
display: flex; flex-direction: column;
}
.pv-platform-section .ps-grid .mp-result .mp-r-thumb { flex: 1; }
.pv-platform-section .ps-grid .mp-result .mp-r-act {
padding: 4px 6px;
background: rgba(255,255,255,.95);
.pv-platform-section .ps-grid .mp-result .mp-r-thumb { flex: 1; position: relative; }
/* hover 浮层 (重跑 + 采用) — 无遮罩,右下角横向小 icon */
.pv-platform-section .ps-grid .mp-r-overlay {
position: absolute; right: 8px; bottom: 8px;
display: flex; align-items: center;
gap: 6px;
opacity: 0; pointer-events: none;
transition: opacity var(--t-base);
z-index: 2;
}
.pv-platform-section .ps-grid .mp-result .add-lib {
width: 100%;
display: inline-flex; align-items: center; justify-content: center; gap: 4px;
background: var(--surface);
border: 1px solid var(--heat-40);
border-radius: var(--r-sm);
color: var(--heat);
height: 22px;
cursor: pointer; font-family: inherit;
.pv-platform-section .ps-grid .mp-result:hover .mp-r-overlay { opacity: 1; pointer-events: auto; }
.pv-platform-section .ps-grid .mp-r-overlay button {
width: 26px; height: 26px;
border-radius: 50%;
background: rgba(255,255,255,.96);
border: 1px solid var(--border-faint);
cursor: pointer;
display: grid; place-items: center;
color: var(--accent-black);
box-shadow: 0 2px 6px rgba(21,20,15,.16);
transition: background var(--t-base), color var(--t-base), transform var(--t-base);
}
.pv-platform-section .ps-grid .mp-r-overlay button:hover { background: var(--heat); color: #fff; border-color: var(--heat); transform: scale(1.06); }
.pv-platform-section .ps-grid .mp-r-overlay button svg { width: 12px; height: 12px; }
/* 已采用角标 */
.pv-platform-section .ps-grid .mp-result .adopt-badge {
position: absolute; top: 6px; left: 6px;
background: var(--accent-forest, #42c366);
color: #fff;
font-family: var(--font-mono); font-size: 10px; font-weight: 600;
letter-spacing: .04em;
padding: 1px 6px;
border-radius: var(--r-pill);
z-index: 3;
display: none;
}
.pv-platform-section .ps-grid .mp-result.adopted .adopt-badge { display: inline-block; }
.pv-platform-section .ps-grid .mp-result.adopted .mp-r-overlay { display: none; }
.pv-platform-section .ps-grid .mp-result.regenerating .mp-r-thumb::before {
content: '生成中…';
position: absolute; inset: 0;
background: rgba(255,255,255,.7);
display: grid; place-items: center;
font-family: var(--font-mono); font-size: 11px;
color: var(--heat); letter-spacing: .04em;
z-index: 1;
}
/* 每批次下方的批量操作 (胶囊按钮组 · 左对齐) */
.pc-pv-batch {
margin-top: 10px;
display: flex; gap: 10px; align-items: center; justify-content: flex-start;
}
.pc-pv-batch[hidden] { display: none; }
.pv-platform-section .pc-pv-batch.batch-foot { margin-top: 10px; }
/* 预览区空态 (新任务且未生成) */
.pc-pv-empty-state {
flex: 1;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
text-align: center;
padding: 40px 24px;
gap: 6px;
}
.pc-pv-empty-state[hidden] { display: none; }
.pc-pv-empty-state .mono {
font-family: var(--font-mono);
font-size: 10.5px;
transition: background var(--t-base);
color: var(--black-alpha-48);
letter-spacing: .06em;
margin-bottom: 4px;
}
.pv-platform-section .ps-grid .mp-result .add-lib:hover { background: var(--heat-12); }
.pv-platform-section .ps-grid .mp-result .add-lib.added {
background: var(--accent-emerald-bg, #e6f4ec);
color: var(--accent-emerald, #1f8a51);
border-color: var(--accent-emerald-bd, #c4e3d1);
.pc-pv-empty-state .title {
font-size: 14px;
font-weight: 600;
color: var(--accent-black);
}
.pv-platform-section .ps-grid .mp-result .add-lib svg { width: 10px; height: 10px; }
.pc-pv-empty-state .hint {
font-size: 12.5px;
color: var(--black-alpha-48);
line-height: 1.6;
max-width: 320px;
}
.pc-pv-empty-state .hint b { color: var(--heat); font-weight: 600; }
/* 预览区 hidden 时收起所有内容元素 */
#pv-summary[hidden], #pv-results[hidden], #pv-foot[hidden] { display: none; }
.pc-pv-batch .summary {
margin-right: auto;
font-family: var(--font-mono); font-size: 11px;
color: var(--black-alpha-48); letter-spacing: .02em;
}
.pc-pv-batch .summary b { color: var(--heat); font-weight: 700; }
.pc-pv-batch .pill-btn {
height: 34px;
padding: 0 18px;
display: inline-flex; align-items: center; gap: 6px;
background: var(--surface);
border: 1px solid var(--black-alpha-12);
border-radius: var(--r-pill);
color: var(--accent-black);
font-family: inherit; font-size: 12.5px;
cursor: pointer;
transition: background var(--t-base), border-color var(--t-base), color var(--t-base);
}
.pc-pv-batch .pill-btn:hover { background: var(--background-lighter); border-color: var(--black-alpha-24); }
.pc-pv-batch .pill-btn.primary {
background: var(--heat); color: #fff; border-color: var(--heat);
}
.pc-pv-batch .pill-btn.primary:hover { filter: brightness(.94); }
.pc-pv-batch .pill-btn svg { width: 13px; height: 13px; }
.pc-pv-foot {
margin-top: 14px; padding-top: 12px;
@@ -610,10 +757,10 @@
<div class="pc-layout">
<!-- ===== 最左栏 · 任务历史 ===== -->
<!-- ===== 最左栏 · 任务中心 ===== -->
<div class="pc-tasks-panel" id="tasks-panel">
<div class="pc-tasks-h">
历史任务
任务中心
<span class="ct" id="tasks-count">0</span>
<button class="new" type="button" id="tasks-new-btn" title="新建任务">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M8 3v10M3 8h10"/></svg>
@@ -634,9 +781,8 @@
<div class="prod-list" id="prod-list">
<!-- 已选商品 chip · JS 动态渲染 -->
</div>
<button class="prod-add" type="button" id="prod-add-btn">
<button class="prod-add" type="button" id="prod-add-btn" title="添加商品" aria-label="添加商品">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
去商品库添加
</button>
</div>
@@ -647,7 +793,7 @@
<span class="title">选择平台</span>
</div>
<div class="platform-grid" id="platform-grid">
<div class="platform-card selected" data-id="dy" data-name="抖音电商">
<div class="platform-card" data-id="dy" data-name="抖音电商">
<div class="p-check"></div>
<div class="p-logo dy">抖</div>
<div class="p-name">抖音电商</div>
@@ -729,16 +875,25 @@
<!-- ===== 右栏 · 预览 ===== -->
<div class="pc-preview" id="pc-preview">
<div class="pc-pv-h">
<div class="row"><span class="k">已选商品</span><span class="v" id="pv-prod">补水保湿精华液</span></div>
<div class="row"><span class="k">已选平台</span><span class="v" id="pv-platforms">抖音电商 / 淘宝</span></div>
<div class="row"><span class="k">生成</span><span class="v" id="pv-count">4</span></div>
<!-- 空态(新任务态 & 还没立即生成时显示) -->
<div class="pc-pv-empty-state" id="pv-empty">
<div class="mono">// EMPTY STATE</div>
<div class="title">还没有生成结果</div>
<div class="hint">先选商品、选平台,点击 <b>立即生成</b> 后,效果图会出现在这里</div>
</div>
<div class="pc-pv-h" id="pv-summary">
<svg class="quote-icon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M9 7c-2.76 0-5 2.24-5 5 0 1.84 1 3.45 2.48 4.32C5.99 17.36 4.99 18 4 18v2c3.31 0 6-2.69 6-6V8c0-.55-.45-1-1-1zm9 0c-2.76 0-5 2.24-5 5 0 1.84 1 3.45 2.48 4.32-.49 1.04-1.49 1.68-2.48 1.68v2c3.31 0 6-2.69 6-6V8c0-.55-.45-1-1-1z"/></svg>
<div class="pv-meta"><b id="pv-count">4 张</b></div>
<div class="pv-line"><span class="k">商品</span><span class="v" id="pv-prod">未选择</span></div>
<div class="pv-line"><span class="k">平台</span><span class="v" id="pv-platforms">未选择</span></div>
</div>
<div id="pv-results">
<!-- 默认占位 -->
</div>
<div class="pc-pv-foot">
// 生成结果默认不入资产库,满意后点 [入资产库] 才扣费并保存
<div class="pc-pv-foot" id="pv-foot">
// 采用即扣费并入对应商品的 <a href="products.html">AI 素材库 →</a>;未采用的图不扣费、不保存
<br>// 任务进度可在 <a href="asset-factory.html">任务中心 →</a> 查看
</div>
</div>
@@ -868,10 +1023,10 @@ const PRODUCTS = [
{ id: 'p7', name: '露露同款裸感瑜伽裤', cat: '运动户外', meta: '健身房 · 42 素材' },
];
// State (单选)
// State (单选 · 默认全空)
const state = {
selectedProd: 'p1', // string | null
selectedPlatform: 'dy', // string | null
selectedProd: null, // string | null
selectedPlatform: null, // string | null
count: 4,
};
const UNIT_PRICE = 0.50;
@@ -879,16 +1034,21 @@ const UNIT_PRICE = 0.50;
// ─── 已选商品 渲染 (单选) ───
function renderSelectedProds() {
const list = document.getElementById('prod-list');
const addBtn = document.getElementById('prod-add-btn');
const id = state.selectedProd;
const p = id ? PRODUCTS.find(x => x.id === id) : null;
if (!p) {
list.innerHTML = '<div class="prod-empty">未选择商品<div class="mono">// 点击下方按钮去商品库选</div></div>';
list.innerHTML = ''; // 不显示占位文字, 由 + 按钮提示
if (addBtn) addBtn.hidden = false;
document.getElementById('pv-prod').textContent = '未选择';
} else {
list.innerHTML = `
<div class="prod-row" data-id="${p.id}">
<div class="placeholder thumb"></div>
<div class="info"><div class="nm">${p.name}</div><div class="meta">${p.cat}</div></div>
<button class="swap" type="button" data-swap="${p.id}" title="切换商品" aria-label="切换商品">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polyline points="17 1 21 5 17 9"/><polyline points="7 23 3 19 7 15"/><path d="M21 5H9a4 4 0 0 0-4 4M3 19h12a4 4 0 0 0 4-4"/></svg>
</button>
<button class="x" type="button" data-rm="${p.id}" aria-label="移除">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 6l12 12M6 18L18 6"/></svg>
</button>
@@ -898,6 +1058,10 @@ function renderSelectedProds() {
state.selectedProd = null;
renderSelectedProds();
});
list.querySelector('button.swap[data-swap]').addEventListener('click', () => {
addBtn.click(); // 复用 + 按钮的逻辑: 打开商品库
});
if (addBtn) addBtn.hidden = true;
document.getElementById('pv-prod').textContent = p.name;
}
updateCost();
@@ -1155,7 +1319,30 @@ document.querySelectorAll('.pill-row').forEach(row => {
});
});
// 渲染右侧预览 (单平台)
// ─── 预览区空态 / 内容 切换 ───
function showPreviewEmpty() {
const empty = document.getElementById('pv-empty');
const sum = document.getElementById('pv-summary');
const results = document.getElementById('pv-results');
const foot = document.getElementById('pv-foot');
if (empty) empty.hidden = false;
if (sum) sum.hidden = true;
if (results) results.hidden = true;
if (foot) foot.hidden = true;
}
function showPreviewContent() {
const empty = document.getElementById('pv-empty');
const sum = document.getElementById('pv-summary');
const results = document.getElementById('pv-results');
const foot = document.getElementById('pv-foot');
if (empty) empty.hidden = true;
if (sum) sum.hidden = false;
if (results) results.hidden = false;
if (foot) foot.hidden = false;
// pv-batch 由 renderResultCards 单独控制
}
// 渲染右侧预览 (占位 — 切平台/数量时显示)
function renderPreviewSections() {
const container = document.getElementById('pv-results');
const id = state.selectedPlatform;
@@ -1172,37 +1359,136 @@ function renderPreviewSections() {
</div>
<div class="ps-grid">
${Array(state.count).fill(0).map(() => `
<div class="mp-result">
<div class="placeholder mp-r-thumb"><span class="ph-frame">${c.dataset.name}</span></div>
<div class="mp-r-act">
<button class="add-lib" type="button" data-cost="${UNIT_PRICE}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"><path d="M12 5v14M5 12h14"/></svg>
入资产库 ¥${UNIT_PRICE.toFixed(2)}
</button>
</div>
<div class="mp-result placeholder-only">
<div class="placeholder mp-r-thumb"><span class="ph-frame">待生成</span></div>
</div>
`).join('')}
</div>
</div>
`;
container.querySelectorAll('.add-lib').forEach(b => {
b.addEventListener('click', e => {
e.stopPropagation();
if (b.classList.contains('added')) return;
b.classList.add('added');
b.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12l5 5L20 7"/></svg> 已入库';
Shell.toast('已入库', '已扣 ¥' + (+b.dataset.cost).toFixed(2));
});
}
// 渲染生成结果 (点立即生成时调,带 hover overlay + 批量 bar)
const RERUN_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>';
const ADOPT_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>';
let _batchSeq = 0;
function appendBatch(n, kind) {
const container = document.getElementById('pv-results');
const id = state.selectedPlatform;
const c = id ? document.querySelector('.platform-card[data-id="' + id + '"]') : null;
if (!c) return;
_batchSeq += 1;
const ts = new Date();
const tsStr = `${String(ts.getHours()).padStart(2,'0')}:${String(ts.getMinutes()).padStart(2,'0')}:${String(ts.getSeconds()).padStart(2,'0')}`;
const labCls = kind === 'gen' ? 'gen' : 'rerun';
const labTxt = kind === 'gen' ? `批次 ${_batchSeq} · 初始生成` : (kind === 'rerun-all' ? `批次 ${_batchSeq} · 全部重跑` : `批次 ${_batchSeq} · 单张重跑`);
const section = document.createElement('div');
section.className = 'pv-platform-section';
section.dataset.kind = kind;
section.innerHTML = `
<div class="ps-h">
<span class="batch-lab ${labCls}">${labTxt}</span>
<span class="sep">·</span>
<span>${c.dataset.name} 套图</span>
<span class="ct">${n} 张 · ${tsStr}</span>
</div>
<div class="ps-grid">
${Array(n).fill(0).map((_, i) => `
<div class="mp-result regenerating" data-idx="${i}">
<div class="placeholder mp-r-thumb"><span class="ph-frame">${c.dataset.name}</span></div>
<div class="mp-r-overlay">
<button class="r-rerun" type="button" title="重跑这一张" aria-label="重跑">${RERUN_SVG}</button>
<button class="r-adopt" type="button" title="采用这一张" aria-label="采用">${ADOPT_SVG}</button>
</div>
<span class="adopt-badge">已采用</span>
</div>
`).join('')}
</div>
<div class="pc-pv-batch batch-foot">
<button class="pill-btn rerun-batch" type="button" title="重跑这一批">
${RERUN_SVG}
<span>全部重跑</span>
</button>
<button class="pill-btn primary adopt-batch" type="button" title="采用这一批">
${ADOPT_SVG}
<span>全部采用 · <span class="adopted">0</span>/<span class="total">${n}</span></span>
</button>
</div>
`;
container.appendChild(section);
section.querySelectorAll('.mp-result.regenerating').forEach(card => {
setTimeout(() => card.classList.remove('regenerating'), 1200);
});
section.querySelectorAll('.r-rerun').forEach(b => b.addEventListener('click', e => {
e.stopPropagation();
rerunOne(b.closest('.mp-result'));
}));
section.querySelectorAll('.r-adopt').forEach(b => b.addEventListener('click', e => {
e.stopPropagation();
adoptOne(b.closest('.mp-result'));
}));
section.querySelector('.rerun-batch').addEventListener('click', () => {
appendBatch(n, 'rerun-all');
Shell.toast('全部重跑', n + ' 张图重新生成中 · 新批次已追加');
});
section.querySelector('.adopt-batch').addEventListener('click', () => {
const cards = section.querySelectorAll('.mp-result:not(.adopted)');
if (!cards.length) { Shell.toast('该批次已全部采用'); return; }
cards.forEach(c => { c.classList.remove('regenerating'); c.classList.add('adopted'); });
updateBatchSummary();
Shell.toast('已全部采用', cards.length + ' 张图入对应商品的 AI 素材 · 扣 ¥' + (cards.length * UNIT_PRICE).toFixed(2));
});
section.scrollIntoView({ behavior: 'smooth', block: 'end' });
updateBatchSummary();
}
function renderResultCards() {
const container = document.getElementById('pv-results');
// 首次生成清掉占位 placeholder-batch,保留已有真实批次(再次「立即生成」追加新批次)
container.querySelectorAll('.placeholder-batch').forEach(el => el.remove());
appendBatch(state.count, 'gen');
}
function rerunOne(card) {
if (!card) return;
appendBatch(1, 'rerun-one');
}
function adoptOne(card) {
if (!card || card.classList.contains('adopted')) return;
card.classList.remove('regenerating');
card.classList.add('adopted');
Shell.toast('已采用', '入对应商品的 AI 素材 · 扣 ¥' + UNIT_PRICE.toFixed(2));
updateBatchSummary();
}
function updateBatchSummary() {
document.querySelectorAll('#pv-results .pv-platform-section').forEach(section => {
const cards = section.querySelectorAll('.mp-result:not(.placeholder-only)');
const adopted = section.querySelectorAll('.mp-result.adopted').length;
const adoptedEl = section.querySelector('.adopt-batch .adopted');
const totalEl = section.querySelector('.adopt-batch .total');
if (adoptedEl) adoptedEl.textContent = adopted;
if (totalEl) totalEl.textContent = cards.length;
});
}
// 立即生成
document.getElementById('pc-go-btn').addEventListener('click', () => {
if (!state.selectedPlatform || !state.selectedProd) return;
Shell.toast('已提交任务', '可在 [任务中心] 查看进度');
renderPreviewSections();
// 已有真实批次时,语义是追加新批次(用户换了设置再次生成)
const container = document.getElementById('pv-results');
const hasReal = container && container.querySelector('.mp-result-batch:not(.placeholder-batch)');
if (hasReal) {
Shell.toast('已追加批次', state.count + ' 张图新增到下方 · 旧批次保留');
} else {
Shell.toast('已提交任务', '可在 [任务中心] 查看进度');
}
showPreviewContent();
renderResultCards();
});
// 批量按钮已下沉到每个批次内部 (.rerun-batch / .adopt-batch)
// 不再有全局 #pv-rerun-all / #pv-adopt-all
// URL ?product=商品名 → 替换默认选中(从 products.html 跳过来时携带)
(function applyUrlProduct() {
const q = new URLSearchParams(location.search);
@@ -1219,6 +1505,7 @@ document.getElementById('pc-go-btn').addEventListener('click', () => {
// 初始
renderSelectedProds();
updatePlatforms();
showPreviewEmpty(); // 默认新任务态 → 右侧显示空态
/* ============================================================
任务历史 (localStorage) · 仅在「立即生成」时创建任务
@@ -1230,6 +1517,32 @@ updatePlatforms();
let tasks = [];
let currentId = null;
let _draftSnap = null; // 临时新任务的状态缓存 (切到真实任务前保存, 切回时恢复)
function saveDraftSnap() {
_draftSnap = {
selectedProd: state.selectedProd,
selectedPlatform: state.selectedPlatform,
count: state.count,
};
}
function applyDraftToForm() {
const snap = _draftSnap || { selectedProd: null, selectedPlatform: null, count: 4 };
state.selectedProd = snap.selectedProd;
state.selectedPlatform = snap.selectedPlatform;
state.count = snap.count;
document.querySelectorAll('.pill-row[data-key="count"] .opt').forEach(b => b.classList.toggle('active', +b.dataset.val === state.count));
document.querySelectorAll('#platform-grid .platform-card').forEach(c => c.classList.toggle('selected', c.dataset.id === state.selectedPlatform));
renderSelectedProds();
updatePlatforms(); // updatePlatforms 内会调 renderPreviewSections, 重置占位 + 隐藏 batch
showPreviewEmpty(); // 临时任务态: 右侧空态
}
function backToDraft() {
if (currentId === null) return;
applyDraftToForm();
currentId = null;
renderTasksList();
}
const PLAT_NAME = {};
document.querySelectorAll('#platform-grid .platform-card').forEach(c => {
@@ -1267,13 +1580,23 @@ updatePlatforms();
function renderTasksList() {
const root = document.getElementById('tasks-list');
document.getElementById('tasks-count').textContent = tasks.length;
if (!tasks.length) {
root.innerHTML = `<div class="pc-tasks-empty">还没有历史任务<span class="mono">// 调整商品/平台/参数后生成</span></div>`;
return;
}
// 临时新任务卡 — 仅在 draft 态 (currentId=null) 或有暂存的 _draftSnap 时显示
// 立即生成后 currentId 指向真实任务,_draftSnap 也未设置 → 不自动追加新草稿
const isDraft = (currentId === null);
const hasDraft = isDraft || _draftSnap !== null;
const draftHtml = hasDraft ? `
<div class="pc-task-card draft${isDraft ? ' active' : ''}" data-draft="1">
<div class="nm">新任务</div>
<div class="meta">
<span class="pill-mini gen">编辑中</span>
</div>
</div>
` : '';
const STATUS_LABEL = { gen: '生成中', ok: '已完成', err: '失败' };
const sorted = [...tasks].sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));
root.innerHTML = sorted.map(t => `
const savedHtml = sorted.map(t => `
<div class="pc-task-card${t.id === currentId ? ' active' : ''}" data-id="${t.id}">
<div class="nm">${escapeHtml(t.name)}</div>
<div class="meta">
@@ -1285,7 +1608,12 @@ updatePlatforms();
</button>
</div>
`).join('');
root.querySelectorAll('.pc-task-card').forEach(card => {
root.innerHTML = draftHtml + savedHtml;
// draft 卡点击 → 切回临时态
const draftCard = root.querySelector('.pc-task-card.draft');
if (draftCard) draftCard.addEventListener('click', backToDraft);
// 真实任务卡点击 → loadTaskIntoForm
root.querySelectorAll('.pc-task-card:not(.draft)').forEach(card => {
card.addEventListener('click', e => {
if (e.target.closest('button.x')) return;
loadTaskIntoForm(card.dataset.id);
@@ -1310,6 +1638,7 @@ updatePlatforms();
function loadTaskIntoForm(id) {
const t = tasks.find(x => x.id === id);
if (!t) return;
if (currentId === null) saveDraftSnap(); // 切走 draft 前保存当前编辑状态
state.selectedProd = t.snap.selectedProd || null;
state.selectedPlatform = t.snap.selectedPlatform || null;
state.count = t.snap.count;
@@ -1317,11 +1646,14 @@ updatePlatforms();
document.querySelectorAll('#platform-grid .platform-card').forEach(c => c.classList.toggle('selected', c.dataset.id === state.selectedPlatform));
renderSelectedProds();
updatePlatforms();
showPreviewContent(); // 真实任务: 显示预览内容
renderResultCards(); // 重新渲染结果卡 (hover overlay + 批量 bar)
currentId = id;
renderTasksList();
}
function newBlankTask() {
_draftSnap = null; // 清空之前的 draft 缓存
state.selectedProd = null;
state.selectedPlatform = null;
state.count = 4;
@@ -1329,6 +1661,7 @@ updatePlatforms();
document.querySelectorAll('#platform-grid .platform-card').forEach(c => c.classList.remove('selected'));
renderSelectedProds();
updatePlatforms();
showPreviewEmpty(); // 新任务态: 右侧空态
currentId = null;
renderTasksList();
}
@@ -1354,6 +1687,13 @@ updatePlatforms();
tasks = load();
renderTasksList();
// URL ?taskId=xxx → 从任务中心跳过来时自动载入
(function applyUrlTaskId() {
const q = new URLSearchParams(location.search);
const tid = q.get('taskId');
if (tid && tasks.some(t => t.id === tid)) loadTaskIntoForm(tid);
})();
})();
</script>
</body>
+22 -15
View File
@@ -557,9 +557,6 @@
}
.asset-card .meta .pill {
padding: 2px 8px;
background: var(--accent-emerald-bg, #e6f4ec);
color: var(--accent-emerald, #1f8a51);
border: 1px solid var(--accent-emerald-bd, #c4e3d1);
border-radius: var(--r-sm);
font-size: 10.5px;
font-weight: 500;
@@ -912,7 +909,7 @@
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg>
</button>
<button class="filter" type="button" data-key="status">
全部状态
通过
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg>
</button>
<div class="right">
@@ -953,10 +950,6 @@
<div class="tab-pane" data-pane="videos">
<div class="pd-toolbar">
<div class="total">该商品视频项目 <span class="ct">(4)</span></div>
<button class="filter" type="button" data-key="status">
全部状态
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg>
</button>
<div class="right">
<button class="filter" type="button" data-key="sort">
最新导出
@@ -966,10 +959,10 @@
</div>
<div class="asset-grid">
<div class="asset-card"><div class="thumb placeholder" style="aspect-ratio: 9/16;"><span class="type-pill">视频 · 9:16</span><span class="ph-frame">补水面膜 · v3</span></div><div class="meta"><span class="pill pass" data-status="pass" title="点击切换状态">通过</span><span class="date">2026-05-20 12:08</span></div></div>
<div class="asset-card"><div class="thumb placeholder" style="aspect-ratio: 9/16;"><span class="type-pill">视频 · 9:16</span><span class="ph-frame">补水面膜 · v2</span></div><div class="meta"><span class="pill pass" data-status="pass" title="点击切换状态">通过</span><span class="date">2026-05-19 10:24</span></div></div>
<div class="asset-card"><div class="thumb placeholder" style="aspect-ratio: 9/16;"><span class="type-pill">视频 · 9:16</span><span class="ph-frame">熬夜急救 · v1</span></div><div class="meta"><span class="pill archive" data-status="archive" title="点击切换状态">归档</span><span class="date">2026-05-18 21:42</span></div></div>
<div class="asset-card"><div class="thumb placeholder" style="aspect-ratio: 9/16;"><span class="type-pill">视频 · 9:16</span><span class="ph-frame">补水面膜 · v1</span></div><div class="meta"><span class="pill fail" data-status="fail" title="点击切换状态">不通过</span><span class="date">2026-05-17 16:00</span></div></div>
<div class="asset-card" data-proj-status="done"><div class="thumb placeholder" style="aspect-ratio: 9/16;"><span class="type-pill">视频 · 9:16</span><span class="ph-frame">补水面膜 · v3</span></div><div class="meta"><span class="pill ok"><span class="dot"></span>已完成</span><span class="date">2026-05-20 12:08</span></div></div>
<div class="asset-card" data-proj-status="wip"><div class="thumb placeholder" style="aspect-ratio: 9/16;"><span class="type-pill">视频 · 9:16</span><span class="ph-frame">补水面膜 · v2</span></div><div class="meta"><span class="pill info"><span class="dot"></span>视频生成 4/6</span><span class="date">2026-05-19 10:24</span></div></div>
<div class="asset-card" data-proj-status="archived"><div class="thumb placeholder" style="aspect-ratio: 9/16;"><span class="type-pill">视频 · 9:16</span><span class="ph-frame">熬夜急救 · v1</span></div><div class="meta"><span class="pill neutral"><span class="dot"></span>已归档</span><span class="date">2026-05-18 21:42</span></div></div>
<div class="asset-card" data-proj-status="fail"><div class="thumb placeholder" style="aspect-ratio: 9/16;"><span class="type-pill">视频 · 9:16</span><span class="ph-frame">补水面膜 · v1</span></div><div class="meta"><span class="pill err"><span class="dot"></span>故事板失败</span><span class="date">2026-05-17 16:00</span></div></div>
</div>
<div class="pd-more"><button type="button">加载更多</button></div>
@@ -1273,15 +1266,17 @@
// 每个 (pane, key) 的可选项 + 默认值(数组首项)
const OPTIONS = {
'assets:type': ['全部类型', '模特上身图', '平台套图', '三视图'],
'assets:status': ['全部状态', '通过', '不通过', '归档'],
// 状态:默认 通过 · 没有「全部状态」选项,只能切换 通过 / 不通过 / 归档
'assets:status': ['通过', '不通过', '归档'],
'assets:sort': ['最新生成', '最早生成'],
'videos:status': ['全部状态', '通过', '不通过', '归档'],
'videos:sort': ['最新导出', '最早导出'],
'tasks:type': ['全部类型', '模特上身图', '平台套图', '视频素材', '三视图'],
'tasks:status': ['全部状态', '已完成', '生成中', '排队中', '失败'],
'tasks:date': ['全部', '今天', '近 7 天', '近 30 天'],
'tasks:sort': ['提交时间倒序', '提交时间正序'],
};
// 「状态」永远视为已生效筛选(没有"全部"选项),即便选了默认值也走过滤逻辑
const ALWAYS_APPLY_KEYS = new Set(['status']);
// 任务行 status pill 的 class → 中文标签
const TASK_STATUS_MAP = { ok: '已完成', gen: '生成中', wait: '排队中', err: '失败', fail: '失败' };
// assets 卡片 data-status → 中文标签
@@ -1385,6 +1380,7 @@
btn.dataset.value = val;
[...btn.childNodes].forEach(n => { if (n.nodeType === 3) n.remove(); });
btn.insertBefore(document.createTextNode(val + ' '), btn.firstChild);
// 仅在切到非默认值时才高亮(状态 chip 即便永远过滤,视觉也保持中性,与「全部类型」一致)
btn.classList.toggle('filtered', val !== btn.dataset.default);
}
@@ -1396,7 +1392,9 @@
});
const isDefault = (key) => {
const btn = paneEl.querySelector('.pd-toolbar .filter[data-key="' + key + '"]');
return !btn || btn.dataset.value === btn.dataset.default;
if (!btn) return true; // 该 pane 没这个 filter → 等同默认,不过滤
if (ALWAYS_APPLY_KEYS.has(key)) return false; // 状态有按钮时,永远走过滤
return btn.dataset.value === btn.dataset.default;
};
if (paneId === 'assets' || paneId === 'videos') {
@@ -1429,6 +1427,15 @@
}
updateCount(paneEl, visible);
toggleEmpty(paneEl, visible === 0);
// 不足 2 行时不显示「加载更多」按钮(布局后用 offsetTop 统计行数)
const moreEl = paneEl.querySelector('.pd-more');
if (moreEl) {
requestAnimationFrame(() => {
const visCards = [...paneEl.querySelectorAll('.asset-card')].filter(c => c.style.display !== 'none');
const rows = new Set(visCards.map(c => c.offsetTop)).size;
moreEl.style.display = rows >= 2 ? '' : 'none';
});
}
} else if (paneId === 'tasks') {
const rows = [...paneEl.querySelectorAll('.task-table .task-row:not(.head)')];
let visible = 0;
+1 -1
View File
@@ -734,7 +734,7 @@
<span class="corner-tr"></span><span class="corner-bl"></span>
<div class="picker-h">
<div class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 3v18"/></svg></div>
<div class="ti"><div class="t">生成白底三视图</div><div class="d">// 一次出 3 张 · 正面 / 侧面 / 背面</div></div>
<div class="ti"><div class="t">生成白底三视图</div><div class="d">// 单张 16:9 · 正 / 侧 / 背 合一</div></div>
<button class="x" onclick="closePicker('white')"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M18 6L6 18M6 6l12 12"/></svg></button>
</div>
<div class="picker-b">