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:
+408
-68
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user