feat(workbench): 三工序图片区视觉对齐 + 任务中心聚合 + 工具台头部筛选

- model-photo / platform-cover · 头部 toolbar 落地: 时间 / 模特(平台) chip 下拉 + 折叠搜索
- model-photo / platform-cover · 图片卡片样式同步图片创作 (.io-cell): bg / hover / .gen 脉冲 / .err 红框
- model-photo / platform-cover · 单图 hover overlay: 再次生成 + 下载 + 更多(加入资产库/删除)
- model-photo / platform-cover · 批次底栏: 再次生成图标统一 + 更多 menu(全部加入资产库/删除该批)
- model-photo · 修 TDZ bug: renderModelMini 调用挪到 MODELS 声明后, 解决整页崩溃
- model-photo · 去掉冗余 pv-summary, 商品自动选最近编辑, task 写入 name 字段
- image-optimize · 单图右上加再次生成图标, 加入 fs-image-tasks-image 与任务中心打通
- image-optimize · 输入区拆 3 行: + 在顶 / textarea 满宽 / 发送在底栏右; 参考图缩略与加号同 64×64
- asset-factory · 任务中心加时间 chip + image 类型 + 跳转表; 删冗余类型列
- pipeline · stage2 商品卡换商品库风格 + AI 生成三视图主 CTA + .tri-missing-badge[hidden] CSS 修复
This commit is contained in:
iye
2026-05-22 19:35:36 +08:00
parent f420af2069
commit 04335f3269
25 changed files with 13042 additions and 2700 deletions
+85 -29
View File
@@ -8,7 +8,7 @@
<style>
#page-content { padding: 24px 28px 60px; }
/* ─── 三 Hero 卡片网格(模特上身图 / 平台套图 / 图片优化 · 等比)─── */
/* ─── 三 Hero 卡片网格(模特上身图 / 平台套图 / 图片创作 · 等比)─── */
.factory-hero {
display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16px; margin-bottom: 56px;
@@ -118,9 +118,6 @@
.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 { 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; }
@@ -365,14 +362,6 @@
</li>
</ul>
<div class="platform-row">
<div class="platform-chip"><span class="code">DY</span><span class="nm">抖音</span></div>
<div class="platform-chip"><span class="code">TB</span><span class="nm">淘宝</span></div>
<div class="platform-chip"><span class="code">XHS</span><span class="nm">小红书</span></div>
<div class="platform-chip"><span class="code">PDD</span><span class="nm">拼多多</span></div>
<div class="platform-chip"><span class="code">AMZ</span><span class="nm">亚马逊</span></div>
</div>
<div class="factory-cta">
<a class="btn btn-primary btn-lg" href="platform-cover.html">
开始生成
@@ -391,16 +380,16 @@
</div>
</div>
<!-- 卡片 C · 图片优化(生成人物 / 商品三视图)-->
<!-- 卡片 C · 图片创作(自由创作 AI 图片工作台)-->
<div class="factory-card with-corners">
<span class="corner-tr" aria-hidden></span>
<span class="corner-bl" aria-hidden></span>
<div class="factory-body">
<div class="factory-text">
<span class="factory-tag">[ TRI-VIEW · OPTIMIZE ]</span>
<div class="factory-title">图片优化</div>
<div class="factory-desc">为人物 / 商品生成三视图,保证多镜头一致性</div>
<span class="factory-tag">[ IMAGE · STUDIO ]</span>
<div class="factory-title">图片创作</div>
<div class="factory-desc">自由创作 AI 图片,适用于详情图 / 海报 / 灵感速写</div>
<ul class="factory-features">
<li>
@@ -424,7 +413,7 @@
</ul>
<div class="factory-cta">
<a class="btn btn-primary btn-lg" href="model-photo.html?mode=tri">
<a class="btn btn-primary btn-lg" href="image-optimize.html">
开始生成
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
</a>
@@ -460,6 +449,16 @@
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
<input class="input" id="tc-search" placeholder="搜索任务名">
</div>
<div class="chip-wrap" data-key="time">
<button class="chip" type="button"><span class="chip-label">时间</span> <svg class="caret" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg></button>
<div class="chip-menu">
<div class="mi selected" data-value="all"><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><span>全部时间</span></div>
<div class="mi-sep"></div>
<div class="mi" data-value="today"><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><span>今天</span></div>
<div class="mi" data-value="1h"><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><span>1 小时内</span></div>
<div class="mi" data-value="10min"><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><span>10 分钟内</span></div>
</div>
</div>
<div class="chip-wrap" data-key="type">
<button class="chip" type="button"><span class="chip-label">任务类型</span> <svg class="caret" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg></button>
<div class="chip-menu"></div>
@@ -488,9 +487,8 @@
<table class="t">
<thead>
<tr>
<th style="width:32%">任务</th>
<th>类型</th>
<th style="width:140px">进度</th>
<th style="width:42%">任务</th>
<th style="width:160px">进度</th>
<th>状态</th>
<th style="width:120px">创建于</th>
<th style="width:48px"></th>
@@ -547,10 +545,19 @@ Shell.render({
(function () {
'use strict';
const TYPE_LABEL = { model: '模特上身图', platform: '平台套图' };
const TYPE_LABEL = { model: '模特上身图', platform: '平台套图', image: '图片创作' };
const STATUS_LABEL = { ok: '已完成', gen: '生成中', 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 KEY_BY_TYPE = {
model: 'fs-image-tasks-model',
platform: 'fs-image-tasks-platform',
image: 'fs-image-tasks-image',
};
const URL_BY_TYPE = {
model: 'model-photo.html',
platform: 'platform-cover.html',
image: 'image-optimize.html',
};
const taskGrid = document.getElementById('task-grid');
const listTbody = document.getElementById('task-list-tbody');
@@ -559,7 +566,21 @@ Shell.render({
let cards = []; // 动态生成的 .task-card 元素数组(顺序与任务时间倒序一致)
const state = { filter: 'all', type: 'all', search: '', view: 'list' };
const state = { filter: 'all', type: 'all', time: 'all', search: '', view: 'list' };
function _timeMatch(createdAt, key) {
if (key === 'all' || !createdAt) return true;
const now = Date.now();
const diff = now - Number(createdAt);
if (key === '10min') return diff <= 10 * 60 * 1000;
if (key === '1h') return diff <= 60 * 60 * 1000;
if (key === 'today') {
const a = new Date(now); const b = new Date(Number(createdAt));
return a.toDateString() === b.toDateString();
}
return true;
}
const TIME_LABEL = { all: '时间', today: '今天', '1h': '1 小时内', '10min': '10 分钟内' };
function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c])); }
@@ -593,9 +614,8 @@ Shell.render({
/* ---------- 点击行 / 卡片 → 跳转工作台(携带 taskId) ---------- */
function goToWorkbench(t) {
const url = (t.type === 'model' ? 'model-photo.html' : 'platform-cover.html')
+ '?taskId=' + encodeURIComponent(t.id);
location.href = url;
const base = URL_BY_TYPE[t.type] || URL_BY_TYPE.model;
location.href = base + '?taskId=' + encodeURIComponent(t.id);
}
/* ---------- 1. 从 task 数据生成卡片 + list 行 ---------- */
@@ -606,6 +626,7 @@ Shell.render({
card.dataset.type = t.type;
card.dataset.name = t.name;
card.dataset.taskId = t.id;
card.dataset.createdAt = String(t.createdAt || 0);
card.style.cursor = 'pointer';
card.innerHTML = `
<button class="card-del-btn" type="button" title="删除任务">
@@ -642,7 +663,6 @@ Shell.render({
</div>
</div>
</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>
@@ -719,6 +739,21 @@ Shell.render({
wrap.querySelectorAll('.mi').forEach(mi => mi.classList.toggle('selected', mi.dataset.value === state.type));
}
function syncTimeChip() {
const wrap = document.querySelector('.chip-wrap[data-key="time"]');
if (!wrap) return;
const label = wrap.querySelector('.chip-label');
const chip = wrap.querySelector('.chip');
if (state.time === 'all') {
label.textContent = '时间';
chip.classList.remove('active');
} else {
label.textContent = TIME_LABEL[state.time] || state.time;
chip.classList.add('active');
}
wrap.querySelectorAll('.mi').forEach(mi => mi.classList.toggle('selected', mi.dataset.value === state.time));
}
/* ---------- 3. applyFilter ---------- */
function applyFilter() {
const q = state.search.toLowerCase();
@@ -726,8 +761,9 @@ Shell.render({
cards.forEach(card => {
const okStatus = state.filter === 'all' || card.dataset.status === state.filter;
const okType = state.type === 'all' || card.dataset.type === state.type;
const okTime = _timeMatch(card.dataset.createdAt, state.time);
const okSearch = !q || (card.dataset.name || '').toLowerCase().includes(q);
const show = okStatus && okType && okSearch;
const show = okStatus && okType && okTime && okSearch;
card.style.display = show ? '' : 'none';
if (card._listRow) card._listRow.style.display = show ? '' : 'none';
if (show) visible++;
@@ -746,7 +782,7 @@ Shell.render({
document.getElementById('tc-sub-err').textContent = counts.err;
document.getElementById('tc-result-meta').innerHTML = `// 显示 <span class="count">${visible}</span> / ${cards.length} 个任务`;
document.getElementById('tc-clear').hidden = !(state.search || state.type !== 'all');
document.getElementById('tc-clear').hidden = !(state.search || state.type !== 'all' || state.time !== 'all');
// 空态
const emptyEl = document.getElementById('tc-empty');
@@ -799,6 +835,24 @@ Shell.render({
syncTypeChip();
applyFilter();
});
// time chip
const timeWrap = document.querySelector('.chip-wrap[data-key="time"]');
const timeMenu = timeWrap.querySelector('.chip-menu');
timeWrap.querySelector('.chip').addEventListener('click', e => {
e.stopPropagation();
const isOpen = timeWrap.classList.contains('open');
document.querySelectorAll('.chip-wrap.open').forEach(w => w.classList.remove('open'));
if (!isOpen) timeWrap.classList.add('open');
});
timeMenu.addEventListener('click', e => {
const mi = e.target.closest('.mi');
if (!mi) return;
e.stopPropagation();
state.time = mi.dataset.value;
timeWrap.classList.remove('open');
syncTimeChip();
applyFilter();
});
document.addEventListener('click', () => {
document.querySelectorAll('.chip-wrap.open').forEach(w => w.classList.remove('open'));
});
@@ -806,8 +860,10 @@ Shell.render({
document.getElementById('tc-clear').addEventListener('click', () => {
state.search = '';
state.type = 'all';
state.time = 'all';
document.getElementById('tc-search').value = '';
syncTypeChip();
syncTimeChip();
applyFilter();
Shell.toast('已清空筛选');
});