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
+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>