AirShelf/v2/asset-factory.html
UI 设计 e293aa43be
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 6s
feat(v2): 添加 V2.1 设计稿目录 · 团队/设置页 · pipeline 多项 mock 优化
2026-05-21 16:18:28 +08:00

901 lines
41 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">
<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 { grid-template-columns: repeat(3, 1fr); }
.tri-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="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 class="placeholder"><span class="ph-frame">侧面</span></div>
<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 ============= -->
<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>
</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"></script>
<script src="assets/new-product-drawer.js"></script>
<script>
Shell.render({
active: 'asset-factory',
crumbs: [{ label: '工作台', href: 'index.html' }, { label: '图片生成' }]
});
/* ============================================================
任务中心 · projects.html 风格 (tabs + toolbar + 双视图)
============================================================ */
(function () {
'use strict';
const TYPE_LABEL = { model: '模特上身图', platform: '平台套图' };
const STATUS_LABEL = { ok: '已完成', gen: '生成中', err: '失败' };
const STATUS_PILL = { ok: 'ok', gen: 'info', err: 'err' };
const taskGrid = document.getElementById('task-grid');
const listTbody = document.getElementById('task-list-tbody');
const gridView = document.getElementById('task-grid');
const listView = document.getElementById('task-list-view');
const cards = [...taskGrid.querySelectorAll('.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])); }
// 任务行点击 → 跳转到对应工作台,携带商品名(任务名一般是「商品 × 模特/平台」格式)
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;
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;
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.innerHTML = `
<td>
<div class="task-name-cell">
<div class="placeholder task-thumb"><span class="ph-frame">${esc(name.split(' ')[0] || '')}</span></div>
<div>
<div class="task-name">${esc(name)}</div>
<div class="task-sub">${esc(subText)}</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>
<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>
`;
// 阻止 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. 构建类型 chip 菜单 ---------- */
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 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');
}
/* ---------- 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; }
});
});
/* ---------- 5. 删除 modal + 配对删除 (复用 projects.html 模式) ---------- */
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 (with ._card) 或 card (with ._listRow)
const row = card._listRow;
const name = card.dataset.name;
card.remove();
if (row) row.remove();
// 同步 cards 数组
const idx = cards.indexOf(card);
if (idx >= 0) cards.splice(idx, 1);
closeDelConfirm();
Shell.toast('已删除', name);
applyFilter();
});
// 绑定网格卡 删除按钮
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;
e.stopPropagation();
const tr = btn.closest('tr');
if (tr) openDelConfirm(tr);
});
/* ---------- 6. 初始化 ---------- */
applyFilter();
})();
</script>
</body>
</html>