feat(workbench): 统一立绘详情页参考布局 · 三视图全 16:9 · 工作台批次追加
详情页 (pipeline / library / model-photo) - 统一参考布局:大立绘+缩略 strip+查看大图,右栏 三视图+简介(标签 chip)+3 列属性表 - 底部仅留「下载」+「使用该资产」,去除收藏 / 关闭 三视图固定单张 16:9 - pipeline / library / model-photo / asset-factory / product-studio 全部同步 - 移除原 actor 3 列 3:4 拆图,改为单容器 16:9 图片工作台 (model-photo / platform-cover) - 立即生成 + 全部重跑 + 单张重跑 均追加新批次到下方,旧批次保留 - 批量按钮下沉到每批次下方,与图片网格左对齐 - hover 重跑/采用 icon 缩小至 26px,右下角横向,无遮罩层 - 立即生成后不再自动新增「编辑中」草稿卡 新建商品 drawer - 无 onSave 回调时默认跳转 product-detail - 卖点新增 「+ 添加卖点」按钮(输入框下方独立行,左对齐) product-detail - 视频项目卡片状态 pill 改为 4 态(已完成/视频生成 4/6/已归档/故事板失败) - 移除视频卡个体「通过/不通过/归档」状态切换 - 去掉冗余「通过」status 筛选;过滤逻辑兼容缺失按钮 sidebar (shell.js) - 图片生成补 badge 12,团队去 badge 清理 - 删除 v2/ 历史镜像目录(与 电商AI平台/ 重复,Dockerfile build context 不依赖) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+161
-190
@@ -118,8 +118,9 @@
|
||||
.model-visual .stack .placeholder { aspect-ratio: 3 / 4; }
|
||||
.kit-visual { grid-template-columns: repeat(4, 1fr); }
|
||||
.kit-visual .placeholder { aspect-ratio: 1 / 1; }
|
||||
.tri-visual { grid-template-columns: repeat(3, 1fr); }
|
||||
.tri-visual .placeholder { aspect-ratio: 1 / 1; }
|
||||
.tri-visual { display: block; }
|
||||
.tri-visual .placeholder { aspect-ratio: 16 / 9; }
|
||||
.tri-visual .placeholder + .placeholder { display: none; }
|
||||
|
||||
/* ─── 任务中心 · section header ─── */
|
||||
.section-h { display: flex; align-items: center; gap: 12px; margin-top: 24px; margin-bottom: 14px; }
|
||||
@@ -432,9 +433,7 @@
|
||||
</div>
|
||||
|
||||
<div class="factory-visual tri-visual">
|
||||
<div class="placeholder"><span class="ph-frame">正面</span></div>
|
||||
<div class="placeholder"><span class="ph-frame">侧面</span></div>
|
||||
<div class="placeholder"><span class="ph-frame">背面</span></div>
|
||||
<div class="placeholder"><span class="ph-frame">正 / 侧 / 背 · 三视图</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -501,117 +500,13 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- ============= GRID VIEW ============= -->
|
||||
<div class="history-grid" id="task-grid" hidden>
|
||||
|
||||
<div class="task-card history-card" data-status="ok" data-type="model" data-name="补水面膜 × Ava">
|
||||
<button class="card-del-btn" type="button" title="删除任务" onclick="event.stopPropagation();" data-action="delete-task"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
|
||||
<div class="placeholder"><span class="ph-frame">Ava · 1:1</span></div>
|
||||
<div class="history-body">
|
||||
<div class="history-name">补水面膜 × Ava</div>
|
||||
<div class="history-type">模特上身图 · 4 张</div>
|
||||
<div class="history-foot">
|
||||
<span class="mono">// 05.19 · 14:30</span>
|
||||
<span class="pill ok"><span class="dot"></span>已完成</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="task-card history-card" data-status="ok" data-type="platform" data-name="精华液 × 平台套图">
|
||||
<button class="card-del-btn" type="button" title="删除任务" onclick="event.stopPropagation();"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
|
||||
<div class="placeholder"><span class="ph-frame">TB / XHS · 1:1</span></div>
|
||||
<div class="history-body">
|
||||
<div class="history-name">精华液 × 平台套图</div>
|
||||
<div class="history-type">平台套图 · 4 张</div>
|
||||
<div class="history-foot">
|
||||
<span class="mono">// 05.19 · 14:25</span>
|
||||
<span class="pill ok"><span class="dot"></span>已完成</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="task-card history-card" data-status="gen" data-type="model" data-name="防晒霜 × Luna">
|
||||
<button class="card-del-btn" type="button" title="删除任务" onclick="event.stopPropagation();"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
|
||||
<div class="placeholder"><span class="ph-frame">Luna · 1:1</span></div>
|
||||
<div class="history-body">
|
||||
<div class="history-name">防晒霜 × Luna</div>
|
||||
<div class="history-type">模特上身图 · 4 张</div>
|
||||
<div class="history-foot">
|
||||
<span class="mono">// 05.19 · 14:20</span>
|
||||
<span class="pill info"><span class="dot"></span>生成中</span>
|
||||
</div>
|
||||
<div class="history-prog">
|
||||
<div class="bar"><span style="width:65%"></span></div>
|
||||
<span class="pct">65%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="task-card history-card" data-status="err" data-type="platform" data-name="面霜 × 平台套图">
|
||||
<button class="card-del-btn" type="button" title="删除任务" onclick="event.stopPropagation();"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
|
||||
<div class="placeholder"><span class="ph-frame">DY / PDD · 1:1</span></div>
|
||||
<div class="history-body">
|
||||
<div class="history-name">面霜 × 平台套图</div>
|
||||
<div class="history-type">平台套图 · 4 张</div>
|
||||
<div class="history-foot">
|
||||
<span class="mono">// 05.19 · 14:15</span>
|
||||
<span class="pill err"><span class="dot"></span>失败</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="task-card history-card" data-status="ok" data-type="model" data-name="口红 × Mia">
|
||||
<button class="card-del-btn" type="button" title="删除任务" onclick="event.stopPropagation();"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
|
||||
<div class="placeholder"><span class="ph-frame">Mia · 1:1</span></div>
|
||||
<div class="history-body">
|
||||
<div class="history-name">口红 × Mia</div>
|
||||
<div class="history-type">模特上身图 · 4 张</div>
|
||||
<div class="history-foot">
|
||||
<span class="mono">// 05.18 · 21:08</span>
|
||||
<span class="pill ok"><span class="dot"></span>已完成</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="task-card history-card" data-status="ok" data-type="platform" data-name="眼霜 × 平台套图">
|
||||
<button class="card-del-btn" type="button" title="删除任务" onclick="event.stopPropagation();"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
|
||||
<div class="placeholder"><span class="ph-frame">TB / DY · 1:1</span></div>
|
||||
<div class="history-body">
|
||||
<div class="history-name">眼霜 × 平台套图</div>
|
||||
<div class="history-type">平台套图 · 8 张</div>
|
||||
<div class="history-foot">
|
||||
<span class="mono">// 05.18 · 18:42</span>
|
||||
<span class="pill ok"><span class="dot"></span>已完成</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="task-card history-card" data-status="ok" data-type="model" data-name="瑜伽裤 × Zoe">
|
||||
<button class="card-del-btn" type="button" title="删除任务" onclick="event.stopPropagation();"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
|
||||
<div class="placeholder"><span class="ph-frame">Zoe · 1:1</span></div>
|
||||
<div class="history-body">
|
||||
<div class="history-name">瑜伽裤 × Zoe</div>
|
||||
<div class="history-type">模特上身图 · 12 张</div>
|
||||
<div class="history-foot">
|
||||
<span class="mono">// 05.18 · 11:20</span>
|
||||
<span class="pill ok"><span class="dot"></span>已完成</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="task-card history-card" data-status="ok" data-type="platform" data-name="咖啡粉 × 平台套图">
|
||||
<button class="card-del-btn" type="button" title="删除任务" onclick="event.stopPropagation();"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
|
||||
<div class="placeholder"><span class="ph-frame">XHS / AMZ · 1:1</span></div>
|
||||
<div class="history-body">
|
||||
<div class="history-name">咖啡粉 × 平台套图</div>
|
||||
<div class="history-type">平台套图 · 4 张</div>
|
||||
<div class="history-foot">
|
||||
<span class="mono">// 05.17 · 16:50</span>
|
||||
<span class="pill ok"><span class="dot"></span>已完成</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- ============= GRID VIEW (JS 从 localStorage 动态渲染) ============= -->
|
||||
<div class="history-grid" id="task-grid" hidden></div>
|
||||
|
||||
<!-- 空态 -->
|
||||
<div id="tc-empty" hidden style="padding:60px 20px;text-align:center;color:var(--black-alpha-48);font-size:13px;line-height:1.6">
|
||||
<div style="font-family:var(--font-mono);font-size:11px;letter-spacing:.04em;margin-bottom:6px;">// NO TASKS YET</div>
|
||||
<div id="tc-empty-text">还没有任务,去上方选一个工序开始生成吧</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -647,68 +542,112 @@ Shell.render({
|
||||
});
|
||||
|
||||
/* ============================================================
|
||||
任务中心 · projects.html 风格 (tabs + toolbar + 双视图)
|
||||
任务中心 · 从 localStorage 读取(与 model-photo / platform-cover 共享)
|
||||
============================================================ */
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const TYPE_LABEL = { model: '模特上身图', platform: '平台套图' };
|
||||
const TYPE_LABEL = { model: '模特上身图', platform: '平台套图' };
|
||||
const STATUS_LABEL = { ok: '已完成', gen: '生成中', err: '失败' };
|
||||
const STATUS_PILL = { ok: 'ok', gen: 'info', err: 'err' };
|
||||
const STATUS_PILL = { ok: 'ok', gen: 'info', err: 'err' };
|
||||
const KEY_BY_TYPE = { model: 'fs-image-tasks-model', platform: 'fs-image-tasks-platform' };
|
||||
|
||||
const taskGrid = document.getElementById('task-grid');
|
||||
const listTbody = document.getElementById('task-list-tbody');
|
||||
const gridView = document.getElementById('task-grid');
|
||||
const gridView = taskGrid;
|
||||
const listView = document.getElementById('task-list-view');
|
||||
const cards = [...taskGrid.querySelectorAll('.task-card')];
|
||||
|
||||
let cards = []; // 动态生成的 .task-card 元素数组(顺序与任务时间倒序一致)
|
||||
|
||||
const state = { filter: 'all', type: 'all', search: '', view: 'list' };
|
||||
|
||||
function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
|
||||
|
||||
// 任务行点击 → 跳转到对应工作台,携带商品名(任务名一般是「商品 × 模特/平台」格式)
|
||||
function goToWorkbench(type, name) {
|
||||
const productName = (name || '').split(/\s[×x]\s/)[0].trim();
|
||||
const q = '?t=' + Date.now() + (productName ? '&product=' + encodeURIComponent(productName) : '');
|
||||
const url = (type === 'model') ? 'model-photo.html' + q
|
||||
: (type === 'platform') ? 'platform-cover.html' + q
|
||||
: 'projects-new.html' + q;
|
||||
/* ---------- localStorage 读写 ---------- */
|
||||
function loadType(type) {
|
||||
try { return JSON.parse(localStorage.getItem(KEY_BY_TYPE[type]) || '[]'); } catch (e) { return []; }
|
||||
}
|
||||
function saveType(type, arr) {
|
||||
try { localStorage.setItem(KEY_BY_TYPE[type], JSON.stringify(arr)); } catch (e) {}
|
||||
}
|
||||
function loadAllTasks() {
|
||||
const all = [];
|
||||
Object.keys(KEY_BY_TYPE).forEach(type => {
|
||||
loadType(type).forEach(t => all.push({ ...t, type })); // type 兜底
|
||||
});
|
||||
all.sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));
|
||||
return all;
|
||||
}
|
||||
|
||||
/* ---------- task → 渲染辅助 ---------- */
|
||||
function subTextOf(t) {
|
||||
const lbl = TYPE_LABEL[t.type] || t.type;
|
||||
const count = (t.snap && t.snap.count) || 4;
|
||||
return `${lbl} · ${count} 张`;
|
||||
}
|
||||
function thumbLabelOf(t) {
|
||||
// 取「商品 × 模特/平台」中右半作为缩略图占位文字
|
||||
const parts = (t.name || '').split(/\s[×x]\s/);
|
||||
return (parts[1] || parts[0] || '—').slice(0, 8);
|
||||
}
|
||||
|
||||
/* ---------- 点击行 / 卡片 → 跳转工作台(携带 taskId) ---------- */
|
||||
function goToWorkbench(t) {
|
||||
const url = (t.type === 'model' ? 'model-photo.html' : 'platform-cover.html')
|
||||
+ '?taskId=' + encodeURIComponent(t.id);
|
||||
location.href = url;
|
||||
}
|
||||
|
||||
/* ---------- 1. 从卡片生成 list 表行 (单数据源) ---------- */
|
||||
function rowFor(card) {
|
||||
const name = card.dataset.name;
|
||||
const type = card.dataset.type;
|
||||
const status = card.dataset.status;
|
||||
const subText = (card.querySelector('.history-type')?.textContent || '').trim();
|
||||
const timeText = (card.querySelector('.history-foot .mono')?.textContent || '').replace(/^\/\/\s*/, '');
|
||||
const pct = card.querySelector('.history-prog .pct')?.textContent || '';
|
||||
const pillClass = STATUS_PILL[status] || 'info';
|
||||
const pillLabel = STATUS_LABEL[status] || status;
|
||||
/* ---------- 1. 从 task 数据生成卡片 + list 行 ---------- */
|
||||
function cardFor(t) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'task-card history-card';
|
||||
card.dataset.status = t.status;
|
||||
card.dataset.type = t.type;
|
||||
card.dataset.name = t.name;
|
||||
card.dataset.taskId = t.id;
|
||||
card.style.cursor = 'pointer';
|
||||
card.innerHTML = `
|
||||
<button class="card-del-btn" type="button" title="删除任务">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg>
|
||||
</button>
|
||||
<div class="placeholder"><span class="ph-frame">${esc(thumbLabelOf(t))}</span></div>
|
||||
<div class="history-body">
|
||||
<div class="history-name">${esc(t.name)}</div>
|
||||
<div class="history-type">${esc(subTextOf(t))}</div>
|
||||
<div class="history-foot">
|
||||
<span class="mono">// ${esc(t.time || '')}</span>
|
||||
<span class="pill ${STATUS_PILL[t.status] || 'info'}"><span class="dot"></span>${esc(STATUS_LABEL[t.status] || t.status)}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
return card;
|
||||
}
|
||||
|
||||
function rowFor(t) {
|
||||
const tr = document.createElement('tr');
|
||||
tr.dataset.taskRow = '1';
|
||||
tr.dataset.name = name;
|
||||
tr.dataset.type = type;
|
||||
tr.dataset.status = status;
|
||||
tr.addEventListener('click', () => goToWorkbench(type, name));
|
||||
tr.dataset.name = t.name;
|
||||
tr.dataset.type = t.type;
|
||||
tr.dataset.status = t.status;
|
||||
tr.dataset.taskId = t.id;
|
||||
tr.addEventListener('click', () => goToWorkbench(t));
|
||||
tr.innerHTML = `
|
||||
<td>
|
||||
<div class="task-name-cell">
|
||||
<div class="placeholder task-thumb"><span class="ph-frame">${esc(name.split(' ')[0] || '')}</span></div>
|
||||
<div class="placeholder task-thumb"><span class="ph-frame">${esc(thumbLabelOf(t))}</span></div>
|
||||
<div>
|
||||
<div class="task-name">${esc(name)}</div>
|
||||
<div class="task-sub">${esc(subText)}</div>
|
||||
<div class="task-name">${esc(t.name)}</div>
|
||||
<div class="task-sub">${esc(subTextOf(t))}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td><span class="muted">${esc(TYPE_LABEL[type] || type)}</span></td>
|
||||
<td>${status === 'gen'
|
||||
? `<div class="task-list-prog"><div class="bar"><span style="width:${pct || '60%'}"></span></div><span class="pct">${esc(pct || '60%')}</span></div>`
|
||||
: (status === 'ok' ? '<span class="muted-2 mono" style="font-size:11px;">已完成</span>' : '<span class="muted-2 mono" style="font-size:11px;">—</span>')}</td>
|
||||
<td><span class="pill ${pillClass}"><span class="dot"></span>${esc(pillLabel)}</span></td>
|
||||
<td class="muted-2">${esc(timeText)}</td>
|
||||
<td><span class="muted">${esc(TYPE_LABEL[t.type] || t.type)}</span></td>
|
||||
<td>${t.status === 'gen'
|
||||
? `<div class="task-list-prog"><div class="bar"><span style="width:60%"></span></div><span class="pct">60%</span></div>`
|
||||
: (t.status === 'ok' ? '<span class="muted-2 mono" style="font-size:11px;">已完成</span>' : '<span class="muted-2 mono" style="font-size:11px;">—</span>')}</td>
|
||||
<td><span class="pill ${STATUS_PILL[t.status] || 'info'}"><span class="dot"></span>${esc(STATUS_LABEL[t.status] || t.status)}</span></td>
|
||||
<td class="muted-2">${esc(t.time || '')}</td>
|
||||
<td>
|
||||
<div class="row-action">
|
||||
<span class="row-more"><svg width="14" height="14" viewBox="0 0 16 16"><circle cx="3" cy="8" r="1.2" fill="currentColor"/><circle cx="8" cy="8" r="1.2" fill="currentColor"/><circle cx="13" cy="8" r="1.2" fill="currentColor"/></svg>
|
||||
@@ -717,24 +656,54 @@ Shell.render({
|
||||
</div>
|
||||
</td>
|
||||
`;
|
||||
// 阻止 row-more 区域冒泡触发整行点击
|
||||
tr.querySelector('.row-more').addEventListener('click', e => e.stopPropagation());
|
||||
return tr;
|
||||
}
|
||||
|
||||
cards.forEach(card => {
|
||||
const tr = rowFor(card);
|
||||
listTbody.appendChild(tr);
|
||||
card._listRow = tr;
|
||||
tr._card = card;
|
||||
});
|
||||
/* ---------- 2. 全量重渲染(load → render → filter) ---------- */
|
||||
function renderAll() {
|
||||
// 清空 grid / list
|
||||
taskGrid.innerHTML = '';
|
||||
listTbody.innerHTML = '';
|
||||
cards = [];
|
||||
|
||||
/* ---------- 2. 构建类型 chip 菜单 ---------- */
|
||||
const tasks = loadAllTasks();
|
||||
tasks.forEach(t => {
|
||||
const card = cardFor(t);
|
||||
const row = rowFor(t);
|
||||
taskGrid.appendChild(card);
|
||||
listTbody.appendChild(row);
|
||||
card._listRow = row;
|
||||
card._task = t;
|
||||
row._card = card;
|
||||
cards.push(card);
|
||||
|
||||
// 卡片点击 → 跳工作台
|
||||
card.addEventListener('click', e => {
|
||||
if (e.target.closest('.card-del-btn')) return;
|
||||
goToWorkbench(t);
|
||||
});
|
||||
// 卡片删除按钮
|
||||
card.querySelector('.card-del-btn').addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
openDelConfirm(card);
|
||||
});
|
||||
});
|
||||
|
||||
// 类型 chip 菜单(基于现有 task 的 type 集合,动态)
|
||||
rebuildTypeMenu();
|
||||
applyFilter();
|
||||
}
|
||||
|
||||
/* ---------- 3. 构建类型 chip 菜单(基于当前 cards 的 type 集合) ---------- */
|
||||
const checkSvg = '<svg class="mi-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg>';
|
||||
const typeMenu = document.querySelector('.chip-wrap[data-key="type"] .chip-menu');
|
||||
const typeOptions = [...new Set(cards.map(c => c.dataset.type))];
|
||||
typeMenu.innerHTML = `<div class="mi selected" data-value="all">${checkSvg}<span>全部任务类型</span></div><div class="mi-sep"></div>`
|
||||
+ typeOptions.map(v => `<div class="mi" data-value="${esc(v)}">${checkSvg}<span>${esc(TYPE_LABEL[v] || v)}</span></div>`).join('');
|
||||
function rebuildTypeMenu() {
|
||||
const typeOptions = [...new Set(cards.map(c => c.dataset.type))];
|
||||
typeMenu.innerHTML = `<div class="mi selected" data-value="all">${checkSvg}<span>全部任务类型</span></div><div class="mi-sep"></div>`
|
||||
+ typeOptions.map(v => `<div class="mi" data-value="${esc(v)}">${checkSvg}<span>${esc(TYPE_LABEL[v] || v)}</span></div>`).join('');
|
||||
syncTypeChip();
|
||||
}
|
||||
|
||||
function syncTypeChip() {
|
||||
const wrap = document.querySelector('.chip-wrap[data-key="type"]');
|
||||
@@ -778,6 +747,24 @@ Shell.render({
|
||||
|
||||
document.getElementById('tc-result-meta').innerHTML = `// 显示 <span class="count">${visible}</span> / ${cards.length} 个任务`;
|
||||
document.getElementById('tc-clear').hidden = !(state.search || state.type !== 'all');
|
||||
|
||||
// 空态
|
||||
const emptyEl = document.getElementById('tc-empty');
|
||||
const emptyText = document.getElementById('tc-empty-text');
|
||||
if (cards.length === 0) {
|
||||
emptyEl.hidden = false;
|
||||
emptyText.textContent = '还没有任务,去上方选一个工序开始生成吧';
|
||||
listView.hidden = true; gridView.hidden = true;
|
||||
} else if (visible === 0) {
|
||||
emptyEl.hidden = false;
|
||||
emptyText.textContent = '没有符合筛选条件的任务';
|
||||
// 视图本身保留,空表头也保留
|
||||
} else {
|
||||
emptyEl.hidden = true;
|
||||
// 恢复 view 显示(可能在 length===0 分支被隐藏)
|
||||
if (state.view === 'list') { listView.hidden = false; gridView.hidden = true; }
|
||||
else { listView.hidden = true; gridView.hidden = false; }
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- 4. 事件绑定 ---------- */
|
||||
@@ -835,7 +822,7 @@ Shell.render({
|
||||
});
|
||||
});
|
||||
|
||||
/* ---------- 5. 删除 modal + 配对删除 (复用 projects.html 模式) ---------- */
|
||||
/* ---------- 6. 删除 modal + 同步 localStorage ---------- */
|
||||
const delBg = document.getElementById('tc-del-bg');
|
||||
const delBody = document.getElementById('tc-del-body');
|
||||
const delCancel = document.getElementById('tc-del-cancel');
|
||||
@@ -853,37 +840,21 @@ Shell.render({
|
||||
delBg.addEventListener('click', e => { if (e.target === delBg) closeDelConfirm(); });
|
||||
delOk.addEventListener('click', () => {
|
||||
if (!_delTarget) return;
|
||||
const card = _delTarget._card || _delTarget; // 可能传 row (with ._card) 或 card (with ._listRow)
|
||||
const row = card._listRow;
|
||||
const card = _delTarget._card || _delTarget; // 可能传 row 或 card
|
||||
const taskId = card.dataset.taskId;
|
||||
const taskType = card.dataset.type;
|
||||
const name = card.dataset.name;
|
||||
card.remove();
|
||||
if (row) row.remove();
|
||||
// 同步 cards 数组
|
||||
const idx = cards.indexOf(card);
|
||||
if (idx >= 0) cards.splice(idx, 1);
|
||||
// 从 localStorage 移除对应任务
|
||||
if (taskType && KEY_BY_TYPE[taskType]) {
|
||||
const arr = loadType(taskType).filter(t => t.id !== taskId);
|
||||
saveType(taskType, arr);
|
||||
}
|
||||
closeDelConfirm();
|
||||
Shell.toast('已删除', name);
|
||||
applyFilter();
|
||||
renderAll();
|
||||
});
|
||||
|
||||
// 绑定网格卡 删除按钮
|
||||
document.querySelectorAll('.task-card .card-del-btn').forEach(btn => {
|
||||
btn.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
const card = btn.closest('.task-card');
|
||||
if (card) openDelConfirm(card);
|
||||
});
|
||||
});
|
||||
|
||||
// 绑定网格卡片点击 → 跳工作台
|
||||
cards.forEach(card => {
|
||||
card.style.cursor = 'pointer';
|
||||
card.addEventListener('click', e => {
|
||||
if (e.target.closest('.card-del-btn')) return;
|
||||
goToWorkbench(card.dataset.type, card.dataset.name);
|
||||
});
|
||||
});
|
||||
// 绑定列表行 删除按钮 (事件委托, 因为是动态生成)
|
||||
// 列表行 删除按钮(事件委托,因为是动态生成)
|
||||
listTbody.addEventListener('click', e => {
|
||||
const btn = e.target.closest('.mi-del-task');
|
||||
if (!btn) return;
|
||||
@@ -892,8 +863,8 @@ Shell.render({
|
||||
if (tr) openDelConfirm(tr);
|
||||
});
|
||||
|
||||
/* ---------- 6. 初始化 ---------- */
|
||||
applyFilter();
|
||||
/* ---------- 7. 初始化 ---------- */
|
||||
renderAll();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
+580
-320
File diff suppressed because it is too large
Load Diff
+642
-150
File diff suppressed because it is too large
Load Diff
+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>
|
||||
|
||||
+22
-15
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user