AirShelf/电商AI平台/asset-factory.html
iye 8a783ca36f
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 6s
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>
2026-05-21 21:12:03 +08:00

872 lines
37 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="zh-CN">
<head>
<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">
<style>
#page-content { padding: 24px 28px 60px; }
/* ─── 三 Hero 卡片网格(模特上身图 / 平台套图 / 图片优化 · 等比)─── */
.factory-hero {
display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16px; margin-bottom: 56px;
}
@media (max-width: 1400px) { .factory-hero { grid-template-columns: 1fr 1fr; } }
@media (max-width: 1000px) { .factory-hero { grid-template-columns: 1fr; } }
.factory-card {
position: relative;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
padding: 28px 30px;
overflow: hidden;
}
/* 卡片内 · 文上图下 单列(3 卡并排时保持视觉一致)*/
.factory-body {
display: flex; flex-direction: column;
gap: 18px;
height: 100%;
}
.factory-text { display: flex; flex-direction: column; min-width: 0; }
.factory-tag {
align-self: flex-start;
font-family: var(--font-mono); font-size: 10.5px;
color: var(--black-alpha-48); letter-spacing: .06em;
padding: 2px 8px;
border: 1px solid var(--border-faint);
background: var(--background-lighter);
border-radius: var(--r-sm);
margin-bottom: 14px;
}
.factory-title {
font-size: 22px; font-weight: 600;
letter-spacing: -.018em; line-height: 1.25;
color: var(--accent-black);
}
.factory-desc {
margin-top: 8px;
font-size: 13.5px; color: var(--black-alpha-64); line-height: 1.55;
}
/* feature 列表 */
.factory-features {
list-style: none; padding: 0;
margin: 22px 0 0;
display: flex; flex-direction: column; gap: 11px;
}
.factory-features li {
display: flex; align-items: center; gap: 10px;
font-size: 13px; color: var(--black-alpha-72); font-weight: 500;
}
.factory-features .ff-ic {
width: 26px; height: 26px;
display: inline-flex; align-items: center; justify-content: center;
background: var(--heat-12);
color: var(--heat);
border-radius: var(--r-md);
flex-shrink: 0;
}
.factory-features .ff-ic svg { width: 14px; height: 14px; }
/* 平台 chip 行 */
.platform-row {
display: flex; flex-wrap: wrap; gap: 8px;
margin-top: 20px;
}
.platform-chip {
display: inline-flex; align-items: center; gap: 7px;
height: 30px; padding: 0 12px 0 8px;
border: 1px solid var(--border-faint);
background: var(--surface);
border-radius: var(--r-pill);
transition: background var(--t-base);
cursor: pointer;
}
.platform-chip:hover { background: var(--black-alpha-4); }
.platform-chip .code {
display: inline-flex; align-items: center; justify-content: center;
width: 20px; height: 20px;
background: var(--accent-black); color: var(--accent-white);
font-family: var(--font-mono); font-size: 8.5px; font-weight: 600;
border-radius: var(--r-pill); letter-spacing: .04em;
}
.platform-chip .nm {
font-size: 12px; color: var(--accent-black); font-weight: 500;
}
/* CTA 行:主按钮 + 价格 mono */
.factory-cta {
margin-top: auto; padding-top: 24px;
display: flex; align-items: center; gap: 14px;
}
.factory-cta .cost {
font-family: var(--font-mono); font-size: 10.5px;
color: var(--black-alpha-48); letter-spacing: .04em;
}
/* 视觉占位 + feature 列表已隐藏 — CTA 卡片只保留标题 + 描述 + 按钮 */
.factory-visual { display: none; }
.factory-features { display: none; }
.model-visual { grid-template-columns: repeat(4, 1fr); }
.model-visual .main { aspect-ratio: 3 / 4; grid-column: span 1; }
.model-visual .stack { display: contents; }
.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; }
.section-h h2 { font-size: 18px; font-weight: 600; letter-spacing: -.01em; }
.section-h .sub-mono { font-family: var(--font-mono); font-size: 11.5px; color: var(--black-alpha-48); letter-spacing: .02em; }
/* ─── 视图切换 (复用 projects.html · 图标 + 文字) ─── */
.view-toggle { display: inline-flex; border: 1px solid var(--border-faint); border-radius: var(--r-md); overflow: hidden; }
.view-toggle button { padding: 0 14px; background: var(--surface); color: var(--black-alpha-56); font-size: 13px; border: 0; border-right: 1px solid var(--border-faint); border-radius: 0; height: 36px; cursor: pointer; font-family: inherit; display: flex; align-items: center; gap: 6px; transition: background var(--t-base), color var(--t-base); }
.view-toggle button:last-child { border-right: 0; }
.view-toggle button:hover { background: var(--background-lighter); color: var(--accent-black); }
.view-toggle button.active { background: var(--heat-12); color: var(--heat); font-weight: 600; }
.view-toggle button svg { width: 13px; height: 13px; }
/* ─── 网格视图 · 卡片(原 history-grid) ─── */
.history-grid {
display: grid; grid-template-columns: repeat(4, 1fr);
gap: 16px;
}
@media (max-width: 1280px) { .history-grid { grid-template-columns: repeat(3, 1fr); } }
@media (max-width: 960px) { .history-grid { grid-template-columns: repeat(2, 1fr); } }
.history-grid[hidden] { display: none; }
.history-card {
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
padding: 12px;
display: grid; grid-template-columns: 78px 1fr; gap: 14px;
align-items: center;
transition: background var(--t-base);
cursor: pointer;
position: relative;
}
.history-card:hover { background: var(--black-alpha-4); }
.history-card:hover .card-del-btn { opacity: 1; }
.history-card .placeholder { width: 78px; height: 78px; }
/* ─── 列表视图 · 表格 ─── */
#task-list-view[hidden] { display: none; }
.task-name-cell { display: flex; align-items: center; gap: 12px; }
.task-thumb { width: 40px; height: 40px; flex-shrink: 0; border-radius: var(--r-sm); }
.task-name { font-weight: 600; color: var(--accent-black); font-size: 13.5px; }
.task-sub { font-size: 11.5px; color: var(--black-alpha-48); margin-top: 3px; font-family: var(--font-mono); letter-spacing: .02em; }
table.t .task-list-prog { display: flex; align-items: center; gap: 8px; min-width: 120px; }
table.t .task-list-prog .bar { flex: 1; height: 4px; background: var(--black-alpha-7); border-radius: 2px; overflow: hidden; }
table.t .task-list-prog .bar span { display: block; height: 100%; background: var(--heat); border-radius: 2px; animation: hp-pulse 1.4s ease-in-out infinite; }
table.t .task-list-prog .pct { font-family: var(--font-mono); font-size: 10.5px; color: var(--heat); letter-spacing: .02em; white-space: nowrap; }
/* ─── 行末 ⋯ 删除气泡 (复用 projects.html) ─── */
.row-action { display: flex; gap: 4px; justify-content: flex-end; }
table.t tbody tr .row-more { opacity: 0; transition: opacity .15s; }
table.t tbody tr:hover .row-more { opacity: 1; }
.row-more {
position: relative; display: inline-flex;
cursor: pointer; align-items: center;
color: var(--black-alpha-56);
padding: 4px;
}
.row-more:hover { color: var(--accent-black); }
.row-more-tip {
position: absolute; top: calc(100% + 6px); right: 0;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
box-shadow: 0 4px 16px rgba(0,0,0,.08);
padding: 4px; min-width: 110px;
opacity: 0; pointer-events: none;
transform: translateY(-2px);
transition: opacity .15s, transform .15s;
z-index: 12;
}
.row-more-tip::before {
content: ''; position: absolute;
top: -8px; left: 0; right: 0; height: 8px;
}
.row-more:hover .row-more-tip,
.row-more-tip:hover { opacity: 1; pointer-events: auto; transform: translateY(0); }
.row-more-tip .mi {
display: flex; align-items: center; gap: 6px;
width: 100%; padding: 6px 10px;
background: transparent; border: 0;
border-radius: var(--r-sm); cursor: pointer;
font-size: 12.5px; color: var(--accent-black);
font-family: inherit; text-align: left;
transition: background var(--t-base), color var(--t-base);
}
.row-more-tip .mi:hover {
background: var(--crimson-bg, #fdebea);
color: var(--accent-crimson, #c43d3d);
}
.row-more-tip .mi svg { width: 13px; height: 13px; }
/* ─── 删除确认 modal · 复用 ─── */
.modal-bg.show { display: flex; }
.mono-acc { font-family: var(--font-mono); color: var(--heat); font-weight: 600; }
.history-body { min-width: 0; display: flex; flex-direction: column; gap: 4px; }
.history-name {
font-size: 13.5px; font-weight: 600; color: var(--accent-black);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.history-type {
font-size: 11.5px; color: var(--black-alpha-48);
font-family: var(--font-mono); letter-spacing: .02em;
}
.history-foot {
display: flex; align-items: center; justify-content: space-between;
margin-top: 4px; gap: 6px; min-width: 0;
}
.history-foot .mono {
font-family: var(--font-mono); font-size: 10.5px;
color: var(--black-alpha-48); letter-spacing: .02em;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.history-foot .pill { padding: 2px 8px; font-size: 10.5px; }
.history-foot .pill .dot { width: 5px; height: 5px; }
/* 进度条(生成中状态) */
.history-prog {
margin-top: 6px;
display: flex; align-items: center; gap: 8px;
}
.history-prog .bar {
flex: 1; height: 4px;
background: var(--black-alpha-7);
border-radius: 2px; overflow: hidden;
}
.history-prog .bar span {
display: block; height: 100%;
background: var(--heat); border-radius: 2px;
animation: hp-pulse 1.4s ease-in-out infinite;
}
@keyframes hp-pulse {
0%, 100% { opacity: 1; transform: scaleY(1); }
50% { opacity: .55; transform: scaleY(.7); }
}
.history-prog .pct {
font-family: var(--font-mono); font-size: 10px;
color: var(--heat); letter-spacing: .02em; white-space: nowrap;
}
</style>
</head>
<body>
<div id="page">
<div class="page-head">
<div>
<h1>图片生成</h1>
<div class="sub">
<span class="mono">// 一键生成</span>
<span>·</span>
<span>电商视觉素材,提升内容制作效率</span>
</div>
</div>
</div>
<!-- 双 Hero 卡片 -->
<div class="factory-hero">
<!-- 卡片 A · 模特上身图 -->
<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">[ MODEL · TRY-ON ]</span>
<div class="factory-title">模特上身图</div>
<div class="factory-desc">选择模特,AI 生成商品模特上身效果图</div>
<ul class="factory-features">
<li>
<span class="ff-ic">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="9" 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>
</span>
支持多模特选择
</li>
<li>
<span class="ff-ic">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="8" height="8"/><rect x="13" y="3" width="8" height="8"/><rect x="3" y="13" width="8" height="8"/><rect x="13" y="13" width="8" height="8"/></svg>
</span>
一次生成 4 张
</li>
<li>
<span class="ff-ic">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="m3 7 9-4 9 4-9 4-9-4z"/><path d="m3 12 9 4 9-4M3 17l9 4 9-4"/></svg>
</span>
支持多商品并行
</li>
</ul>
<div class="factory-cta">
<a class="btn btn-primary btn-lg" href="model-photo.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>
<span class="cost">[ ≈ ¥0.30 / 张 ]</span>
</div>
</div>
<div class="factory-visual model-visual">
<div class="placeholder main"><span class="ph-frame">Ava · 9:16</span></div>
<div class="stack">
<div class="placeholder"><span class="ph-frame">变体 01</span></div>
<div class="placeholder"><span class="ph-frame">变体 02</span></div>
<div class="placeholder"><span class="ph-frame">变体 03</span></div>
</div>
</div>
</div>
</div>
<!-- 卡片 B · 平台套图 -->
<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">[ PLATFORM · KIT ]</span>
<div class="factory-title">平台套图</div>
<div class="factory-desc">选择平台模板,AI 生成电商平台套图</div>
<ul class="factory-features">
<li>
<span class="ff-ic">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21V9M9 21V5M15 21v-8M21 21V11"/></svg>
</span>
覆盖主流电商平台
</li>
<li>
<span class="ff-ic">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="8" height="8"/><rect x="13" y="3" width="8" height="8"/><rect x="3" y="13" width="8" height="8"/><rect x="13" y="13" width="8" height="8"/></svg>
</span>
一键生成 4 张套图
</li>
<li>
<span class="ff-ic">
<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"/></svg>
</span>
智能排版设计
</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">
开始生成
<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>
<span class="cost">[ ≈ ¥0.50 / 张 ]</span>
</div>
</div>
<div class="factory-visual kit-visual">
<div class="placeholder"><span class="ph-frame">套图 / TB</span></div>
<div class="placeholder"><span class="ph-frame">套图 / DY</span></div>
<div class="placeholder"><span class="ph-frame">套图 / XHS</span></div>
<div class="placeholder"><span class="ph-frame">套图 / PDD</span></div>
</div>
</div>
</div>
<!-- 卡片 C · 图片优化(生成人物 / 商品三视图)-->
<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>
<ul class="factory-features">
<li>
<span class="ff-ic">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="9" r="3"/><path d="M3 19c0-3 2.7-5 6-5s6 2 6 5"/></svg>
</span>
人物 · 商品 全支持
</li>
<li>
<span class="ff-ic">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="6" height="18"/><rect x="9" y="3" width="6" height="18"/><rect x="15" y="3" width="6" height="18"/></svg>
</span>
正面 / 侧面 / 背面 一次输出
</li>
<li>
<span class="ff-ic">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/></svg>
</span>
多镜头一致性保证
</li>
</ul>
<div class="factory-cta">
<a class="btn btn-primary btn-lg" href="model-photo.html?mode=tri">
开始生成
<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>
<span class="cost">[ ≈ ¥0.40 / 组 ]</span>
</div>
</div>
<div class="factory-visual tri-visual">
<div class="placeholder"><span class="ph-frame">正 / 侧 / 背 · 三视图</span></div>
</div>
</div>
</div>
</div>
<!-- ============= 任务中心 · 参考 projects.html 布局 ============= -->
<div class="section-h">
<h2>任务中心</h2>
<span class="sub-mono">// <span id="tc-sub-total">0</span> 个 · <span id="tc-sub-gen">0</span> 生成中 · <span id="tc-sub-ok">0</span> 已完成 · <span id="tc-sub-err">0</span> 失败</span>
</div>
<!-- 状态 tabs (复用 .tabs) -->
<div class="tabs" id="tc-tabs">
<div class="tab active" data-filter="all">全部 <span class="count">0</span></div>
<div class="tab" data-filter="gen">生成中 <span class="count">0</span></div>
<div class="tab" data-filter="ok">已完成 <span class="count">0</span></div>
<div class="tab" data-filter="err">失败 <span class="count">0</span></div>
</div>
<!-- toolbar: search + 类型 chip + clear + view-toggle -->
<div class="toolbar">
<div class="search-inline">
<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="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>
</div>
<button class="clear-filters" id="tc-clear" type="button" hidden>
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 4l8 8M12 4l-8 8"/></svg>
清空筛选
</button>
<span class="spacer"></span>
<div class="view-toggle" id="tc-view-toggle">
<button type="button" data-view="grid">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2" y="2" width="5" height="5"/><rect x="9" y="2" width="5" height="5"/><rect x="2" y="9" width="5" height="5"/><rect x="9" y="9" width="5" height="5"/></svg>
网格
</button>
<button type="button" class="active" data-view="list">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 4h12M2 8h12M2 12h12"/></svg>
列表
</button>
</div>
</div>
<div class="result-meta" id="tc-result-meta">// 显示 <span class="count">0</span> / 0 个任务</div>
<!-- ============= LIST VIEW (默认) ============= -->
<div id="task-list-view">
<table class="t">
<thead>
<tr>
<th style="width:32%">任务</th>
<th>类型</th>
<th style="width:140px">进度</th>
<th>状态</th>
<th style="width:120px">创建于</th>
<th style="width:48px"></th>
</tr>
</thead>
<tbody id="task-list-tbody"><!-- JS 从卡片同步生成 --></tbody>
</table>
</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>
<!-- ===== 删除确认 modal (复用 projects.html 风格) ===== -->
<div class="modal-bg" id="tc-del-bg">
<div class="modal" role="dialog">
<span class="corner-tr" aria-hidden></span>
<span class="corner-bl" aria-hidden></span>
<div class="modal-h">
<div class="ic-m" style="background:var(--crimson-bg,#fdebea);color:var(--accent-crimson,#c43d3d)">
<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>
</div>
<div class="ti">确认删除任务<span>// CONFIRM DELETE</span></div>
</div>
<div class="modal-b" id="tc-del-body">即将删除任务记录。</div>
<div class="modal-f">
<button class="btn" type="button" id="tc-del-cancel">取消</button>
<button class="btn" type="button" id="tc-del-ok" style="background:var(--accent-crimson);color:var(--accent-white);border-color:var(--accent-crimson)">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/></svg>
确认删除
</button>
</div>
</div>
</div>
<script src="assets/shell.js?v=202605211643"></script>
<script src="assets/new-product-drawer.js?v=202605211643"></script>
<script>
Shell.render({
active: 'asset-factory',
crumbs: [{ label: '工作台', href: 'index.html' }, { label: '图片生成' }]
});
/* ============================================================
任务中心 · 从 localStorage 读取(与 model-photo / platform-cover 共享)
============================================================ */
(function () {
'use strict';
const TYPE_LABEL = { model: '模特上身图', platform: '平台套图' };
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 taskGrid = document.getElementById('task-grid');
const listTbody = document.getElementById('task-list-tbody');
const gridView = taskGrid;
const listView = document.getElementById('task-list-view');
let cards = []; // 动态生成的 .task-card 元素数组(顺序与任务时间倒序一致)
const state = { filter: 'all', type: 'all', search: '', view: 'list' };
function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c])); }
/* ---------- 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. 从 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 = 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(thumbLabelOf(t))}</span></div>
<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[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>
<div class="row-more-tip"><button class="mi mi-del-task" type="button"><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>
</span>
</div>
</td>
`;
tr.querySelector('.row-more').addEventListener('click', e => e.stopPropagation());
return tr;
}
/* ---------- 2. 全量重渲染(load → render → filter) ---------- */
function renderAll() {
// 清空 grid / list
taskGrid.innerHTML = '';
listTbody.innerHTML = '';
cards = [];
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');
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"]');
const label = wrap.querySelector('.chip-label');
const chip = wrap.querySelector('.chip');
if (state.type === 'all') {
label.textContent = '任务类型';
chip.classList.remove('active');
} else {
label.textContent = TYPE_LABEL[state.type] || state.type;
chip.classList.add('active');
}
wrap.querySelectorAll('.mi').forEach(mi => mi.classList.toggle('selected', mi.dataset.value === state.type));
}
/* ---------- 3. applyFilter ---------- */
function applyFilter() {
const q = state.search.toLowerCase();
let visible = 0;
cards.forEach(card => {
const okStatus = state.filter === 'all' || card.dataset.status === state.filter;
const okType = state.type === 'all' || card.dataset.type === state.type;
const okSearch = !q || (card.dataset.name || '').toLowerCase().includes(q);
const show = okStatus && okType && okSearch;
card.style.display = show ? '' : 'none';
if (card._listRow) card._listRow.style.display = show ? '' : 'none';
if (show) visible++;
});
// 计数
const counts = { all: cards.length, gen: 0, ok: 0, err: 0 };
cards.forEach(c => { if (counts[c.dataset.status] !== undefined) counts[c.dataset.status]++; });
document.querySelectorAll('#tc-tabs .tab').forEach(t => {
const f = t.dataset.filter;
t.querySelector('.count').textContent = f === 'all' ? counts.all : counts[f];
});
document.getElementById('tc-sub-total').textContent = counts.all;
document.getElementById('tc-sub-gen').textContent = counts.gen;
document.getElementById('tc-sub-ok').textContent = counts.ok;
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');
// 空态
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. 事件绑定 ---------- */
// status tabs
document.querySelectorAll('#tc-tabs .tab').forEach(t => {
t.addEventListener('click', () => {
document.querySelectorAll('#tc-tabs .tab').forEach(x => x.classList.remove('active'));
t.classList.add('active');
state.filter = t.dataset.filter;
applyFilter();
});
});
// search
document.getElementById('tc-search').addEventListener('input', e => {
state.search = e.target.value.trim();
applyFilter();
});
// type chip
const typeWrap = document.querySelector('.chip-wrap[data-key="type"]');
typeWrap.querySelector('.chip').addEventListener('click', e => {
e.stopPropagation();
const isOpen = typeWrap.classList.contains('open');
document.querySelectorAll('.chip-wrap.open').forEach(w => w.classList.remove('open'));
if (!isOpen) typeWrap.classList.add('open');
});
typeMenu.addEventListener('click', e => {
const mi = e.target.closest('.mi');
if (!mi) return;
e.stopPropagation();
state.type = mi.dataset.value;
typeWrap.classList.remove('open');
syncTypeChip();
applyFilter();
});
document.addEventListener('click', () => {
document.querySelectorAll('.chip-wrap.open').forEach(w => w.classList.remove('open'));
});
// clear filters
document.getElementById('tc-clear').addEventListener('click', () => {
state.search = '';
state.type = 'all';
document.getElementById('tc-search').value = '';
syncTypeChip();
applyFilter();
Shell.toast('已清空筛选');
});
// view toggle
document.querySelectorAll('#tc-view-toggle button').forEach(b => {
b.addEventListener('click', () => {
document.querySelectorAll('#tc-view-toggle button').forEach(x => x.classList.remove('active'));
b.classList.add('active');
state.view = b.dataset.view;
if (state.view === 'list') { listView.hidden = false; gridView.hidden = true; }
else { listView.hidden = true; gridView.hidden = false; }
});
});
/* ---------- 6. 删除 modal + 同步 localStorage ---------- */
const delBg = document.getElementById('tc-del-bg');
const delBody = document.getElementById('tc-del-body');
const delCancel = document.getElementById('tc-del-cancel');
const delOk = document.getElementById('tc-del-ok');
let _delTarget = null;
function openDelConfirm(target) {
_delTarget = target;
const name = target.dataset.name || '该任务';
delBody.innerHTML = '即将删除任务 <span class="mono-acc">' + esc(name) + '</span>。任务记录将清除,已入库的素材不受影响。';
delBg.classList.add('show');
}
function closeDelConfirm() { delBg.classList.remove('show'); _delTarget = null; }
delCancel.addEventListener('click', closeDelConfirm);
delBg.addEventListener('click', e => { if (e.target === delBg) closeDelConfirm(); });
delOk.addEventListener('click', () => {
if (!_delTarget) return;
const card = _delTarget._card || _delTarget; // 可能传 row 或 card
const taskId = card.dataset.taskId;
const taskType = card.dataset.type;
const name = card.dataset.name;
// 从 localStorage 移除对应任务
if (taskType && KEY_BY_TYPE[taskType]) {
const arr = loadType(taskType).filter(t => t.id !== taskId);
saveType(taskType, arr);
}
closeDelConfirm();
Shell.toast('已删除', name);
renderAll();
});
// 列表行 删除按钮(事件委托,因为是动态生成)
listTbody.addEventListener('click', e => {
const btn = e.target.closest('.mi-del-task');
if (!btn) return;
e.stopPropagation();
const tr = btn.closest('tr');
if (tr) openDelConfirm(tr);
});
/* ---------- 7. 初始化 ---------- */
renderAll();
})();
</script>
</body>
</html>