AirShelf/电商AI平台/asset-factory.html
iye 086d92991e
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 7s
统一 Airshelf 界面组件与图标
2026-05-27 12:29:41 +08:00

929 lines
40 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>图片生成 · Airshelf</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=2026052607">
<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; }
/* ─── 任务中心 · 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="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 · 图片创作(自由创作 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">[ IMAGE · STUDIO ]</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"/><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="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>
<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="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>
</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:42%">任务</th>
<th style="width:160px">进度</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/icons.js?v=2026052608"></script>
<script src="assets/shell.js?v=2026052607"></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: '平台套图', 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',
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');
const gridView = taskGrid;
const listView = document.getElementById('task-list-view');
let cards = []; // 动态生成的 .task-card 元素数组(顺序与任务时间倒序一致)
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])); }
/* ---------- 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 base = URL_BY_TYPE[t.type] || URL_BY_TYPE.model;
location.href = base + '?taskId=' + encodeURIComponent(t.id);
}
/* ---------- 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.dataset.createdAt = String(t.createdAt || 0);
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>${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));
}
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();
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 okTime = _timeMatch(card.dataset.createdAt, state.time);
const okSearch = !q || (card.dataset.name || '').toLowerCase().includes(q);
const show = okStatus && okType && okTime && 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' || state.time !== '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();
});
// 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'));
});
// clear filters
document.getElementById('tc-clear').addEventListener('click', () => {
state.search = '';
state.type = 'all';
state.time = 'all';
document.getElementById('tc-search').value = '';
syncTypeChip();
syncTimeChip();
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>