feat(workbench): 三工序图片区视觉对齐 + 任务中心聚合 + 工具台头部筛选
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 7s

- model-photo / platform-cover · 头部 toolbar 落地: 时间 / 模特(平台) chip 下拉 + 折叠搜索
- model-photo / platform-cover · 图片卡片样式同步图片创作 (.io-cell): bg / hover / .gen 脉冲 / .err 红框
- model-photo / platform-cover · 单图 hover overlay: 再次生成 + 下载 + 更多(加入资产库/删除)
- model-photo / platform-cover · 批次底栏: 再次生成图标统一 + 更多 menu(全部加入资产库/删除该批)
- model-photo · 修 TDZ bug: renderModelMini 调用挪到 MODELS 声明后, 解决整页崩溃
- model-photo · 去掉冗余 pv-summary, 商品自动选最近编辑, task 写入 name 字段
- image-optimize · 单图右上加再次生成图标, 加入 fs-image-tasks-image 与任务中心打通
- image-optimize · 输入区拆 3 行: + 在顶 / textarea 满宽 / 发送在底栏右; 参考图缩略与加号同 64×64
- asset-factory · 任务中心加时间 chip + image 类型 + 跳转表; 删冗余类型列
- pipeline · stage2 商品卡换商品库风格 + AI 生成三视图主 CTA + .tri-missing-badge[hidden] CSS 修复
This commit is contained in:
iye 2026-05-22 19:35:36 +08:00
parent f420af2069
commit 04335f3269
25 changed files with 13042 additions and 2700 deletions

99
CLAUDE.md Normal file
View File

@ -0,0 +1,99 @@
# 流·Studio · 电商 AI 平台 · Claude Code 工程约定
> **本文件由 Claude Code 启动时自动加载。所有 AI 协作必须遵循以下规则。**
---
## 项目简介
**流·Studio** · AI 短视频带货生成平台 · 5 阶段流水线(商品 → 故事板 → 镜头 → 生成 → 投放)
- **设计代号:** Restraint · V2.1 · Firecrawl-aligned
- **主要工作目录:** [电商AI平台/](电商AI平台/)
- **Next.js 工程(独立):** [app/](app/)
- **V1 历史归档:** [v1/](v1/)
- **V2.1 归档(原 v2.1/):** [v2/](v2/)
---
## ★ 设计规范铁律(每次涉及页面 / CSS / UI 必读)
### 触发条件
**只要任务涉及以下任一种,必须先 Read [电商AI平台/design.md](电商AI平台/design.md):**
- 修改 `.html` 文件
- 修改 `assets/restraint.css` 或任何 `.css`
- 修改 inline `<style>`
- 添加新页面 / 新组件
- 调整布局 / 间距 / 颜色 / 字号
- 用户提到"页面" "样式" "视觉" "组件" "色" "字" "圆角" "间距" 等关键词
### 必读章节
- [design.md §0 AI 协作铁律](电商AI平台/design.md#0--ai-协作铁律每次启动必读) — 必读
- [design.md §1 设计哲学](电商AI平台/design.md#1--设计哲学) — 价值观
- [design.md §3 组件清单](电商AI平台/design.md#4--组件清单restraintcss-已实现--不要重发明) — 用现成组件
- [design.md §8 Don't List](电商AI平台/design.md#8--dont-list绝对禁止--每次自检) — 自检
### 7 条铁律
1. **任何页面 / CSS 调整前必须 Read [电商AI平台/design.md](电商AI平台/design.md)** — 不读不动手
2. **检查 [电商AI平台/assets/restraint.css](电商AI平台/assets/restraint.css) 已有组件**`Grep ".btn|.pill|.input"`
3. **禁止在页面 inline `<style>` 重写共享类**(`.btn` `.pill` `.input` `.modal` `.drawer` `.toast` `.field` `.tabs` `.chip` `.stats` `.list-row` 等)— 要变体回 restraint.css 加
4. **禁止创建新色值** — 必须用 design.md §2.1 的 token,不写裸 hex
5. **禁止改动基础 token**(`--heat` `--background-base` `--border-faint` 等)— 改了破坏全站
6. **完成后对照 [design.md §8 Don't List](电商AI平台/design.md#8--dont-list绝对禁止--每次自检) 逐条自检**
7. **不确定就问用户**,不要凭感觉发挥 — 用户原话:"我都希望你能遵循我们的设计规范,而不是乱做"
---
## 设计核心速记(详见 design.md)
- **冷灰底** `#f9f9f9` · 主橙 `#fa5d19` · 主前景 `#262626`
- **全场 8 px 圆角**(Pill / dot 999 例外)· `>12 px` 直接判错
- **inside-border** 而非真 `border`(hover 不抖动)
- **单橙锚点** · 全场只有一个 accent · hover 用 alpha 不用换 hue
- **Mono 装饰必有** · `[ 200 OK ]` `// 05.14` `[ /v2 ]`(品牌签名)
- **主 CTA 唯一允许阴影** · 4 层橙色发光 · 其他场景禁阴影
- **Inter(英)+ Alibaba PuHuiTi(中)+ JetBrains Mono(装饰)** · 字符级 fallthrough
- **字重仅 3 档** · 400 / 500 / 600 · 700 仅给 Ctrl K 徽标
---
## Git 工作流
- **当前开发分支:** `dev`
- **主分支:** `main` (生产)
- **严禁直推 master/main** — 走 dev 分支 → PR → 合并触发 CI/CD
- **严禁 `--no-verify` 跳过 hook**
- **Push 规则:** 默认不 push,改完即停 · 用户明确说"push / 推一下"才执行
- **commit 前不要 amend** — 创建新 commit,避免破坏历史
## 文件操作
- **三视图 = 单张 16:9 图** · 不要拆成 3 张缩略 · 用 `aspect-ratio: 16/9` 单容器
- **设计稿优先** · 写代码前必须先读 [电商AI平台/_design_src/](电商AI平台/_design_src/) 设计稿(如果有)
- **`.pen` 文件加密** · 只能用 pencil MCP 工具,不能 Read/Grep
---
## 用户偏好
- **角色:** UI 设计师 · 不读代码报错,只看最终视觉结果
- **不需要的:** 终端报错截图、深奥的代码解释、过度的实施细节
- **需要的:** 简短状态更新、视觉结果对照、清晰的"对/错"反馈
---
## 关键路径速查
| 资产 | 路径 |
| ---- | ---- |
| **设计规范(SSoT)** | [电商AI平台/design.md](电商AI平台/design.md) |
| **共享 CSS** | [电商AI平台/assets/restraint.css](电商AI平台/assets/restraint.css) |
| **Shell 注入** | [电商AI平台/assets/shell.js](电商AI平台/assets/shell.js) |
| **视觉样板间(归档)** | [电商AI平台/_archive/design-system.html](电商AI平台/_archive/design-system.html) |
| **规范理论(归档)** | [电商AI平台/_archive/DESIGN_SPEC_V2.md](电商AI平台/_archive/DESIGN_SPEC_V2.md) |
| **设计稿源** | [电商AI平台/_design_src/](电商AI平台/_design_src/) |
---
**违反任何规范规则,用户有权要求重做,无需解释。**

Binary file not shown.

After

Width:  |  Height:  |  Size: 993 KiB

File diff suppressed because it is too large Load Diff

View File

@ -52,6 +52,34 @@
.balance-actions .btn-ghost { background: transparent; color: var(--accent-white); border: 1px solid rgba(255,255,255,.25); flex: 1; }
.balance-actions .btn-ghost:hover { background: rgba(255,255,255,.1); color: var(--accent-white); }
/* 导出菜单 */
.balance-actions .export-menu {
position: absolute; right: 0; top: calc(100% + 6px); z-index: 30;
min-width: 220px;
background: var(--surface); color: var(--accent-black);
border: 1px solid var(--border-faint); border-radius: var(--r-md);
box-shadow: 0 8px 24px rgba(0,0,0,.16);
padding: 6px;
display: flex; flex-direction: column; gap: 2px;
}
.balance-actions .export-menu[hidden] { display: none; }
.balance-actions .export-menu button {
background: transparent; border: 0; padding: 8px 10px;
display: grid; grid-template-columns: 22px 1fr; gap: 8px;
align-items: center; cursor: pointer;
border-radius: var(--r-sm);
font-family: inherit; font-size: 13px; color: var(--accent-black);
text-align: left;
}
.balance-actions .export-menu button:hover { background: var(--background-lighter); }
.balance-actions .export-menu button .ic {
width: 22px; height: 22px; display: grid; place-items: center;
font-family: var(--font-mono); color: var(--heat); font-size: 14px; font-weight: 700;
background: var(--heat-12); border-radius: var(--r-sm);
}
.balance-actions .export-menu button .t { display: flex; flex-direction: column; min-width: 0; line-height: 1.3; }
.balance-actions .export-menu button .t .d { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; margin-top: 2px; }
/* ─── 快速充值 pane(右栏)─── */
.pane { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 20px; margin-bottom: 16px; }
.pane h3 { font-size: 14px; font-weight: 600; margin-bottom: 6px; }
@ -85,10 +113,10 @@
.tab-panel { display: none; }
.tab-panel.active { display: block; }
/* ─── 总览 · 趋势 + 阶段分布 ─── */
.overview-grid { display: grid; grid-template-columns: 1.6fr 1fr; gap: 16px; align-items: start; }
/* ─── 总览 · 趋势 + 阶段分布 (两栏等高) ─── */
.overview-grid { display: grid; grid-template-columns: 1.6fr 1fr; gap: 16px; align-items: stretch; }
.trend-pane { padding: 18px 20px 14px; }
.trend-pane { padding: 18px 20px 14px; display: flex; flex-direction: column; }
.trend-head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 14px; }
.trend-head h3 { margin-bottom: 0; }
.trend-head .sub { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; }
@ -96,7 +124,7 @@
.trend-head .chip { font-family: var(--font-mono); font-size: 10.5px; padding: 3px 8px; border: 1px solid var(--border-faint); border-radius: var(--r-pill); color: var(--black-alpha-56); cursor: pointer; }
.trend-head .chip.active { background: var(--accent-black); color: var(--accent-white); border-color: var(--accent-black); }
.trend-chart { display: grid; grid-template-rows: 1fr auto; gap: 6px; height: 170px; padding: 6px 4px 2px; position: relative; }
.trend-chart { display: grid; grid-template-rows: 1fr auto; gap: 6px; min-height: 170px; flex: 1; padding: 6px 4px 2px; position: relative; }
.trend-chart .bars { display: grid; grid-template-columns: repeat(14, 1fr); gap: 5px; align-items: end; height: 100%; }
.trend-chart .bar { background: var(--background-lighter); border-radius: 2px 2px 0 0; position: relative; transition: background var(--t-base); cursor: pointer; }
.trend-chart .bar > span { display: block; width: 100%; background: var(--heat); border-radius: 2px 2px 0 0; }
@ -218,7 +246,17 @@
</div>
<div class="balance-actions">
<button class="btn btn-lg" onclick="openTopup()">充值</button>
<button class="btn btn-ghost btn-lg" onclick="Shell.toast('提取明细', 'CSV · /billing/export')">导出账单</button>
<div class="export-wrap" style="flex:1; position:relative;">
<button class="btn btn-ghost btn-lg" id="export-trigger" type="button" style="width:100%;">
导出账单
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-left:6px; vertical-align:-1px;"><polyline points="6 9 12 15 18 9"/></svg>
</button>
<div class="export-menu" id="export-menu" hidden>
<button type="button" data-fmt="csv"><span class="ic"></span><span class="t">CSV<span class="d">// 全部明细 · Excel 兼容</span></span></button>
<button type="button" data-fmt="xlsx"><span class="ic"></span><span class="t">XLSX<span class="d">// 含格式 + 公式</span></span></button>
<button type="button" data-fmt="pdf"><span class="ic"></span><span class="t">PDF<span class="d">// 含发票抬头</span></span></button>
</div>
</div>
</div>
</div>
@ -256,11 +294,11 @@
<div class="pane trend-pane">
<div class="trend-head">
<h3>消费趋势</h3>
<span class="sub">// 近 14 天 · 单位 ¥</span>
<span class="sub" id="trend-sub">// 近 14 天 · 单位 ¥</span>
<span class="spacer"></span>
<button class="chip active"></button>
<button class="chip"></button>
<button class="chip"></button>
<button class="chip active" data-grain="day"></button>
<button class="chip" data-grain="week"></button>
<button class="chip" data-grain="month"></button>
</div>
<div class="trend-chart">
<div class="bars" id="trend-bars">
@ -314,10 +352,20 @@
<!-- ===== Tab 2: 按项目 ===== -->
<div class="tab-panel" id="panel-by-project">
<div class="filter-bar">
<select><option>全部状态</option><option>进行中</option><option>已完成</option><option>已归档</option></select>
<select><option>本月</option><option>近 30 天</option><option>近 90 天</option></select>
<select id="proj-f-status">
<option value="all">全部状态</option>
<option value="wip">进行中</option>
<option value="ok">已完成</option>
<option value="fail">失败 · 待重跑</option>
</select>
<select id="proj-f-range">
<option value="month">本月</option>
<option value="30d">近 30 天</option>
<option value="90d">近 90 天</option>
</select>
<button class="chip" id="proj-f-reset" type="button" style="height:30px;font-size:11px;display:none;">清除筛选</button>
<span class="spacer"></span>
<span class="ct"><b style="color:var(--accent-black);">8</b> 个项目 · 当月消耗 ¥162.60</span>
<span class="ct" id="proj-count"><b style="color:var(--accent-black);">0</b> 个项目 · 消耗 ¥0.00</span>
</div>
<table class="billing-table">
<thead>
@ -327,22 +375,35 @@
<th>所属成员</th>
<th>当前阶段</th>
<th>状态</th>
<th style="text-align:right;">当月消耗</th>
<th style="text-align:right;">消耗</th>
</tr>
</thead>
<tbody id="proj-body">
<!-- JS 注入 -->
</tbody>
</table>
<div id="proj-empty" class="empty-state" style="display:none; padding:48px 0; text-align:center; font-family:var(--font-mono); font-size:12px; color:var(--black-alpha-48); letter-spacing:.02em;">
// 当前筛选条件下没有项目 · 试试调整状态 / 时间范围
</div>
</div>
<!-- ===== Tab 3: 按成员 ===== -->
<div class="tab-panel" id="panel-by-member">
<div class="filter-bar">
<select><option>全部角色</option><option>超管</option><option>团管</option><option>成员</option></select>
<select><option>本月</option><option>近 30 天</option><option>近 90 天</option></select>
<select id="mem-f-role">
<option value="all">全部角色</option>
<option value="super">超管</option>
<option value="admin">团管</option>
<option value="member">成员</option>
</select>
<select id="mem-f-range">
<option value="month">本月</option>
<option value="30d">近 30 天</option>
<option value="90d">近 90 天</option>
</select>
<button class="chip" id="mem-f-reset" type="button" style="height:30px;font-size:11px;display:none;">清除筛选</button>
<span class="spacer"></span>
<span class="ct"><b style="color:var(--accent-black);">5</b> 人 · 当月合计 ¥319.00</span>
<span class="ct" id="mem-count"><b style="color:var(--accent-black);">0</b> 人 · 合计 ¥0.00</span>
</div>
<table class="billing-table">
<thead>
@ -350,7 +411,7 @@
<th>成员</th>
<th>角色</th>
<th>已完成项目</th>
<th>当月已用 / 月度额度</th>
<th>已用 / 月度额度</th>
<th>最近活跃</th>
</tr>
</thead>
@ -358,16 +419,39 @@
<!-- JS 注入 -->
</tbody>
</table>
<div id="mem-empty" class="empty-state" style="display:none; padding:48px 0; text-align:center; font-family:var(--font-mono); font-size:12px; color:var(--black-alpha-48); letter-spacing:.02em;">
// 当前筛选条件下没有成员 · 试试调整角色 / 时间范围
</div>
</div>
<!-- ===== Tab 4: 账单流水 ===== -->
<div class="tab-panel" id="panel-bills">
<div class="filter-bar">
<select><option>全部阶段</option><option>视频片段</option><option>故事板</option><option>基础资产</option><option>脚本 LLM</option><option>充值</option><option>导出</option></select>
<select><option>全部成员</option><option>小李</option><option>张运营</option><option>王小姐</option><option>陈策划</option></select>
<select><option>近 30 天</option><option>近 7 天</option><option>本月</option><option>全部</option></select>
<select id="bills-f-stage">
<option value="all">全部阶段</option>
<option value="视频片段">视频片段</option>
<option value="故事板">故事板</option>
<option value="基础资产">基础资产</option>
<option value="脚本 LLM">脚本 LLM</option>
<option value="充值">充值</option>
<option value="导出">导出</option>
</select>
<select id="bills-f-member">
<option value="all">全部成员</option>
<option value="李">小李</option>
<option value="张">张运营</option>
<option value="王">王小姐</option>
<option value="陈">陈策划</option>
</select>
<select id="bills-f-range">
<option value="30d">近 30 天</option>
<option value="7d">近 7 天</option>
<option value="month">本月</option>
<option value="all">全部</option>
</select>
<button class="chip" id="bills-f-reset" type="button" style="height:30px;font-size:11px;display:none;">清除筛选</button>
<span class="spacer"></span>
<span class="ct"><b style="color:var(--accent-black);">28</b> 条 · <button class="chip" style="height:24px;font-size:11px;margin-left:4px;" onclick="Shell.toast('导出 CSV', '/billing/export?range=30d')">导出 CSV</button></span>
<span class="ct"><b id="bills-count" style="color:var(--accent-black);">0</b></span>
</div>
<table class="billing-table">
<thead>
@ -384,6 +468,9 @@
<!-- JS 注入 -->
</tbody>
</table>
<div id="bills-empty" class="empty-state" style="display:none; padding:48px 0; text-align:center; font-family:var(--font-mono); font-size:12px; color:var(--black-alpha-48); letter-spacing:.02em;">
// 当前筛选条件下没有账单 · 试试调整阶段 / 成员 / 时间范围
</div>
</div>
<!-- 充值 modal -->
@ -427,6 +514,24 @@ const TREND_DAYS = [
{ d: '05.20', v: 19.40 }, { d: '05.21', v: 7.80 },
];
// 8 周(以「W18~W21 + 前 4 周」做演示),按 7 天合计估算
const TREND_WEEKS = [
{ d: 'W14', v: 64.20 }, { d: 'W15', v: 92.80 }, { d: 'W16', v: 118.40 }, { d: 'W17', v: 78.60 },
{ d: 'W18', v: 102.30 }, { d: 'W19', v: 138.20 }, { d: 'W20', v: 86.40 }, { d: 'W21', v: 27.20 },
];
// 6 个月 demo,自然月聚合估算 + 当月真实合计 162.60
const TREND_MONTHS = [
{ d: '2025-12', v: 384.20 }, { d: '2026-01', v: 528.40 }, { d: '2026-02', v: 296.80 },
{ d: '2026-03', v: 412.00 }, { d: '2026-04', v: 348.60 }, { d: '2026-05', v: 162.60 },
];
const TREND_DATA = { day: TREND_DAYS, week: TREND_WEEKS, month: TREND_MONTHS };
const TREND_LABEL = {
day: { sub: '// 近 14 天 · 单位 ¥', sumLbl: '14 天合计', avgLbl: '日均' },
week: { sub: '// 近 8 周 · 单位 ¥', sumLbl: '8 周合计', avgLbl: '周均' },
month: { sub: '// 近 6 月 · 单位 ¥', sumLbl: '6 月合计', avgLbl: '月均' },
};
let _trendGrain = 'day';
const PROJECTS_BILL = [
{ name: '补水面膜 · v3', product: '透真补水面膜', owner: '李', role: 'super', stage: 'Stage 3 故事板', stagePct: 60, status: 'wip', statusLabel: '进行中', amount: 48.20 },
{ name: '透真防晒 · 通勤对比', product: '透真防晒', owner: '李', role: 'super', stage: 'Stage 5 导出', stagePct: 100, status: 'ok', statusLabel: '已完成', amount: 32.60 },
@ -484,26 +589,61 @@ function amtCls(n) { return n > 0 ? 'pos' : (n === 0 ? 'zero' : 'neg'); }
/* ─── 趋势柱 ─── */
function renderTrend() {
const data = TREND_DATA[_trendGrain];
const meta = TREND_LABEL[_trendGrain];
const bars = document.getElementById('trend-bars');
const xax = document.getElementById('trend-xaxis');
const max = Math.max(...TREND_DAYS.map(d => d.v));
bars.innerHTML = TREND_DAYS.map(d => {
const max = Math.max(...data.map(d => d.v));
// bars 的 grid-template-columns 默认是 repeat(14, 1fr),需要按数据长度动态调整
bars.style.gridTemplateColumns = `repeat(${data.length}, 1fr)`;
xax.style.gridTemplateColumns = `repeat(${data.length}, 1fr)`;
bars.innerHTML = data.map(d => {
const h = max > 0 ? (d.v / max * 100) : 0;
const isPeak = d.v === max;
return `<div class="bar${isPeak ? ' peak' : ''}" title="${d.d} · ${fmtMoney(d.v)}"><span style="height: ${h.toFixed(1)}%"></span></div>`;
}).join('');
xax.innerHTML = TREND_DAYS.map((d, i) => i % 2 === 0 ? `<span>${d.d.slice(3)}</span>` : '<span></span>').join('');
const sum = TREND_DAYS.reduce((s, d) => s + d.v, 0);
// x 轴标签:日 = 隔列显示 MM·DD 的 DD 部分;周/月 = 全显示
if (_trendGrain === 'day') {
xax.innerHTML = data.map((d, i) => i % 2 === 0 ? `<span>${d.d.slice(3)}</span>` : '<span></span>').join('');
} else if (_trendGrain === 'week') {
xax.innerHTML = data.map(d => `<span>${d.d}</span>`).join('');
} else {
xax.innerHTML = data.map(d => `<span>${d.d.slice(5)}</span>`).join('');
}
const sum = data.reduce((s, d) => s + d.v, 0);
document.getElementById('trend-sub').textContent = meta.sub;
document.getElementById('trend-sum').textContent = fmtMoney(sum);
document.getElementById('trend-avg').textContent = fmtMoney(sum / TREND_DAYS.length);
document.getElementById('trend-avg').textContent = fmtMoney(sum / data.length);
document.getElementById('trend-peak').textContent = fmtMoney(max);
// 旁标 label
document.querySelectorAll('.trend-foot .item').forEach((it, idx) => {
const k = it.querySelector('.k');
if (idx === 0 && k) k.textContent = meta.sumLbl;
else if (idx === 1 && k) k.textContent = meta.avgLbl;
});
}
/* ─── 按项目 表格 ─── */
/* ─── 趋势 日/周/月 切换 ─── */
document.querySelectorAll('.trend-head .chip[data-grain]').forEach(chip => {
chip.addEventListener('click', () => {
document.querySelectorAll('.trend-head .chip[data-grain]').forEach(c => c.classList.remove('active'));
chip.classList.add('active');
_trendGrain = chip.dataset.grain;
renderTrend();
});
});
/* ─── 按项目 表格 + 筛选 ─── */
const PROJ_FILTER = { status: 'all', range: 'month' };
const RANGE_MULT = { 'month': 1, '30d': 1, '90d': 2.6 }; // 演示用,30/90 天放大倍率(月 = 当月真实)
const RANGE_LBL = { 'month': '当月', '30d': '近 30 天', '90d': '近 90 天' };
function renderProjects() {
const tb = document.getElementById('proj-body');
tb.innerHTML = PROJECTS_BILL.map(p => {
const mult = RANGE_MULT[PROJ_FILTER.range] || 1;
const list = PROJECTS_BILL.filter(p => PROJ_FILTER.status === 'all' || p.status === PROJ_FILTER.status);
tb.innerHTML = list.map(p => {
const r = ROLE_META[p.role];
const amt = p.amount * mult;
return `
<tr>
<td><a href="pipeline.html?product=${encodeURIComponent(p.product)}" style="color:var(--accent-black);text-decoration:none;font-weight:500;">${p.name}</a></td>
@ -511,25 +651,49 @@ function renderProjects() {
<td><span class="who"><span class="av">${p.owner}</span><span class="role-pill ${r.cls}"><span class="dot"></span>${r.label}</span></span></td>
<td><span class="muted">${p.stage}</span><span class="progress-mini"><span style="width:${p.stagePct}%"></span></span></td>
<td><span class="status-tag ${p.status}">${p.statusLabel}</span></td>
<td class="${amtCls(-p.amount)}">${p.amount === 0 ? '¥0.00' : '-' + fmtMoney(p.amount)}</td>
<td class="${amtCls(-amt)}">${amt === 0 ? '¥0.00' : '-' + fmtMoney(amt)}</td>
</tr>
`;
}).join('');
const sum = list.reduce((s, p) => s + p.amount * mult, 0);
document.getElementById('proj-count').innerHTML =
`共 <b style="color:var(--accent-black);">${list.length}</b> 个项目 · ${RANGE_LBL[PROJ_FILTER.range]}消耗 ${fmtMoney(sum)}`;
const empty = document.getElementById('proj-empty');
const tbl = document.querySelector('#panel-by-project .billing-table');
empty.style.display = list.length === 0 ? '' : 'none';
tbl.style.display = list.length === 0 ? 'none' : '';
const isDef = PROJ_FILTER.status === 'all' && PROJ_FILTER.range === 'month';
document.getElementById('proj-f-reset').style.display = isDef ? 'none' : '';
}
/* ─── 按成员 表格 ─── */
['status', 'range'].forEach(key => {
const sel = document.getElementById('proj-f-' + key);
sel.addEventListener('change', () => { PROJ_FILTER[key] = sel.value; renderProjects(); });
});
document.getElementById('proj-f-reset').addEventListener('click', () => {
PROJ_FILTER.status = 'all'; PROJ_FILTER.range = 'month';
document.getElementById('proj-f-status').value = 'all';
document.getElementById('proj-f-range').value = 'month';
renderProjects();
});
/* ─── 按成员 表格 + 筛选 ─── */
const MEM_FILTER = { role: 'all', range: 'month' };
function renderMembers() {
const tb = document.getElementById('member-body');
tb.innerHTML = MEMBERS_BILL.map(m => {
const mult = RANGE_MULT[MEM_FILTER.range] || 1;
const list = MEMBERS_BILL.filter(m => MEM_FILTER.role === 'all' || m.role === MEM_FILTER.role);
tb.innerHTML = list.map(m => {
const r = ROLE_META[m.role];
const pct = m.monthly > 0 ? (m.used / m.monthly * 100) : 0;
const used = m.used * mult;
const pct = m.monthly > 0 ? (used / m.monthly * 100) : 0;
return `
<tr style="cursor:pointer;" onclick="location.href='team.html'">
<td><span class="who"><span class="av">${m.av}</span><strong style="font-weight:500;">${m.name}</strong></span></td>
<td><span class="role-pill ${r.cls}"><span class="dot"></span>${r.label}</span></td>
<td>${m.projectsDone}</td>
<td>
<strong style="font-variant-numeric:tabular-nums;font-weight:600;">${fmtMoney(m.used)}</strong>
<strong style="font-variant-numeric:tabular-nums;font-weight:600;">${fmtMoney(used)}</strong>
<span class="muted"> / ${fmtMoney(m.monthly)} · ${pct.toFixed(1)}%</span>
<span class="progress-mini"><span style="width:${Math.min(100, pct)}%; background:${pct >= 85 ? '#B45309' : 'var(--heat)'};"></span></span>
</td>
@ -537,12 +701,62 @@ function renderMembers() {
</tr>
`;
}).join('');
const sum = list.reduce((s, m) => s + m.used * mult, 0);
document.getElementById('mem-count').innerHTML =
`共 <b style="color:var(--accent-black);">${list.length}</b> 人 · ${RANGE_LBL[MEM_FILTER.range]}合计 ${fmtMoney(sum)}`;
const empty = document.getElementById('mem-empty');
const tbl = document.querySelector('#panel-by-member .billing-table');
empty.style.display = list.length === 0 ? '' : 'none';
tbl.style.display = list.length === 0 ? 'none' : '';
const isDef = MEM_FILTER.role === 'all' && MEM_FILTER.range === 'month';
document.getElementById('mem-f-reset').style.display = isDef ? 'none' : '';
}
['role', 'range'].forEach(key => {
const sel = document.getElementById('mem-f-' + key);
sel.addEventListener('change', () => { MEM_FILTER[key] = sel.value; renderMembers(); });
});
document.getElementById('mem-f-reset').addEventListener('click', () => {
MEM_FILTER.role = 'all'; MEM_FILTER.range = 'month';
document.getElementById('mem-f-role').value = 'all';
document.getElementById('mem-f-range').value = 'month';
renderMembers();
});
/* ─── 账单流水:筛选 + 表格渲染 ─── */
const BILLS_FILTER = { stage: 'all', member: 'all', range: '30d' };
// demo "今天" = 05.21,所有 ts 都是 MM.DD 格式,这里基于这一假定算时间区间
const TODAY_MD = '05.21';
function mdToDay(md) {
// 把 "MM.DD" 当成 2026 年的日子换算成 1970 epoch ms,只用来比较先后
const [m, d] = md.split('.').map(Number);
return Date.UTC(2026, (m || 1) - 1, d || 1);
}
const TODAY_MS = mdToDay(TODAY_MD);
function passRange(ts, range) {
if (range === 'all') return true;
const md = ts.slice(0, 5); // "05.21"
if (range === 'month') return md.startsWith(TODAY_MD.slice(0, 3));
const diff = TODAY_MS - mdToDay(md);
if (range === '7d') return diff <= 6 * 86400000;
if (range === '30d') return diff <= 29 * 86400000;
return true;
}
function getFilteredBills() {
return BILLS.filter(b => {
if (BILLS_FILTER.stage !== 'all' && b.type !== BILLS_FILTER.stage) return false;
if (BILLS_FILTER.member !== 'all' && b.who !== BILLS_FILTER.member) return false;
if (!passRange(b.ts, BILLS_FILTER.range)) return false;
return true;
});
}
/* ─── 账单流水 表格 ─── */
function renderBills() {
const tb = document.getElementById('bills-body');
tb.innerHTML = BILLS.map(b => {
const list = getFilteredBills();
tb.innerHTML = list.map(b => {
const r = ROLE_META[b.role];
return `
<tr>
@ -555,8 +769,37 @@ function renderBills() {
</tr>
`;
}).join('');
document.getElementById('bills-count').textContent = list.length;
const empty = document.getElementById('bills-empty');
const table = document.querySelector('#panel-bills .billing-table');
if (list.length === 0) {
empty.style.display = '';
table.style.display = 'none';
} else {
empty.style.display = 'none';
table.style.display = '';
}
// 「清除筛选」按钮:任何一个非默认就显示
const isDefault = BILLS_FILTER.stage === 'all' && BILLS_FILTER.member === 'all' && BILLS_FILTER.range === '30d';
document.getElementById('bills-f-reset').style.display = isDefault ? 'none' : '';
}
/* ─── 筛选绑定 ─── */
['stage', 'member', 'range'].forEach(key => {
const sel = document.getElementById('bills-f-' + key);
sel.addEventListener('change', () => {
BILLS_FILTER[key] = sel.value;
renderBills();
});
});
document.getElementById('bills-f-reset').addEventListener('click', () => {
BILLS_FILTER.stage = 'all'; BILLS_FILTER.member = 'all'; BILLS_FILTER.range = '30d';
document.getElementById('bills-f-stage').value = 'all';
document.getElementById('bills-f-member').value = 'all';
document.getElementById('bills-f-range').value = '30d';
renderBills();
});
/* ─── Tab 切换 ─── */
document.querySelectorAll('.billing-tabs .tab').forEach(tab => {
tab.addEventListener('click', () => {
@ -602,6 +845,29 @@ function topupDone() {
Shell.toast('充值成功', '余额已更新 · 可开发票');
}
/* ─── 导出菜单 ─── */
(function bindExport() {
const trigger = document.getElementById('export-trigger');
const menu = document.getElementById('export-menu');
if (!trigger || !menu) return;
const FMT_LABEL = { csv: 'CSV', xlsx: 'XLSX', pdf: 'PDF' };
trigger.addEventListener('click', e => {
e.stopPropagation();
menu.hidden = !menu.hidden;
});
menu.querySelectorAll('button[data-fmt]').forEach(b => {
b.addEventListener('click', e => {
e.stopPropagation();
const fmt = b.dataset.fmt;
menu.hidden = true;
Shell.toast('正在生成 ' + FMT_LABEL[fmt], '完成后会发送到注册邮箱');
});
});
document.addEventListener('click', () => { if (!menu.hidden) menu.hidden = true; });
// Esc 关闭
document.addEventListener('keydown', e => { if (e.key === 'Escape' && !menu.hidden) menu.hidden = true; });
})();
/* ─── 初始化 ─── */
renderTrend();
renderProjects();

View File

@ -8,7 +8,7 @@
<style>
#page-content { padding: 24px 28px 60px; }
/* ─── 三 Hero 卡片网格(模特上身图 / 平台套图 / 图片优化 · 等比)─── */
/* ─── 三 Hero 卡片网格(模特上身图 / 平台套图 / 图片创作 · 等比)─── */
.factory-hero {
display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16px; margin-bottom: 56px;
@ -118,9 +118,6 @@
.model-visual .stack .placeholder { aspect-ratio: 3 / 4; }
.kit-visual { grid-template-columns: repeat(4, 1fr); }
.kit-visual .placeholder { aspect-ratio: 1 / 1; }
.tri-visual { display: block; }
.tri-visual .placeholder { aspect-ratio: 16 / 9; }
.tri-visual .placeholder + .placeholder { display: none; }
/* ─── 任务中心 · section header ─── */
.section-h { display: flex; align-items: center; gap: 12px; margin-top: 24px; margin-bottom: 14px; }
@ -365,14 +362,6 @@
</li>
</ul>
<div class="platform-row">
<div class="platform-chip"><span class="code">DY</span><span class="nm">抖音</span></div>
<div class="platform-chip"><span class="code">TB</span><span class="nm">淘宝</span></div>
<div class="platform-chip"><span class="code">XHS</span><span class="nm">小红书</span></div>
<div class="platform-chip"><span class="code">PDD</span><span class="nm">拼多多</span></div>
<div class="platform-chip"><span class="code">AMZ</span><span class="nm">亚马逊</span></div>
</div>
<div class="factory-cta">
<a class="btn btn-primary btn-lg" href="platform-cover.html">
开始生成
@ -391,16 +380,16 @@
</div>
</div>
<!-- 卡片 C · 图片优化(生成人物 / 商品三视图)-->
<!-- 卡片 C · 图片创作(自由创作 AI 图片工作台)-->
<div class="factory-card with-corners">
<span class="corner-tr" aria-hidden></span>
<span class="corner-bl" aria-hidden></span>
<div class="factory-body">
<div class="factory-text">
<span class="factory-tag">[ TRI-VIEW · OPTIMIZE ]</span>
<div class="factory-title">图片优化</div>
<div class="factory-desc">为人物 / 商品生成三视图,保证多镜头一致性</div>
<span class="factory-tag">[ IMAGE · STUDIO ]</span>
<div class="factory-title">图片创作</div>
<div class="factory-desc">自由创作 AI 图片,适用于详情图 / 海报 / 灵感速写</div>
<ul class="factory-features">
<li>
@ -424,7 +413,7 @@
</ul>
<div class="factory-cta">
<a class="btn btn-primary btn-lg" href="model-photo.html?mode=tri">
<a class="btn btn-primary btn-lg" href="image-optimize.html">
开始生成
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
</a>
@ -460,6 +449,16 @@
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
<input class="input" id="tc-search" placeholder="搜索任务名">
</div>
<div class="chip-wrap" data-key="time">
<button class="chip" type="button"><span class="chip-label">时间</span> <svg class="caret" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg></button>
<div class="chip-menu">
<div class="mi selected" data-value="all"><svg class="mi-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg><span>全部时间</span></div>
<div class="mi-sep"></div>
<div class="mi" data-value="today"><svg class="mi-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg><span>今天</span></div>
<div class="mi" data-value="1h"><svg class="mi-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg><span>1 小时内</span></div>
<div class="mi" data-value="10min"><svg class="mi-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg><span>10 分钟内</span></div>
</div>
</div>
<div class="chip-wrap" data-key="type">
<button class="chip" type="button"><span class="chip-label">任务类型</span> <svg class="caret" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg></button>
<div class="chip-menu"></div>
@ -488,9 +487,8 @@
<table class="t">
<thead>
<tr>
<th style="width:32%">任务</th>
<th>类型</th>
<th style="width:140px">进度</th>
<th style="width:42%">任务</th>
<th style="width:160px">进度</th>
<th>状态</th>
<th style="width:120px">创建于</th>
<th style="width:48px"></th>
@ -547,10 +545,19 @@ Shell.render({
(function () {
'use strict';
const TYPE_LABEL = { model: '模特上身图', platform: '平台套图' };
const TYPE_LABEL = { model: '模特上身图', platform: '平台套图', image: '图片创作' };
const STATUS_LABEL = { ok: '已完成', gen: '生成中', err: '失败' };
const STATUS_PILL = { ok: 'ok', gen: 'info', err: 'err' };
const KEY_BY_TYPE = { model: 'fs-image-tasks-model', platform: 'fs-image-tasks-platform' };
const KEY_BY_TYPE = {
model: 'fs-image-tasks-model',
platform: 'fs-image-tasks-platform',
image: 'fs-image-tasks-image',
};
const URL_BY_TYPE = {
model: 'model-photo.html',
platform: 'platform-cover.html',
image: 'image-optimize.html',
};
const taskGrid = document.getElementById('task-grid');
const listTbody = document.getElementById('task-list-tbody');
@ -559,7 +566,21 @@ Shell.render({
let cards = []; // 动态生成的 .task-card 元素数组(顺序与任务时间倒序一致)
const state = { filter: 'all', type: 'all', search: '', view: 'list' };
const state = { filter: 'all', type: 'all', time: 'all', search: '', view: 'list' };
function _timeMatch(createdAt, key) {
if (key === 'all' || !createdAt) return true;
const now = Date.now();
const diff = now - Number(createdAt);
if (key === '10min') return diff <= 10 * 60 * 1000;
if (key === '1h') return diff <= 60 * 60 * 1000;
if (key === 'today') {
const a = new Date(now); const b = new Date(Number(createdAt));
return a.toDateString() === b.toDateString();
}
return true;
}
const TIME_LABEL = { all: '时间', today: '今天', '1h': '1 小时内', '10min': '10 分钟内' };
function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c])); }
@ -593,9 +614,8 @@ Shell.render({
/* ---------- 点击行 / 卡片 → 跳转工作台(携带 taskId) ---------- */
function goToWorkbench(t) {
const url = (t.type === 'model' ? 'model-photo.html' : 'platform-cover.html')
+ '?taskId=' + encodeURIComponent(t.id);
location.href = url;
const base = URL_BY_TYPE[t.type] || URL_BY_TYPE.model;
location.href = base + '?taskId=' + encodeURIComponent(t.id);
}
/* ---------- 1. 从 task 数据生成卡片 + list 行 ---------- */
@ -606,6 +626,7 @@ Shell.render({
card.dataset.type = t.type;
card.dataset.name = t.name;
card.dataset.taskId = t.id;
card.dataset.createdAt = String(t.createdAt || 0);
card.style.cursor = 'pointer';
card.innerHTML = `
<button class="card-del-btn" type="button" title="删除任务">
@ -642,7 +663,6 @@ Shell.render({
</div>
</div>
</td>
<td><span class="muted">${esc(TYPE_LABEL[t.type] || t.type)}</span></td>
<td>${t.status === 'gen'
? `<div class="task-list-prog"><div class="bar"><span style="width:60%"></span></div><span class="pct">60%</span></div>`
: (t.status === 'ok' ? '<span class="muted-2 mono" style="font-size:11px;">已完成</span>' : '<span class="muted-2 mono" style="font-size:11px;"></span>')}</td>
@ -719,6 +739,21 @@ Shell.render({
wrap.querySelectorAll('.mi').forEach(mi => mi.classList.toggle('selected', mi.dataset.value === state.type));
}
function syncTimeChip() {
const wrap = document.querySelector('.chip-wrap[data-key="time"]');
if (!wrap) return;
const label = wrap.querySelector('.chip-label');
const chip = wrap.querySelector('.chip');
if (state.time === 'all') {
label.textContent = '时间';
chip.classList.remove('active');
} else {
label.textContent = TIME_LABEL[state.time] || state.time;
chip.classList.add('active');
}
wrap.querySelectorAll('.mi').forEach(mi => mi.classList.toggle('selected', mi.dataset.value === state.time));
}
/* ---------- 3. applyFilter ---------- */
function applyFilter() {
const q = state.search.toLowerCase();
@ -726,8 +761,9 @@ Shell.render({
cards.forEach(card => {
const okStatus = state.filter === 'all' || card.dataset.status === state.filter;
const okType = state.type === 'all' || card.dataset.type === state.type;
const okTime = _timeMatch(card.dataset.createdAt, state.time);
const okSearch = !q || (card.dataset.name || '').toLowerCase().includes(q);
const show = okStatus && okType && okSearch;
const show = okStatus && okType && okTime && okSearch;
card.style.display = show ? '' : 'none';
if (card._listRow) card._listRow.style.display = show ? '' : 'none';
if (show) visible++;
@ -746,7 +782,7 @@ Shell.render({
document.getElementById('tc-sub-err').textContent = counts.err;
document.getElementById('tc-result-meta').innerHTML = `// 显示 <span class="count">${visible}</span> / ${cards.length} 个任务`;
document.getElementById('tc-clear').hidden = !(state.search || state.type !== 'all');
document.getElementById('tc-clear').hidden = !(state.search || state.type !== 'all' || state.time !== 'all');
// 空态
const emptyEl = document.getElementById('tc-empty');
@ -799,6 +835,24 @@ Shell.render({
syncTypeChip();
applyFilter();
});
// time chip
const timeWrap = document.querySelector('.chip-wrap[data-key="time"]');
const timeMenu = timeWrap.querySelector('.chip-menu');
timeWrap.querySelector('.chip').addEventListener('click', e => {
e.stopPropagation();
const isOpen = timeWrap.classList.contains('open');
document.querySelectorAll('.chip-wrap.open').forEach(w => w.classList.remove('open'));
if (!isOpen) timeWrap.classList.add('open');
});
timeMenu.addEventListener('click', e => {
const mi = e.target.closest('.mi');
if (!mi) return;
e.stopPropagation();
state.time = mi.dataset.value;
timeWrap.classList.remove('open');
syncTimeChip();
applyFilter();
});
document.addEventListener('click', () => {
document.querySelectorAll('.chip-wrap.open').forEach(w => w.classList.remove('open'));
});
@ -806,8 +860,10 @@ Shell.render({
document.getElementById('tc-clear').addEventListener('click', () => {
state.search = '';
state.type = 'all';
state.time = 'all';
document.getElementById('tc-search').value = '';
syncTypeChip();
syncTimeChip();
applyFilter();
Shell.toast('已清空筛选');
});

View File

@ -250,6 +250,58 @@
@media (max-width: 900px) {
#${DRAWER_ID} .pf-upload-row { grid-template-columns: 1fr; }
}
/* 放弃填写 · 确认弹窗 · 覆盖在 drawer 上 */
#npd-confirm-bg {
position: fixed; inset: 0; z-index: 1200;
background: rgba(21, 20, 15, .42);
display: grid; place-items: center;
padding: 16px;
}
#npd-confirm-bg[hidden] { display: none; }
.npd-confirm-modal {
width: min(420px, 92vw);
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
box-shadow: 0 16px 48px rgba(21, 20, 15, .18);
overflow: hidden;
}
.npd-confirm-modal .dh {
display: flex; align-items: center; gap: 12px;
padding: 18px 20px 14px;
}
.npd-confirm-modal .dh .ic {
width: 36px; height: 36px; flex-shrink: 0;
border-radius: var(--r-md);
background: rgba(180, 30, 30, .08); color: var(--accent-crimson);
display: grid; place-items: center;
}
.npd-confirm-modal .dh .ic svg { width: 18px; height: 18px; }
.npd-confirm-modal .dh .ti { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.npd-confirm-modal .dh .ti strong { font-size: 14.5px; color: var(--accent-black); font-weight: 600; }
.npd-confirm-modal .dh .ti .mono { font-family: var(--font-mono); font-size: 11.5px; color: var(--black-alpha-48); letter-spacing: .02em; line-height: 1.5; }
.npd-confirm-modal .df {
display: flex; gap: 8px;
padding: 0 20px 18px;
justify-content: flex-end;
}
.npd-confirm-modal .df button {
height: 32px; padding: 0 14px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
font-size: 12.5px; color: var(--accent-black);
font-family: inherit; cursor: pointer;
transition: border-color var(--t-base), background var(--t-base), color var(--t-base);
}
.npd-confirm-modal .df button:hover { border-color: var(--heat-20); background: var(--heat-12); color: var(--heat); }
.npd-confirm-modal .df button.danger { color: var(--accent-crimson); }
.npd-confirm-modal .df button.danger:hover {
background: rgba(180, 30, 30, .08);
border-color: rgba(180, 30, 30, .35);
color: var(--accent-crimson);
}
`;
const HTML = `
@ -341,11 +393,33 @@
</button>
</div>
</aside>
<!-- 放弃填写 · 确认弹窗(覆盖在 drawer 之上) -->
<div id="npd-confirm-bg" hidden>
<div class="npd-confirm-modal" role="dialog" aria-label="放弃填写">
<div class="dh">
<div class="ic">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><path d="M12 9v4M12 17h.01"/></svg>
</div>
<div class="ti">
<strong>放弃当前填写?</strong>
<span class="mono">// 已填写的商品名、卖点、图片等内容将不会保留</span>
</div>
</div>
<div class="df">
<button type="button" data-act="keep">继续填写</button>
<button type="button" class="danger" data-act="discard">放弃退出</button>
</div>
</div>
</div>
`;
/* ---------- DOM refs (populated by ensureInjected) ---------- */
let injected = false;
let bg, drawer, $f, $grid, $bullets, $blInput;
let confirmBg;
let _forceClose = false; // 保存成功后绕过脏检查
let _initialCat = ''; // 打开时类目下拉的初始值,用来判断是否被改过
let currentOpts = {};
const PF_MAX = 5;
let pfFiles = []; // { id, dataUrl, name }
@ -368,8 +442,9 @@
wrap.innerHTML = HTML;
while (wrap.firstChild) document.body.appendChild(wrap.firstChild);
bg = document.getElementById(DRAWER_BG_ID);
drawer = document.getElementById(DRAWER_ID);
bg = document.getElementById(DRAWER_BG_ID);
drawer = document.getElementById(DRAWER_ID);
confirmBg = document.getElementById('npd-confirm-bg');
$f = {
name: drawer.querySelector('[data-f="name"]'),
cat: drawer.querySelector('[data-f="cat"]'),
@ -549,6 +624,7 @@
if (drawer.classList.contains('show')) return; // 已开则不重复锁
currentOpts = opts || {};
resetForm();
_initialCat = $f.cat.value; // 快照「打开时类目」用于脏检查
bg.classList.add('show');
drawer.classList.add('show');
drawer.setAttribute('aria-hidden', 'false');
@ -556,9 +632,49 @@
setTimeout(() => $f.name.focus(), 280);
}
function close() {
/* ---------- 脏检查 + 退出确认 ---------- */
function isDirty() {
if (!injected) return false;
if (($f.name.value || '').trim()) return true;
if (($f.target.value || '').trim()) return true;
if (($blInput.value || '').trim()) return true;
if (pfFiles.length > 0) return true;
if ($bullets.querySelectorAll('.bl-item').length > 0) return true;
if ($f.cat.value !== _initialCat) return true;
return false;
}
function confirmDiscard() {
return new Promise(resolve => {
confirmBg.hidden = false;
const buttons = confirmBg.querySelectorAll('button[data-act]');
function done(choice) {
confirmBg.hidden = true;
buttons.forEach(b => b.onclick = null);
confirmBg.onclick = null;
document.removeEventListener('keydown', escHandler, true);
resolve(choice === 'discard');
}
function escHandler(e) {
if (e.key === 'Escape') {
e.stopPropagation();
done('keep');
}
}
buttons.forEach(b => b.onclick = () => done(b.dataset.act));
confirmBg.onclick = e => { if (e.target === confirmBg) done('keep'); };
// 用 capture · 截在外层 Esc 监听器之前
document.addEventListener('keydown', escHandler, true);
});
}
async function close() {
if (!injected) return;
if (!drawer.classList.contains('show')) return; // 已关则不重复解锁
if (!_forceClose && isDirty()) {
const ok = await confirmDiscard();
if (!ok) return;
}
bg.classList.remove('show');
drawer.classList.remove('show');
drawer.setAttribute('aria-hidden', 'true');
@ -566,7 +682,20 @@
if (typeof currentOpts.onClose === 'function') currentOpts.onClose();
}
function closeForce() {
_forceClose = true;
try { close(); } finally { _forceClose = false; }
}
function save() {
// 兜底:用户在卖点输入框敲了字但没回车/没点「添加卖点」就直接点创建,
// 这里自动提交一次,避免静默丢失最后一条卖点(常见误操作)
if ($blInput && ($blInput.value || '').trim()) {
blAdd($blInput.value);
$blInput.value = '';
updateBlAddBtn();
}
const name = ($f.name.value || '').trim();
const cat = $f.cat.value;
const target = ($f.target.value || '').trim();
@ -595,15 +724,14 @@
images,
imgs: images.length,
};
// 持久化到 localStorage('fs-extra-products'),让 products.html 下次加载时
// 自动从 storage 读出并 prepend 到 grid(否则用户在工作台创建后 → 跳详情 →
// 回商品库会看不到刚创建的商品)
// 持久化到 sessionStorage('fs-extra-products') · 仅当前标签页生命周期内有效
// 关闭标签页/浏览器后自动清空,不会跨会话累积演示残留
try {
const KEY = 'fs-extra-products';
const list = JSON.parse(localStorage.getItem(KEY) || '[]');
const list = JSON.parse(sessionStorage.getItem(KEY) || '[]');
list.push({
id: product.id,
name, cat,
name, cat, target,
tags: '',
assets: 0,
videos: 0,
@ -611,13 +739,19 @@
date: new Date().toISOString().slice(0, 10),
createdAt: Date.now(),
});
localStorage.setItem(KEY, JSON.stringify(list));
sessionStorage.setItem(KEY, JSON.stringify(list));
} catch (e) { /* storage 不可用降级到只跳转 */ }
// 短期 sessionStorage 缓存完整 product(含图片 dataUrl),供 detail 页 ?id=new
// 读出后渲染。读完即清除,避免污染后续操作。
try {
sessionStorage.setItem('npd-last-created', JSON.stringify(product));
} catch (e) { /* dataUrl 可能过大 → 退化为不带图,detail 仍能渲染文本字段 */ }
if (typeof currentOpts.onSave === 'function') {
toast('商品已创建', '+ ' + name);
currentOpts.onSave(product);
close();
closeForce();
return;
}
// 默认行为: 不 close drawer · 跳转期间 drawer 仍覆盖 host 页面 → 视觉上彻底

View File

@ -431,6 +431,15 @@ main { position: relative; background: var(--background-base); min-width: 0; }
}
.page-head .sub .mono { font-family: var(--font-mono); font-size: 11.5px; color: var(--black-alpha-48); letter-spacing: .04em; }
.page-head .actions { display: flex; gap: 10px; align-items: center; }
/* page-head 右上角主操作按钮 · 统一尺寸(对齐 .btn-lg),覆盖各页里 btn / btn-sm / btn-lg 混用 */
.page-head .actions > .btn,
.page-head .actions > a.btn,
.page-head .actions > button.btn {
height: 40px;
padding: 0 20px;
font-size: 13.5px;
}
.page-head .actions > .btn svg { width: 14px; height: 14px; }
.section-h { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 14px; }
.section-h h2 { font-size: 16px; font-weight: 600; letter-spacing: -.01em; color: var(--accent-black); }
@ -1415,11 +1424,22 @@ table.t tbody tr:hover { background: var(--black-alpha-4); }
background: var(--background-lighter);
}
/* ─── Scrollbar ─── */
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--black-alpha-12); border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: var(--black-alpha-24); }
/* Scrollbar · 全局隐藏可视滚动条,保留滚动能力
!important 覆盖各页 inline <style> 里残留的 scrollbar-width:thin / 自定义 webkit 滚动条 */
* { scrollbar-width: none !important; -ms-overflow-style: none !important; }
*::-webkit-scrollbar {
width: 0 !important;
height: 0 !important;
display: none !important;
-webkit-appearance: none !important;
}
*::-webkit-scrollbar-track,
*::-webkit-scrollbar-thumb,
*::-webkit-scrollbar-thumb:hover,
*::-webkit-scrollbar-corner {
background: transparent !important;
display: none !important;
}
/* ─── Responsive ─── */
@media (max-width: 1100px) {
@ -1456,10 +1476,29 @@ table.t tbody tr:hover { background: var(--black-alpha-4); }
letter-spacing: .03em;
border-radius: var(--r-sm);
box-shadow: 0 1px 2px rgba(0,0,0,.12);
pointer-events: none;
cursor: help;
user-select: none;
transition: background var(--t-base), box-shadow var(--t-base), transform var(--t-base);
}
.tri-missing-badge::before {
.tri-missing-badge[hidden] { display: none; }
.tri-missing-badge:hover {
box-shadow: 0 3px 8px rgba(0,0,0,.18);
transform: translateY(-1px);
}
.tri-missing-badge .ico {
width: 12px; height: 12px;
border: 1px solid currentColor;
border-radius: 50%;
display: grid; place-items: center;
font-size: 9px; font-weight: 700;
line-height: 1;
font-family: var(--font-mono);
}
.tri-missing-badge .ico::before { content: '!'; }
.tri-missing-badge .lbl-mono { font-family: var(--font-mono); letter-spacing: .04em; }
/* 兼容旧 markup(没有 .ico 子元素的也保留 ! 圆点) */
.tri-missing-badge:not(:has(.ico))::before {
content: '!';
width: 12px; height: 12px;
border: 1px solid currentColor;
@ -1468,4 +1507,85 @@ table.t tbody tr:hover { background: var(--black-alpha-4); }
font-size: 9px; font-weight: 700;
line-height: 1;
}
.tri-missing-badge .lbl-mono { font-family: var(--font-mono); letter-spacing: .04em; }
/* hover popover · V2.1 dark tooltip with arrow */
.tri-missing-pop {
position: absolute;
top: calc(100% + 9px);
left: 0;
width: 252px;
padding: 11px 13px 12px;
background: var(--accent-black);
color: var(--accent-white);
border-radius: var(--r-md);
box-shadow: 0 8px 24px rgba(0,0,0,.20), 0 2px 6px rgba(0,0,0,.10);
opacity: 0; visibility: hidden;
transform: translateY(-4px);
transition: opacity .15s ease, visibility .15s ease, transform .15s ease;
pointer-events: none;
z-index: 30;
font-family: var(--font-sans);
font-weight: 400;
letter-spacing: 0;
text-transform: none;
text-align: left;
cursor: default;
}
.tri-missing-pop::before {
content: '';
position: absolute;
top: -5px; left: 14px;
width: 10px; height: 10px;
background: var(--accent-black);
transform: rotate(45deg);
border-radius: 2px;
}
.tri-missing-pop .pop-h {
display: flex; align-items: center; gap: 6px;
font-family: var(--font-mono);
font-size: 11px;
letter-spacing: .04em;
color: var(--heat);
margin-bottom: 6px;
}
.tri-missing-pop .pop-h svg { display: block; }
.tri-missing-pop .pop-body {
font-size: 12px;
line-height: 1.6;
color: rgba(255,255,255,.88);
}
.tri-missing-pop .pop-tip {
display: block;
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid rgba(255,255,255,.12);
font-family: var(--font-mono);
font-size: 11px;
line-height: 1.55;
letter-spacing: .02em;
color: rgba(255,255,255,.78);
}
.tri-missing-pop .pop-tip b {
color: var(--accent-white);
font-weight: 600;
letter-spacing: .03em;
}
.tri-missing-badge:hover .tri-missing-pop,
.tri-missing-badge:focus-within .tri-missing-pop {
opacity: 1; visibility: visible; transform: translateY(0);
transition-delay: .12s;
}
/* 让 popover 能溢出 thumb / 卡片 容器(否则被 overflow:hidden 裁切) */
.placeholder:has(.tri-missing-badge:hover),
.placeholder:has(.tri-missing-badge:focus-within) {
overflow: visible;
z-index: 4;
}
.product-card:has(.tri-missing-badge:hover),
.product-card:has(.tri-missing-badge:focus-within),
.asset-card:has(.tri-missing-badge:hover),
.asset-card:has(.tri-missing-badge:focus-within) {
overflow: visible;
z-index: 5;
}

View File

@ -21,7 +21,7 @@ const NAV = [
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="4" width="18" height="16"/><path d="M7 4v16M16 4v16M3 9h18M3 15h18"/></svg>'
},
{
id: 'asset-factory', label: '图片生成', href: 'asset-factory.html', badge: '12',
id: 'asset-factory', label: '图片生成', href: 'asset-factory.html',
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l1.7 4.6L18 9l-4.3 1.4L12 15l-1.7-4.6L6 9l4.3-1.4L12 3z"/><path d="M19 16l.85 2.3L22 19l-2.15.7L19 22l-.85-2.3L16 19l2.15-.7L19 16z"/></svg>'
},
{

File diff suppressed because it is too large Load Diff

712
电商AI平台/design.md Normal file
View File

@ -0,0 +1,712 @@
# 流·Studio 设计规范 · design.md
> **唯一权威 source of truth** · 所有页面调整必须遵循本文件。
> **代号:** Restraint(克制)· V2.1 · Firecrawl-aligned
> **维护日期:** 2026-05-22
> **配套实现:** [assets/restraint.css](assets/restraint.css) (token + 100+ 组件类 · 1592 行)
> **可视样板间(归档):** [_archive/design-system.html](_archive/design-system.html)
> **历史规范(归档):** [_archive/DESIGN_SPEC_V2.md](_archive/DESIGN_SPEC_V2.md)
---
## §0 · AI 协作铁律(每次启动必读)
**Claude / 任何 AI 在做页面或 CSS 调整前,必须执行以下流程:**
1. **先 Read 本文件** · 至少读 §1 设计哲学 + §3 组件清单 + §8 Don't List
2. **检查 restraint.css 是否已有该组件** · 用 `Grep "\.btn|\.pill|\.input" assets/restraint.css` 查现成类名
3. **禁止在页面 inline `<style>` 重写 restraint.css 已有的共享类**(`.btn` `.pill` `.input` `.modal` `.drawer` `.toast` `.field` `.tabs` `.chip` 等)— 要变体先回 restraint.css 加
4. **禁止创建新色值** · 颜色必须用 §2.1 的 token,不能写裸 hex
5. **禁止动 token 数值** · 不要改 `--heat` `--background-base` `--border-faint` 等基础变量,改了破坏全站
6. **完成后对照 §8 Don't List 自检**
7. **不确定就问用户**,而不是凭感觉发挥
**违反任何一条,用户有权要求重做。**
---
## §1 · 设计哲学
**一句话:** 一台精密设备的工作面板。
**三条铁律:**
1. **克制大于装饰** · 留白 > 容器 > 内容,大量空气感
2. **单色锚点** · 全场只有一个 accent(橙),只用于 CTA / 关键状态 / 强调单词
3. **结构清晰可见** · 用 1 px 边框 + 8 px 圆角 + 准星 + 装订线 + mono 标签暴露"图纸感",而非阴影/渐变隐藏结构
**避免的 "AI 味":** 渐变铺面 / 玻璃拟态 / 彩色阴影 / emoji 图标 / 圆角无差别 / 卡片"贴纸感" / 装饰盖过内容。
**核心签名(4 个不能丢):**
- 主橙 `#fa5d19`(Firecrawl 实测)+ 单 hue alpha 阶梯
- 全场 8 px 圆角(Pill 999 例外)
- 主容器左右装订线 + 四角准星
- Mono 装饰 `[ 200 OK ]` `// 05.14` `[ /v2 ]`(品牌签名 · Firecrawl 没有)
---
## §2 · 全局 Token
### §2.1 色彩 · 你 90% 时间用这 14 个
| Token | Hex / Value | 用途 |
| ----- | ----------- | ---- |
| `--background-base` | `#f9f9f9` | 页面底色 · 冷灰无色相 |
| `--background-lighter` | `#fbfbfb` | 容器底 / hover 浅底(部分场景) |
| `--surface` | `#ffffff` | 卡片 / 主容器表面 |
| `--surface-raised` | `#ffffff` | Modal / 浮层 |
| `--border-faint` | `#ededed` | **默认 1 px 边框 ★ 80% 场景** |
| `--border-muted` | `#e8e8e8` | 略深(主分隔线) |
| `--border-loud` | `#e6e6e6` | 最深(强分隔) |
| `--accent-black` | `#262626` | **主前景色 · 文字** |
| `--heat` | `#fa5d19` | **主橙 100% · CTA / 链接 ★** |
| `--heat-12` | `rgba(250,93,25,.12)` | tint 底 · active nav / icon-box |
| `--heat-20` | `rgba(250,93,25,.20)` | pill 边框 / selection 底色 |
| `--heat-40` | `rgba(250,93,25,.40)` | focus ring / disabled CTA |
| `--accent-forest` | `#42c366` | 绿 · success / 已完成 |
| `--accent-crimson` | `#eb3424` | 红 · error / 失败 |
| `--accent-honey` | `#ecb730` | 黄 · warning |
### §2.2 Black-Alpha 阶梯(20 档 · 核心工具尺)
> **规则:** 024% 用 `rgba(0,0,0,...)`;32% 起换 `rgba(38,38,38,...)`(避免叠出"灰中带蓝")。
最常用 8 档:
| Token | 用途 |
| ----- | ---- |
| `--black-alpha-4` | **hover 底色 ★** |
| `--black-alpha-7` | **active 底色 ★** |
| `--black-alpha-12` | **inside-border / disabled fg ★** |
| `--black-alpha-24` | input hover 边框 |
| `--black-alpha-48` | **占位字色 ★** |
| `--black-alpha-56` | **次级文字 / 未选中 Tab ★** |
| `--black-alpha-64` | 描述文字 |
| `--black-alpha-88` | 近主前景 |
完整 20 档清单见 [assets/restraint.css](assets/restraint.css) §root。
### §2.3 状态色配套(底 / 边)
| 含义 | 主色 | 配套底色 8% | 配套边框 20% |
| ---- | ---- | ----------- | ------------ |
| 成功 | `--accent-forest` `#42c366` | `--forest-bg` | `--forest-bd` |
| 失败 | `--accent-crimson` `#eb3424` | `--crimson-bg` | `--crimson-bd` |
| 信息 | `--heat` `#fa5d19` | `--heat-12` | `--heat-20` |
| 警告 | `--accent-honey` `#ecb730` | `--honey-bg` | `--honey-bd` |
### §2.4 Selection · Firecrawl 签名细节
```css
::selection { background: var(--heat-20); color: var(--heat); }
```
任何文字选中时,底色 20% 橙 + 文字 100% 橙。已全局生效 · 不要覆盖。
### §2.5 字体族 · 中英协作
| 用途 | 字体声明 | 加载方式 |
| ---- | -------- | -------- |
| 正文 / UI · `--font-sans` | `'Inter', 'Alibaba PuHuiTi', 'PingFang SC', 'Microsoft YaHei', system-ui, sans-serif` | Inter Google Fonts + 普惠体 CDN |
| 强制纯英 · `--font-inter` | `'Inter', system-ui, sans-serif` | 用于 Ctrl K 等英文徽标 |
| Mono 装饰 · `--font-mono` | `'JetBrains Mono', 'Geist Mono', ui-monospace, 'Alibaba PuHuiTi', 'PingFang SC', 'Microsoft YaHei', monospace` | JetBrains Google Fonts |
**字符级 fallthrough 原理:** 浏览器对每个字符逐个查找,Inter 不含 CJK → 中文自动跳到普惠体。**不需要 JS,不会字重错位。**
**字重档位仅 3 档:** `400 / 500 / 600`。700 仅用于 `Ctrl K` 这种纯英文徽标。
### §2.6 字号 / 字重 / 行高(11 档)
| 角色 | 字号 | 字重 | 字距 | 行高 | 用途 |
| ---- | ---- | ---- | ---- | ---- | ---- |
| H1 / Hero | 36 px | 500 | -0.024em | 1.2 | 页面主标题 |
| 区块 H2 | 28 px | 500 | -0.02em | 1.25 | section-head |
| KPI 数值 | 32 px | 500 | -0.02em | 1.1 | 统计大数(`tabular-nums`) |
| 子区 H3 | 16 px | 500 | -0.01em | 1.4 | subsection |
| 卡片标题 | 14 px | 500 | normal | 1.4 | 项目名 / 商品名 |
| 正文 body | 14 px | 400 | normal | **1.65** | 默认正文 |
| 描述次级 | 13 px | 400 | normal | 1.8 | 子区说明 |
| Label / Tab | 13 px | 500 | normal | 1.4 | 按钮文字 / Tab |
| Pill | 11.5 px | 500 | normal | 1.3 | 状态徽标 |
| Inter Bold 徽标 | 11.5 px | 700 | 0.02em | 1 | Ctrl K / ESC(`--font-inter`) |
| Mono 标签 | 11 px | 400 | 0.04em | 1.5 | `[ STATUS ]` `// 注释` |
| Mono 散点 | 8.5 px | 400 | 0.04em | 1 | 背景 ASCII 装饰 |
**数值类必加** `font-variant-numeric: tabular-nums`(或加 `.num` 工具类)。
**正文中文行高 1.651.8**(给中文留呼吸)。
### §2.7 圆角
| 元素 | 值 | Token |
| ---- | -- | ----- |
| 所有结构性容器 / 按钮 / 输入框 / 缩略图 | **8 px** | `--r-md` |
| 头像 / 小色块 | 6 px | — |
| Mono 标签 / kbd / badge | 4 px | `--r-sm` |
| 进度条段位 | 2 px | — |
| Pill / dot | 999 px | `--r-pill` |
**默认是 8 px。** 不确定就选 8。`>12 px` 直接判错。
### §2.8 间距(全站统一)
**基础栅格:** 4 px。
**阶梯:** `4 / 8 / 12 / 16 / 20 / 24 / 28 / 32 / 40 / 48 / 64 / 72 / 80 / 104`
| 场景 | 值 |
| ---- | -- |
| Sidebar 宽度 | `248 px` |
| Topbar 高度 | `64 px` |
| Content padding | `48 28 72` |
| 卡片内 padding(大) | `28 30` / `24 28` |
| 卡片内 padding(列表行) | `14 18` ~ `20 24` |
| KPI cell padding | `24 28` |
| 主区块间(section margin) | `3648 px`(子)/ `64104 px`(主) |
| 子区标题底距 | `mb 14`(h2)/ `mb 22-28`(h1) |
| 卡片网格 gap | `14`(子区)/ `24`(主区) |
| Modal padding | `head 24 28 / body 24 28 / foot 18 28` |
**最重要的一条:** 别再吝啬空气。**当不确定 padding 是 16 还是 24 时,选 24。** 当不确定 margin 是 48 还是 64 时,选 64。
### §2.9 阴影(只 3 个允许场景)
| 场景 | 阴影 |
| ---- | ---- |
| 主 CTA(`.btn-primary`) | 4 层橙色阴影 `--shadow-cta` |
| 主 CTA hover | 阴影抬升 `--shadow-cta-hover` |
| Toast / Dropdown 浮层 | 白色 `0 4px 20px rgba(21,20,15,.06)` = `--shadow-floating` |
**禁:** 灰色阴影 / 文字阴影 / 通用 box-shadow。
### §2.10 边框策略 · inside-border
默认边框用 `::before` 伪元素绘制(而非真 `border`),hover 时让 `::before` 透明度 → 0,不触发布局抖动。
```css
.inside-border { position: relative; }
.inside-border::before {
content: ''; position: absolute; inset: 0;
border: 1px solid var(--black-alpha-12);
border-radius: inherit;
pointer-events: none;
transition: opacity .2s, border-color .2s;
}
.inside-border:hover::before { opacity: 0; }
```
**禁:** 2 px / 3 px 实线 / 真 `border` + hover 边框消失。
**例外:** `.tip` 提示框允许 `1 px dashed`
---
## §3 · 页面骨架(全站统一)
```
┌──────────────────────────────────────────────┐
<div class="app">
│ ┌────────┬──────────────────────────────┐ │
│ │ │ <div class="topbar"> │ │
│ │ aside │ crumbs · right(chips) │ │
│ │ side │ </div> │ │
│ │ bar │ ┌────────────────────────┐ │ │
│ │ 248px │ │ <div class="content"> │ │ │
│ │ │ │ <div class="page-head"> │ │
│ │ │ │ <div class="section-h"> │ │
│ │ │ │ ... │ │ │
│ │ │ │ </div> │ │ │
│ │ │ └────────────────────────┘ │ │
│ └────────┴──────────────────────────────┘ │
</div>
└──────────────────────────────────────────────┘
```
### §3.1 入口约束
每个新页面 `<head>`:
```html
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<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">
```
每个新页面 `<body>` 末尾:
```html
<script src="assets/shell.js" defer></script>
```
`shell.js` 自动注入 sidebar + topbar,**页面只写 `<main>` 内的 `.content` 内容**。
### §3.2 标题区(必有)
```html
<div class="page-head">
<div>
<h1>商品库</h1>
<div class="sub">
<span class="mono">// 12 个商品</span> · 你的所有商品和卖点
</div>
</div>
<div class="actions">
<button class="btn">导入</button>
<button class="btn btn-primary">+ 新建商品</button>
</div>
</div>
```
**page-head 内主操作按钮强制 40 px 高**(`.page-head .actions > .btn { height: 40px }` 已全局覆盖)。
### §3.3 子区块(可选)
```html
<div class="section-h">
<h2>最近项目</h2>
<a class="more">[ ALL · 12 ] →</a>
</div>
<!-- 内容 -->
```
`.more` mono 11.5 / `--black-alpha-48` / hover 转橙。
### §3.4 主容器装订线(签名元素 · 主工作台型必有)
工作台 / 项目列表 / 商品库 / 流水线主区:左右两条 1 px 边线 + 4 角圆弧内凹 SVG 准星。Modal / 编辑器全屏画布不必加。
```css
.workbench-container {
max-width: 1480px;
margin: 0 auto;
border-left: 1px solid var(--border-faint);
border-right: 1px solid var(--border-faint);
position: relative;
}
/* 4 角 SVG · 用 .with-corners + .corner-tr/.corner-bl */
```
---
## §4 · 组件清单(restraint.css 已实现 · 不要重发明)
### §4.1 按钮族 · 三级体系(按功能重要性区分)
> **核心原则:** 按钮的视觉强度 = 操作的重要程度。一个区域**只能有一个**一级按钮(像航海图上只能有一个北极星)。视觉等级混乱比按钮不好看更致命。
#### 三级体系
| 等级 | 类名 | 视觉 | 语义 | 数量约束 |
| ---- | ---- | ---- | ---- | -------- |
| **一级 · Primary** | `.btn .btn-primary` | 橙底 + 4 层橙阴影 + 字重 600 | **页面/弹窗的最重要动作** · 用户主动作 | **一个区域只能有一个** ★ |
| **二级 · Secondary** | `.btn` 默认 | 白底 + inside-border + 主前景字 | 跟主操作**并列**的重要动作 | 可以有多个 |
| **三级 · Tertiary** | `.btn .btn-ghost` | 透明底 + 无框 + alpha-56 字 | **辅助/链接式**的弱化操作 | 不限 |
#### 选谁?决策表
| 操作类型 | 等级 | 例子 |
| -------- | ---- | ---- |
| 提交 / 确认 / 创建 / 下一步 / 保存 / 生成 | **一级** | "创建商品" "确认删除" "开始生成" |
| 取消 / 重置 / 导出 / 导入 / 上一步 / 关闭 | **二级** | "取消" "重置" "导出 CSV" |
| 跳过 / 了解更多 / 查看详情 / 折叠 / 链接式 | **三级** | "跳过" "了解更多 →" "[查看] [编辑] [删除]" 行内动作 |
| 破坏性(删除/危险) | **二级**(在 modal 里)或**一级**(确认按钮) | modal 里:取消[二级] + 确认删除[一级] |
#### 典型组合
| 场景 | 组合 |
| ---- | ---- |
| `.page-head .actions`(页面标题区) | [二级] + [一级] · 例:[导入] [+ 新建商品] |
| `.modal-f`(弹窗底栏) | [二级取消] + [一级确认] · 永远主操作在右 |
| 卡片行内操作 | [三级] × N · 例:[查看] [编辑] [删除] |
| 表单底部 | [三级上一步] + spacer + [二级保存草稿] + [一级提交] |
| 空状态 CTA | [一级] · 唯一一个 |
#### 尺寸阶梯(独立于等级,按上下文密度选)
| 类名 | 高度 | padding | 字号 | 用法 |
| ---- | ---- | ------- | ---- | ---- |
| `.btn-sm` | 28 px | `0 12` | 12 | 行内 / 列表行末 / 表格内 / 紧凑工具栏 |
| 默认 | 36 px | `0 16` | 13 | **标准 ★** · 通用场景 |
| `.btn-lg` | 40 px | `0 20` | 13.5 | hero CTA / page-head 主操作(已全局覆盖) |
| `.icon-btn` 方形 | 36×36 | — | — | 纯 icon(无文字时) |
> **等级 × 尺寸** 可自由组合 — 一级可以是 sm/默认/lg,二级三级同理。
#### 5 种状态
| 状态 | 一级表现 | 二级表现 | 三级表现 |
| ---- | -------- | -------- | -------- |
| Default | 橙底 + 4 层橙阴影 | 白底 + inside-border `--black-alpha-12` | 透明 + 字 `--black-alpha-56` |
| Hover | 阴影抬升(第 3 层加亮) | 底 `--black-alpha-4` + 边 `--black-alpha-24` | 底 `--black-alpha-4` + 字转 `--accent-black` |
| Active(按下) | `scale(.995)` + 阴影 inset 加深 | 底 `--black-alpha-7` + `scale(.99)` | 底 `--black-alpha-7` |
| Focused | 2 px `--heat-40` ring · offset 2 | 同左 | 同左 |
| Disabled | 底 `--heat-40` + 字 `#fff` + 阴影消失 + `not-allowed` | 底 `--black-alpha-5` + 字 `--black-alpha-32` + 边 `--black-alpha-12` | 字 `--black-alpha-32` |
**所有按钮过渡:** `background 200ms, transform 100ms ease`
#### 常见错误
- ❌ **一个区域两个一级按钮** — 用户视线分散 → 改为 [二级] + [一级]
- ❌ **危险操作用一级橙色** — 改为放在 modal 里二次确认,modal 内的"确认删除"用一级
- ❌ **重要操作用三级 ghost** — 弱化了主流程,改用一级或二级
- ❌ **三级按钮加边框** — 三级就是 ghost,加边框就变二级了
- ❌ **页面 inline 调高度/字号** — 用 `.btn-sm` / `.btn-lg`,不要 inline 改
### §4.2 Pill(状态徽标 · 严格 3 级分层)
> **同级别尺寸必须完全一致。** 不允许混用。
| 级别 | 类名 | 高度 | padding | 字号 | dot | 用途 |
| ---- | ---- | ---- | ------- | ---- | --- | ---- |
| L1 大胶囊 | `.pill-l1` | 28 px | `0 12` | 13 | 8 px | 项目状态 / 列表行主标签 |
| **L2 中胶囊 ★ 默认** | `.pill-l2`(或不写) | 22 px | `0 10` | 11.5 | 6 px | 卡片内 / 表格内默认 |
| L3 小胶囊 | `.pill-l3` | 18 px | `0 8` | 10.5 | 5 px | KPI 角标 / 行内 mono 标签 |
**4 种色调:**
| 类名 | 文字色 | 底色 | 边框 | 含义 |
| ---- | ------ | ---- | ---- | ---- |
| `.pill.info` | `--heat` | `--heat-12` | `--heat-20` | 信息 / 进行中 / 默认强调 |
| `.pill.ok` | `--accent-forest` | `--forest-bg` | `--forest-bd` | 成功 / 已完成 |
| `.pill.err` | `--accent-crimson` | `--crimson-bg` | `--crimson-bd` | 失败 / 错误 |
| `.pill.neutral` | `--black-alpha-56` | `--black-alpha-4` | `--border-faint` | 中性 / 已归档 |
```html
<span class="pill pill-l2 pill-info"><span class="dot"></span>生成中</span>
```
每个 pill 前置 `<span class="dot"></span>`,色继承 `currentColor`
### §4.3 输入族
| 类名 | 高度 | 用法 |
| ---- | ---- | ---- |
| `.input` | 36 px | 单行文本框 |
| `.textarea` | 自动 | 多行 · min 88 px / line-height 1.6 / resize vertical |
| `.select` | 36 px | 下拉 · 带 chevron 背景图 |
| `.field` | flex col / gap 6 | 包 label + input + hint 的容器 |
| `.field-label` | 13 / 500 | 字段标签(必填用 `<span class="req">*</span>`) |
| `.field-hint` | 12 / `--black-alpha-48` | 字段提示 |
| `.search-box`(sidebar) | 36 px | 顶部全局搜索 + Ctrl K |
| `.toolbar .search-inline` | 36 px | 列表页工具栏内嵌搜索(左侧 icon 必加 `z-index: 2`) |
**5 种状态:**
| 状态 | 边框 | 底色 |
| ---- | ---- | ---- |
| Default | `--black-alpha-12` | `--surface` |
| Hover | `--black-alpha-24` | `--surface` |
| Focus | `--heat-40` + `inset 0 0 0 1px --heat-40` | `--surface` |
| Error | `--accent-crimson` | `--crimson-bg` |
| Disabled | `--black-alpha-12` | `--black-alpha-5` + 字 `--black-alpha-32` |
**占位字色:** `--black-alpha-48`,disabled 占位 `--black-alpha-24`
**带 Ctrl K 搜索框关键坑:**
1. 左侧 icon 必须 `z-index: 2`(否则被 input 白底盖住)
2. 用 "Ctrl K" 纯文本,**不要用 `⌘` 字符**(JetBrains Mono 不带该字形,显示成方框)
3. 快捷键提示不要 kbd 边框,**纯灰色 mono 平铺**(克制)
4. 字体用 `var(--font-inter) / 700`(Inter Bold 紧凑感)
### §4.4 表单控件(Checkbox / Radio / Switch)
**通用原则:** 全部用真 SVG indicator(via background-image data URI 或 inline `<svg>`),**禁止用 border-width / transform: rotate(45deg) 凑对勾**。
| 控件 | 容器尺寸 | Checked 表现 |
| ---- | -------- | ------------ |
| Checkbox `.ck` | 16×16 / 4 px 圆角 / 边 `--black-alpha-24` | `--heat` 底 + 12×12 白 SVG checkmark |
| Indeterminate | 同上 | `--heat` 底 + 12×12 白 SVG 横线 |
| Radio `.rd` | 16×16 圆 / 边 `--black-alpha-24` | 8×8 `--heat` 实心圆(`::after` 即可) |
| Switch `.sw` | 28×16 / 999 圆角 | On: `--heat` 底,圆球右移 `left: 14px` |
Disabled: 底 `--black-alpha-5` + 边 `--black-alpha-12` + 半透明。
### §4.5 KPI 统计行(`.stats`)
```html
<div class="stats with-corners">
<div class="stat">
<div class="lbl">本月营收 <span class="pill pill-l3 pill-ok"><span class="dot"></span>+33%</span></div>
<div class="v">¥327<small>.40 K</small></div>
<div class="delta up">↑ 较上月 +33%</div>
</div>
<!-- ×4 -->
</div>
```
- 1 行 4 格 grid,共用一个 inside-border 容器,圆角 8 px,**无 gap**
- 列与列之间用 `border-right: 1px solid var(--border-faint)`(最后一列去掉)
- 容器四角加 SVG "+" 准星(`.with-corners`)
- 每格结构:label + L3 pill → 大数字(32 px) → delta / mini progress
### §4.6 进度条(5 段流水线)
```html
<div class="prog">
<span class="done"></span><span class="done"></span>
<span class="cur"></span><!-- 或 .now -->
<span></span><span></span>
</div>
```
每段 18×5 px / 3 px 间距 / 2 px 圆角。
| 状态 | 颜色 | 动画 |
| ---- | ---- | ---- |
| 未开始 | `--black-alpha-8` | 静态 |
| 已完成 `.done` | `--accent-forest` | 静态 |
| 进行中 `.cur``.now` | `--heat` | **1.4s 脉动** opacity 1↔0.55 + scaleY 1↔0.7 |
| 失败 `.fail` / `.err` | `--accent-crimson` | 静态 |
**核心:** 动 = 在运行,静 = 完成/失败。脉动只给"进行中"。
### §4.7 列表行(`.list-row`)
```
[缩略图 56×72] [标题 + meta] [.prog 5 段] [.pill L2] [.btn-sm]
```
- grid: `56px 1fr auto auto auto` / gap 22
- padding: `20 24`
- 行间 1 px 分隔(`--border-faint`),最后行去
- Hover: `--black-alpha-4` 底色
### §4.8 Tabs(主 / 副)
**主 Tab(下划线激活):**
- 高 36 px / padding `0 14` / 字号 13 / 字重 500
- 未选中: `--black-alpha-56`
- 选中: `--accent-black` + bottom 2 px `--heat` 横线
- Hover(未选中): `--black-alpha-4`
**副 Tab(过滤型):**
- 高 28 px / padding `0 10` / 字号 12
- 未选中: `--black-alpha-56` + 灰度 icon
- 选中: `--accent-black` + 彩色 icon
- Tab 之间用 1 px 高 12 的 `--ink-alpha-7` 竖条分隔
### §4.9 Chip 筛选(下拉)
```html
<div class="chip-wrap">
<button class="chip"><span>商品品类</span><svg class="caret"></svg></button>
<div class="chip-menu"><!-- .mi 选项 · 多选含 .mi-check --></div>
</div>
```
- `.chip` 高 36 / padding `0 14` / 字号 13 / 圆角 8
- Active: 边 `--heat-40` + 字 `--heat` + 底 `--heat-12`
- 打开后菜单 `.chip-wrap.open .chip-menu { display: block }`
- 选项 `.mi` 高 32 / hover `--black-alpha-4` / selected `--heat-12` + 右侧 `--heat`
### §4.10 卡片 / 快捷入口
| 类名 | 描述 |
| ---- | ---- |
| `.card-hard` | 通用硬卡 · 白底 + inside-border + 8 px 圆角 |
| `.card-hard.with-corners` | 加四角 SVG 准星 |
| `.shortcut` | 快捷入口 · 左 32×32 橙 icon-box + 右标题 + mono 描述 |
| `.tip` | 提示框 · **1 px dashed** 边框 + 加粗标题 + 正文 |
| `.placeholder` | 缩略图占位 · 灰底网纹 · 内置 `.ph-frame` 显示 `9:16` |
| `.product-card / .proj-card / .asset-card` | 各类卡片(模板见现有页面) |
`.icon-box`:32×32 / 8 px 圆角 / `--heat-12` 底 / 16 px line icon(`--heat`)。
### §4.11 浮层 / 反馈
| 组件 | 类名 | 关键参数 |
| ---- | ---- | -------- |
| Modal | `.modal-bg > .modal > .modal-h/b/f` | 居中 460-480 px · 4 角 SVG 准星 · `rgba(21,20,15,.42)` 遮罩 + `backdrop-filter: blur(8px)` · 进入 `scale(.96→1)` / 250ms 弹性 · ESC / 点击遮罩关闭 |
| Drawer | `.drawer-bg > .drawer > .drawer-h/b/f` | 右侧抽屉 · 默认 540 px(商品 drawer 820 px) · 进入 `translateX(100%→0)` / 250 ms |
| Toast | `.toast`(`.show` 触发) | 右下 24 px / 白底 inside-border + `--shadow-floating` / 进入 `translateX(420→0)` 300ms 弹性 / 自动 2400 ms 消失 / 内含 24×24 橙 icon-box + 标题 + mono 副文 |
| Empty | `.empty-state.show` | 虚线边框 + 灰 icon + 标题(14/600)+ mono 描述 |
| 缺图徽标 | `.tri-missing-badge` + `.tri-missing-pop` | 商品/人物卡缩图左上角橙底告警徽标 + hover 暗色 tooltip |
| 卡片删除 | `.card-del-btn` | 卡片右上角 hover 显示 · 32×32 / 红 hover |
| 批量栏 | `.bulk-bar` | 选中后吸底浮动操作栏 |
| 卡片勾选 | `.card-check` | 卡片左上角 22×22 圆形 checkbox(编辑模式 hover 显示) |
| Spinner | `.spinner` | 18×18 旋转 · 2 px 橙边 |
### §4.12 Icon 系统
**通用规则:**
- 一律 **SVG inline**,禁止 `<img>` 引图标
- 库:Lucide 或 Phosphor Regular
- `stroke-width: 1.5` / `stroke-linecap: round` / `stroke-linejoin: round` / `fill: none`
- 颜色:`stroke="currentColor"` 继承
- **禁:** 彩色 emoji / filled icon
**5 档尺寸:**
| 档 | 尺寸 | 用途 |
| -- | ---- | ---- |
| S | 14 px | 内嵌 inline 文字旁 |
| **M ★ 默认** | 16 px | 按钮内 / Tab / list 行 |
| L | 20 px | 顶栏 / 快捷入口 / dropdown 触发器 |
| XL | 24 px | Modal 头 / Toast / 空状态 |
| Hero | 36 px | 空状态插画 / 大占位 |
**颜色:** 默认 `--black-alpha-56` / hover `--accent-black` / active `--heat` / disabled `--black-alpha-12` / 在主 CTA 内 `#fff`
---
## §5 · Mono 装饰元素(品牌签名 · 不能丢)
| 用法 | 示例 |
| ---- | ---- |
| 方括号标签 | `[ 200 OK ]` `[ /v2 ]` `[ .MP4 · 9:16 ]` `[ STUDIO ]` `[ ALL · 12 ] →` |
| 注释时间戳 | `// 05.14 · 周五 · 14:32` |
| 命令路径 | `/sidebar collapse · /toast dismiss` |
| 数值后缀 | `¥327`(主体大字)+ `<small>.40</small>`(小字次级) |
| 强调单词上色 | `<b style="color: var(--accent-black)">3 个项目</b>` — 加深一档,**不变橙**。橙色只留给 CTA |
| ASCII 散点装饰 | 主区域 4 角撒 `· · + +XX+ +XXXX·` / 8.5 px / `--black-alpha-12` |
| 边角 Mono 标签 | 主区域 4 角:左上 `[ 200 OK ]` / 右上 `[ /v2 ]` / 左下 `[ .MP4 · 9:16 ]` / 右下 `[ STUDIO ]` |
| 链接式"更多" | `[ ALL · 12 ] →` — mono 标签 + 箭头,`--black-alpha-48`,hover 转橙 |
| 缩略图占位 | 不放假图,放比例 `9:16` mono 字符 |
**字体:** `var(--font-mono)` / 11 px / `0.04em` 字距(标签场景)。
---
## §6 · 五种页面级布局模式
页面骨架统一,**内容区布局**按用途分 5 类:
### A · 看板型(Dashboard)
**代表页:** [index.html](index.html) · [projects.html](projects.html)(列表)· [library.html](library.html)
**布局:** KPI 行(`.stats.with-corners`)→ 多个 section(`.section-h` + 内容卡)→ 列表行 / shortcut 网格
**辅助类:** `.dash-grid`(1.7 fr + 1 fr 主辅)/ `.recent-row` / `.shortcut` / `.tip`(虚线提示)
### B · 列表 + 筛选型(List + Filter)
**代表页:** [products.html](products.html) · [projects.html](projects.html) · [library.html](library.html) · [team.html](team.html)
**布局:**
```
[ toolbar(search-inline + chip-wrap × n + view-toggle) ] ← sticky top
[ result-meta · 共 N 条 ]
[ ░░ 卡片网格 / 列表 ░░ ]
[ pagination ] ← sticky bottom
[ bulk-bar(选中后显示) ] ← 吸底浮动
```
**辅助类:** `.product-card / .proj-card / .asset-card` · `.view-toggle` · `.pagination` · `.bulk-bar` · `.card-check` · `.card-del-btn`
### C · 编辑器型(Editor / IDE)
**代表页:** [pipeline.html](pipeline.html) · [model-photo.html](model-photo.html) · [image-optimize.html](image-optimize.html) · [studio.html](studio.html)
**特征:** 锁视口高度 · 多栏内部滚 · 大量页面级独有样式(目前 inline `<style>` 600-1400 行)
**布局:** 左栏(资产/导航 sticky)+ 中央(画布/参数)+ 右栏(预览/AI 助手)
**⚠️ 警告:** 这一类页面定制最多,改之前先确认 restraint.css 没有现成组件再加 inline。
### D · 表单 / 向导型(Wizard / Form)
**代表页:** [projects-new.html](projects-new.html) · [products.html](products.html) drawer · [account.html](account.html) · [settings.html](settings.html)
**布局:** 左侧 sticky 步数条 + 右侧多 pane 同时显示(非 Tab 切换)
**辅助类:** `.wizard` · `.steps .step` · `.wiz-pane` · `.opt-card` · `.bullet-list`
### E · 单卡型(Single Screen)
**代表页:** [login.html](login.html) · [register.html](register.html)
**特征:** 不渲染 sidebar/topbar · 全屏灰底 + 中心白卡
---
## §7 · 待统一清单(V2.1 落地偏离点)
> 扫完 18 个 HTML 发现的偏离点。按影响视觉一致性的程度排序。**改页面时遇到这些点,顺手改正。**
### 高优先级(影响视觉对齐)
| # | 问题 | 现状 | 应改为 |
| - | ---- | ---- | ------ |
| 1 | **按钮等级混乱**(★ 最严重) | 各页随意选 `.btn` / `.btn-primary` / `.btn-ghost`,有的页面 2 个一级按钮并列,有的把"取消"做成主橙;尺寸也乱(38/32/44 混) | 按 §4.1 三级体系:**一个区域只 1 个一级**(`.btn-primary`,主动作)· 二级 `.btn`(并列动作)· 三级 `.btn-ghost`(辅助)· 尺寸默认 36 / lg 40 / sm 28 |
| 2 | 输入框高度 | products 38 / projects search 32 / login 36 | 统一 36 |
| 3 | Tab 激活样式 | product-detail 下划线 / library 底色填充 | **统一下划线 + bottom 2 px `--heat`** |
| 4 | Hover 底色 | 多数 `--background-lighter` / 部分 `--black-alpha-4` | **统一 `--black-alpha-4`** |
| 5 | 卡片标题字号 | projects 13.5 / products 14 | **统一 14 px / 500** |
| 6 | 卡片网格列宽 | products 240 固定 / library auto-fill 180 / projects-new 4 列固定 | 商品类 240 · 资产类 180 · 通用 `auto-fill minmax(220px, 1fr)` |
### 中优先级(语义偏离)
| # | 问题 | 现状 | 应改为 |
| - | ---- | ---- | ------ |
| 7 | Pill 变体不全 | restraint.css 只 4 种(info/ok/err/neutral),library 用了 `.pill.archive` 没定义 | 在 restraint.css 补 `.pill.archive`(灰)、`.pill.warn`(honey 黄) |
| 8 | X 关闭按钮 | drawer 30 / modal 32 / toast inline 28 | **统一 32×32 / 8 px 圆角** |
| 9 | label 字重 | products 500 / product-detail 600 | **统一 500** |
| 10 | mono 装饰 | 部分页有 4 角 `[ 200 OK ]`,部分没 | **主工作台型必须有,编辑器型可省** |
| 11 | section-h 分隔 | 部分用 `border-bottom`,部分不用 | **统一不用**(留白即分隔) |
### 低优先级(架构清理)
| # | 问题 | 现状 | 应改为 |
| - | ---- | ---- | ------ |
| 12 | 编辑器页 inline `<style>` 过大 | pipeline 795 / model-photo 1393 / platform-cover 1003 行 | 抽 `.stepper / .shot-card / .mp-prod-item / .pl-modal` 等高频组件到 restraint.css 或独立 editor.css |
| 13 | 四角准星重复 SVG | restraint.css 已有 `.with-corners` 工具,部分页面又自己定义了 SVG 背景 | 全部改用 `.with-corners` + `<span class="corner-tr/.corner-bl">` |
| 14 | 滚动条样式残留 | 部分页 inline `scrollbar-width: thin` | restraint.css 已 `!important` 全局隐藏,清理 inline |
| 15 | mono 字体里的中文 | 部分页直接写 `font-family: monospace` 走系统字体 | 全部换 `var(--font-mono)` |
---
## §8 · Don't List(绝对禁止 · 每次自检)
- ❌ **0 px 硬切角的卡片** — 一律 8 px
- ❌ **大圆角 `>12 px`** — 直接判错
- ❌ **渐变背景 / 玻璃拟态** — 只允许 modal 遮罩 `backdrop-filter: blur`
- ❌ **彩色 emoji** — 一律 SVG line icon · stroke 1.5
- ❌ **多个 accent 色** — 全场只有橙(其他色仅做语义信号点)
- ❌ **hover 时换深 hue 的橙** — 用 alpha,不用更深的橙(`#D04E1F` 这种判错)
- ❌ **真 `border` + hover 边框消失** — 用 inside-border `::before`
- ❌ **居中对齐大段正文** — 全部左对齐
- ❌ **同一行混用直角和圆角** — "不要有些是直角,胶囊又是圆角"
- ❌ **荧光状态色** — 避免霓虹绿、电光蓝、霓虹粉
- ❌ **页面内 `<style>` 重写共享类**(`.btn` `.pill` `.input` `.modal` 等)— 改 restraint.css
- ❌ **新建色 token** — 必须复用现有 token
- ❌ **`⌘` Unicode 字符** — JetBrains Mono webfont 不带该字形,显示成方框 → 用 "Ctrl K" 纯文本或 SVG
- ❌ **多色 emoji icon / filled icon** — 一律 line icon
- ❌ **居中卡片浮在背景上的"贴纸感"** — 用边框 + 装订线,不用阴影
- ❌ **彩虹流光 / hover 旋转缩放** — 无意义微动效
- ❌ **场记板 / 丝绒 / 霓虹灯** — 装饰盖过内容,判错
---
## §9 · 新页面 / 改页面 checklist(每次必过)
### 写代码前
- [ ] 已 Read 本文件 §1 § 3 § 8(至少)
- [ ] 用 `Grep` 查 restraint.css 是否已有该组件
- [ ] 看 `_archive/design-system.html` 找视觉参考(用浏览器打开 file://)
- [ ] 不确定的设计点 — **先问用户**,不要凭感觉
### 写代码时
- [ ] HTML 用 `<div class="app"><aside class="sidebar"></aside><main><div class="topbar"></div><div class="content">…</div></main></div>` 骨架
- [ ] head 含 `<link rel="stylesheet" href="assets/restraint.css">` + Inter + JetBrains Mono Google Fonts
- [ ] body 末尾 `<script src="assets/shell.js" defer></script>`
- [ ] 标题区用 `.page-head > h1 + .sub`
- [ ] 主操作按钮放 `.page-head > .actions`(自动 40 px 高)
- [ ] 子区块用 `.section-h > h2 + .more`
- [ ] 按钮全 `.btn` 系列 · 不要自己写
- [ ] 状态徽标全 `.pill.info/.ok/.err/.neutral` · 要新变体先回 restraint.css 加
- [ ] 输入框全 `.input / .select / .textarea` · 字段用 `.field`
- [ ] 浮层全用现成 Modal / Drawer / Toast
- [ ] 图标 SVG line icon · stroke 1.5 / linecap round / `stroke="currentColor"`
- [ ] 时间戳 mono 注释 `// 05.22 · 周四`
- [ ] 强调单词加深(不变橙) `<b style="color:var(--accent-black)">3 个</b>`
- [ ] 数字加 `.num``tabular-nums`
- [ ] 状态色按语义选 `--heat / --accent-forest / --accent-crimson / --accent-honey`
### 写完自检
- [ ] 对照 §8 Don't List 逐条过
- [ ] 在浏览器打开页面 · 截图 + 跟 design-system.html 对比
- [ ] 测 hover / focus / active / disabled 状态都正确
- [ ] 测 dark mode(给 body 加 `.dark` class 看是否破)
- [ ] 移动端缩到 1100 px 以下看响应式
### 提交前
- [ ] 不在 master/main 上,在 dev 分支
- [ ] 不带 `--no-verify`,不跳过 hook
- [ ] commit 信息简洁,说"为什么"不只是"什么"
---
## §10 · 参考资料
- [assets/restraint.css](assets/restraint.css) — 共享 CSS · token 定义 + 100+ 组件类(1592 行)· 实现层
- [assets/shell.js](assets/shell.js) — 通用 sidebar / topbar 注入
- [_archive/design-system.html](_archive/design-system.html) — 历史交互样板间(浏览器打开看效果)
- [_archive/DESIGN_SPEC_V2.md](_archive/DESIGN_SPEC_V2.md) — 历史详细规范(理论依据 / 迁移轨迹)
- 视觉灵感:Firecrawl Playground · Linear · Stripe Dashboard
- 图纸感来源:印刷套版准星 + 老 Unix 终端
---
**任何条款不清晰、想加新组件、或想改 token,先在群里讨论 → 写进本文件 → 改 restraint.css。**
**永远不要在某个页面 inline `<style>` 里偷偷扩展规范。**

File diff suppressed because it is too large Load Diff

View File

@ -57,12 +57,12 @@
<div class="v">8</div>
<div class="delta up"><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="M12 19V5M5 12l7-7 7 7"/></svg> 本月 +3</div>
</a>
<a class="stat" href="projects.html">
<a class="stat" href="projects.html?filter=wip">
<div class="lbl">进行中 <span class="badge">WIP</span></div>
<div class="v">3</div>
<div class="delta">2 个待审核</div>
</a>
<a class="stat" href="projects.html">
<a class="stat" href="projects.html?filter=done">
<div class="lbl">本月成片 <span class="badge">DONE</span></div>
<div class="v">3</div>
<div class="delta up"><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="M12 19V5M5 12l7-7 7 7"/></svg> 较上月 +33%</div>

View File

@ -176,6 +176,42 @@
.bulk-bar button svg { width: 12px; height: 12px; }
.bulk-bar .clear-sel { color: rgba(255,255,255,.6); font-size: 12px; cursor: pointer; background: none; border: 0; padding: 4px 6px; }
.bulk-bar .clear-sel:hover { color: var(--accent-white); }
/* 移动到 · 弹层菜单 (向上弹) */
.bulk-bar .move-wrap { position: relative; display: inline-flex; }
.bulk-bar .move-menu {
position: absolute;
bottom: calc(100% + 8px);
right: 0;
min-width: 160px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
padding: 6px;
box-shadow: 0 8px 24px rgba(0,0,0,.18);
display: none;
z-index: 2;
}
.bulk-bar .move-menu.show { display: block; }
.bulk-bar .move-menu .mv-item {
display: flex; align-items: center; gap: 8px;
width: 100%; height: 32px; padding: 0 10px;
background: transparent; border: 0;
color: var(--accent-black);
font-size: 13px; font-family: inherit;
cursor: pointer; border-radius: var(--r-sm);
text-align: left;
}
.bulk-bar .move-menu .mv-item:hover { background: var(--heat-12); color: var(--heat); }
.bulk-bar .move-menu .mv-item svg { width: 12px; height: 12px; opacity: .7; }
/* tab 作为拖拽目标 hover 态 */
.tabs .tab.drag-over {
background: var(--heat-12);
color: var(--heat);
border-radius: var(--r-sm);
box-shadow: inset 0 0 0 1px var(--heat-40);
}
body.edit-mode .asset-card { cursor: grab; }
body.edit-mode .asset-card.dragging { opacity: .4; }
.asset-thumb { aspect-ratio: 1; }
.asset-card.video .asset-thumb { aspect-ratio: 9/16; max-height: 280px; }
.asset-body { padding: 12px 14px; }
@ -286,6 +322,7 @@
<div class="tab" data-tab="products">商品图 <span class="count">0</span></div>
<div class="tab" data-tab="finals">成片 <span class="count">0</span></div>
<div class="tab" data-tab="uploads">我的上传 <span class="count">0</span></div>
<div class="tab" data-tab="unclassified">未分类 <span class="count">0</span></div>
</div>
<div class="toolbar">
@ -399,7 +436,18 @@
<span class="card-check" aria-hidden="true"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg></span>
<button class="card-del-btn" type="button" title="删除资产" onclick="event.stopPropagation();" data-action="delete-asset"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
<div class="placeholder asset-thumb">
<span class="tri-missing-badge" title="手动上传的人物未生成三视图,前往图片生成可补齐"><span class="lbl-mono">缺三视图</span></span>
<span class="tri-missing-badge" tabindex="0" role="button" aria-label="缺三视图,查看说明">
<span class="ico" aria-hidden="true"></span>
<span class="lbl-mono">缺三视图</span>
<span class="tri-missing-pop" role="tooltip">
<span class="pop-h">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M12 9v4M12 17h.01"/><path d="M10.3 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/></svg>
MISSING TRI-VIEW
</span>
<span class="pop-body">手动上传的人物未生成 <b>正 / 侧 / 背</b> 三视图。直接进入图片或视频生成,人脸/服饰一致性可能下降。</span>
<span class="pop-tip">建议:前往 <b>图片生成</b> 先补齐三视图,再发起后续生成。</span>
</span>
</span>
<span class="ph-frame">妈妈 · 居家</span>
</div>
<div class="asset-body">
@ -456,7 +504,18 @@
<span class="card-check" aria-hidden="true"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg></span>
<button class="card-del-btn" type="button" title="删除资产" onclick="event.stopPropagation();" data-action="delete-asset"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
<div class="placeholder asset-thumb">
<span class="tri-missing-badge" title="手动上传的人物未生成三视图,前往图片生成可补齐"><span class="lbl-mono">缺三视图</span></span>
<span class="tri-missing-badge" tabindex="0" role="button" aria-label="缺三视图,查看说明">
<span class="ico" aria-hidden="true"></span>
<span class="lbl-mono">缺三视图</span>
<span class="tri-missing-pop" role="tooltip">
<span class="pop-h">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M12 9v4M12 17h.01"/><path d="M10.3 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/></svg>
MISSING TRI-VIEW
</span>
<span class="pop-body">手动上传的人物未生成 <b>正 / 侧 / 背</b> 三视图。直接进入图片或视频生成,人脸/服饰一致性可能下降。</span>
<span class="pop-tip">建议:前往 <b>图片生成</b> 先补齐三视图,再发起后续生成。</span>
</span>
</span>
<span class="ph-frame">李爷爷 · 居家</span>
</div>
<div class="asset-body">
@ -692,6 +751,9 @@
</div>
</div>
<!-- ============ 未分类(由图片优化"加入资产库"持久化进来) ============ -->
<div class="asset-grid" data-tab="unclassified" id="grid-unclassified" hidden></div>
<div class="empty-state" id="empty">
<div class="ic-empty">
<svg width="20" height="20" 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>
@ -866,6 +928,14 @@
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" 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="move-wrap">
<button type="button" id="bulk-move">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"/><path d="M3 12h12"/></svg>
移动到
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" style="margin-left:2px"><path d="M4 10l4-4 4 4"/></svg>
</button>
<div class="move-menu" id="bulk-move-menu"></div>
</div>
<button type="button" id="bulk-exit">完成</button>
</div>
@ -897,7 +967,47 @@ Shell.render({ active: 'library', crumbs: [{ label: '工作台', href: 'index.ht
})();
// ============== State ==============
const TAB_KEYS = ['people', 'scenes', 'products', 'finals', 'uploads'];
const TAB_KEYS = ['people', 'scenes', 'products', 'finals', 'uploads', 'unclassified'];
/* ============== 加载图片优化"加入资产库"持久化数据 ==============
image-optimize.html 把图保存到 localStorage['fs-library-unclassified']
这里读出后注入到 #grid-unclassified ============== */
(function loadUnclassified() {
let list;
try { list = JSON.parse(localStorage.getItem('fs-library-unclassified') || '[]'); } catch (e) { list = []; }
if (!Array.isArray(list) || !list.length) return;
const grid = document.getElementById('grid-unclassified');
if (!grid) return;
function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c])); }
function fmtDate(ts) {
if (!ts) return '';
const d = new Date(ts), z = n => (n < 10 ? '0' + n : '' + n);
return d.getFullYear() + z(d.getMonth() + 1) + z(d.getDate());
}
list.forEach(it => {
const card = document.createElement('div');
card.className = 'asset-card';
card.dataset.name = it.name || '未命名';
card.dataset.kind = '未分类';
card.dataset.source = it.source || '图片优化';
card.dataset.used = '0';
card.dataset.added = fmtDate(it.addedAt);
card.dataset.libId = it.id || '';
card.innerHTML = `
<span class="card-check" aria-hidden="true"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg></span>
<button class="card-del-btn" type="button" title="删除资产" onclick="event.stopPropagation();" data-action="delete-asset"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
<div class="placeholder asset-thumb"><span class="ph-frame">${esc(it.name || '未命名')}</span></div>
<div class="asset-body">
<div class="asset-name">${esc(it.name || '未命名')}</div>
<div class="asset-meta">未分类 · ${esc(it.source || '图片优化')} · ${esc(it.ratio || '')}</div>
</div>
`;
card.addEventListener('click', () => {
if (typeof Shell !== 'undefined' && Shell.toast) Shell.toast('查看资产', it.name || '未分类素材');
});
grid.appendChild(card);
});
})();
const PAGE_SIZES = [12, 24, 48, 96];
const state = {
tab: 'people',
@ -1648,7 +1758,19 @@ delCancel.addEventListener('click', closeDelConfirm);
delBg.addEventListener('click', e => { if (e.target === delBg) closeDelConfirm(); });
delOk.addEventListener('click', () => {
const n = _delQueue.length;
// 收集被删除中、source 是"未分类"的 libId,同步从 localStorage 移除
const removedLibIds = _delQueue
.filter(c => c.dataset.libId)
.map(c => c.dataset.libId);
_delQueue.forEach(card => card.remove());
if (removedLibIds.length) {
try {
const LIB_KEY = 'fs-library-unclassified';
const list = JSON.parse(localStorage.getItem(LIB_KEY) || '[]');
const next = (Array.isArray(list) ? list : []).filter(x => !removedLibIds.includes(x.id));
localStorage.setItem(LIB_KEY, JSON.stringify(next));
} catch (e) { /* ignore */ }
}
closeDelConfirm();
Shell.toast('已删除', n === 1 ? '资产已移除' : '已删除 ' + n + ' 个资产');
updateBulkBar();
@ -1684,6 +1806,7 @@ function enterEditMode() {
document.body.classList.add('edit-mode');
libManageBtn.classList.add('active');
libManageLabel.textContent = '完成';
applyDraggableToCards(true);
updateBulkBar();
}
function exitEditMode() {
@ -1691,6 +1814,8 @@ function exitEditMode() {
libManageBtn.classList.remove('active');
libManageLabel.textContent = '管理资产';
document.querySelectorAll('.asset-card.selected').forEach(c => c.classList.remove('selected'));
applyDraggableToCards(false);
if (bulkMoveMenu) bulkMoveMenu.classList.remove('show');
}
libManageBtn.addEventListener('click', () => {
if (document.body.classList.contains('edit-mode')) exitEditMode();
@ -1707,6 +1832,132 @@ bulkDel.addEventListener('click', () => {
openDelConfirm(sel);
});
// ── 移动到 · 菜单 + 拖拽 ──
const TAB_NAMES = {
people: '人物', scenes: '场景', products: '商品图',
finals: '成片', uploads: '我的上传', unclassified: '未分类'
};
const bulkMove = document.getElementById('bulk-move');
const bulkMoveMenu = document.getElementById('bulk-move-menu');
function getCurrentTab() {
const active = document.querySelector('#asset-tabs .tab.active');
return active ? active.dataset.tab : TAB_KEYS[0];
}
function refreshAllTabCounts() {
TAB_KEYS.forEach(t => {
const tab = document.querySelector(`.tab[data-tab="${t}"] .count`);
if (tab && cardsByTab[t]) tab.textContent = cardsByTab[t].length;
});
const total = TAB_KEYS.reduce((s, t) => s + (cardsByTab[t] ? cardsByTab[t].length : 0), 0);
const sidebarBadge = document.querySelector('aside.sidebar a[href="library.html"] .pill-mini');
if (sidebarBadge) sidebarBadge.textContent = total;
const subMap = { people:'sub-people', scenes:'sub-scenes', products:'sub-products', finals:'sub-finals' };
Object.keys(subMap).forEach(k => {
const el = document.getElementById(subMap[k]);
if (el && cardsByTab[k]) el.textContent = cardsByTab[k].length;
});
}
function moveSelectedTo(targetTab) {
const sel = getSelected();
if (!sel.length) { Shell.toast('请先选中资产'); return; }
const targetGrid = document.getElementById(`grid-${targetTab}`);
if (!targetGrid) return;
let movedCount = 0;
sel.forEach(card => {
// 找出 card 当前所在 tab (DOM-based,跨多 tab 的 selected 也能正确移动)
const curGrid = card.closest('.asset-grid');
const curTab = curGrid ? curGrid.dataset.tab : getCurrentTab();
if (curTab === targetTab) return; // 同分类跳过
// 更新内存
if (cardsByTab[curTab]) {
const idx = cardsByTab[curTab].indexOf(card);
if (idx >= 0) cardsByTab[curTab].splice(idx, 1);
}
if (cardsByTab[targetTab]) cardsByTab[targetTab].push(card);
// 更新 DOM
card.classList.remove('selected');
card.dataset.kind = TAB_NAMES[targetTab];
targetGrid.appendChild(card);
movedCount += 1;
});
refreshAllTabCounts();
updateBulkBar();
if (movedCount > 0) {
Shell.toast('已移动', `${movedCount} 个资产 → 「${TAB_NAMES[targetTab]}」`);
} else {
Shell.toast('未移动', '所选资产已在该分类');
}
}
function renderMoveMenu() {
const cur = getCurrentTab();
bulkMoveMenu.innerHTML = TAB_KEYS
.filter(t => t !== cur)
.map(t => `<button class="mv-item" type="button" data-target="${t}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
移到「${TAB_NAMES[t]}」
</button>`).join('');
bulkMoveMenu.querySelectorAll('.mv-item').forEach(btn => {
btn.addEventListener('click', () => {
moveSelectedTo(btn.dataset.target);
bulkMoveMenu.classList.remove('show');
});
});
}
bulkMove.addEventListener('click', e => {
e.stopPropagation();
if (!getSelected().length) { Shell.toast('请先选中资产'); return; }
renderMoveMenu();
bulkMoveMenu.classList.toggle('show');
});
document.addEventListener('click', e => {
if (!bulkMove.contains(e.target) && !bulkMoveMenu.contains(e.target)) {
bulkMoveMenu.classList.remove('show');
}
});
// ── 拖拽到 tab 移动 (edit-mode 下生效) ──
function applyDraggableToCards(on) {
document.querySelectorAll('.asset-card').forEach(c => {
if (on) c.setAttribute('draggable', 'true');
else c.removeAttribute('draggable');
});
}
document.addEventListener('dragstart', e => {
const card = e.target.closest('.asset-card');
if (!card || !document.body.classList.contains('edit-mode')) return;
// 没选中就当前 card 也算 (允许直接拖单张)
if (!card.classList.contains('selected')) {
card.classList.add('selected');
updateBulkBar();
}
card.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
try { e.dataTransfer.setData('text/plain', 'move-asset'); } catch (err) {}
});
document.addEventListener('dragend', e => {
const card = e.target.closest('.asset-card');
if (card) card.classList.remove('dragging');
document.querySelectorAll('#asset-tabs .tab.drag-over').forEach(t => t.classList.remove('drag-over'));
});
document.querySelectorAll('#asset-tabs .tab').forEach(tab => {
tab.addEventListener('dragover', e => {
if (!document.body.classList.contains('edit-mode')) return;
if (tab.dataset.tab === getCurrentTab()) return;
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
tab.classList.add('drag-over');
});
tab.addEventListener('dragleave', () => tab.classList.remove('drag-over'));
tab.addEventListener('drop', e => {
if (!document.body.classList.contains('edit-mode')) return;
e.preventDefault();
tab.classList.remove('drag-over');
if (tab.dataset.tab === getCurrentTab()) return;
moveSelectedTo(tab.dataset.tab);
});
});
// 编辑模式下,卡片点击切换 selected (不再 toast / 打开)
document.querySelectorAll('.asset-card').forEach(card => {
card.addEventListener('click', e => {

View File

@ -0,0 +1,664 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>方案 A · 商品空间 · 模特上身图 · 流·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>
.app { height: 100vh; overflow: hidden; }
main { display: flex; flex-direction: column; min-height: 0; }
#page-content { flex: 1; min-height: 0; display: flex; flex-direction: column; padding: 0; }
/* ===== 两栏:左侧栏 / 右侧主区 ===== */
.dma {
flex: 1; min-height: 0;
display: grid;
grid-template-columns: 260px 1fr;
}
/* ── 左侧栏 · 仅商品空间(搜索 + 列表 + 全部入口) ── */
.dma-side {
border-right: 1px solid var(--border-faint);
background: var(--surface);
display: flex; flex-direction: column;
min-height: 0;
}
.dma-side-h {
padding: 14px 14px 10px;
flex-shrink: 0;
}
.dma-side-h .ti-row {
display: flex; align-items: center;
margin-bottom: 10px;
}
.dma-side-h .ti {
font-size: 11px; font-family: var(--font-mono);
color: var(--black-alpha-48); letter-spacing: .08em;
text-transform: uppercase;
}
.dma-side-h .ti-row .add {
margin-left: auto;
width: 22px; height: 22px;
display: grid; place-items: center;
background: var(--heat-12); color: var(--heat);
border: 0; border-radius: var(--r-sm);
cursor: pointer;
}
.dma-side-h .add svg { width: 11px; height: 11px; }
/* 搜索框 (Q1-A) */
.dma-search {
display: flex; align-items: center; gap: 8px;
height: 32px; padding: 0 10px;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
transition: border-color var(--t-base), background var(--t-base);
}
.dma-search:focus-within { border-color: var(--heat-40); background: var(--surface); }
.dma-search svg { width: 13px; height: 13px; color: var(--black-alpha-48); flex-shrink: 0; }
.dma-search input {
flex: 1; min-width: 0; height: 100%;
border: 0; outline: 0; background: transparent;
font-size: 12.5px; color: var(--accent-black); font-family: inherit;
}
/* 商品空间列表(flex:1 占据中部所有剩余空间) */
.dma-prod-list {
flex: 1; min-height: 0;
overflow-y: auto;
padding: 4px 10px 10px;
display: flex; flex-direction: column; gap: 4px;
}
.dma-prod {
display: flex; align-items: center; gap: 10px;
padding: 8px;
border: 1px solid transparent;
border-radius: var(--r-sm);
cursor: pointer;
transition: background var(--t-base), border-color var(--t-base);
}
.dma-prod:hover { background: var(--background-lighter); }
.dma-prod.active { background: var(--heat-12); border-color: var(--heat-20); }
.dma-prod .thumb {
flex-shrink: 0;
width: 40px; height: 40px;
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
overflow: hidden;
background: repeating-linear-gradient(135deg, transparent 0 4px, rgba(0,0,0,.04) 4px 5px);
display: grid; place-items: center;
font-family: var(--font-mono); font-size: 9px;
color: var(--black-alpha-32);
}
.dma-prod.active .thumb { border-color: var(--heat); }
.dma-prod .body { flex: 1; min-width: 0; }
.dma-prod .nm {
font-size: 12.5px;
color: var(--accent-black); font-weight: 500;
line-height: 1.3;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.dma-prod.active .nm { color: var(--heat); font-weight: 600; }
.dma-prod .sub {
margin-top: 2px;
font-family: var(--font-mono); font-size: 10px;
color: var(--black-alpha-48); letter-spacing: .02em;
}
/* 全部商品入口(Q1-B · 固定贴底) */
.dma-all {
flex-shrink: 0;
margin: 0 10px 12px;
padding: 10px 12px;
background: var(--background-lighter);
border: 1px dashed var(--border-faint);
border-radius: var(--r-sm);
color: var(--black-alpha-72);
font-size: 12px;
font-family: inherit;
cursor: pointer;
display: flex; align-items: center; gap: 6px;
transition: border-color var(--t-base), color var(--t-base), background var(--t-base);
}
.dma-all:hover { border-color: var(--heat); color: var(--heat); background: var(--heat-12); }
.dma-all .ct {
color: var(--black-alpha-48);
font-family: var(--font-mono); font-size: 10.5px;
margin-left: auto;
}
.dma-all svg { width: 12px; height: 12px; }
/* ── 右侧主区 ── */
.dma-main {
display: flex; flex-direction: column;
min-height: 0;
overflow: hidden;
}
.dma-main-h {
flex-shrink: 0;
display: flex; align-items: center; gap: 14px;
padding: 14px 28px;
border-bottom: 1px solid var(--border-faint);
background: var(--surface);
}
.dma-main-h .crumb {
font-family: var(--font-mono); font-size: 10.5px;
color: var(--black-alpha-48); letter-spacing: .04em;
}
.dma-main-h h2 {
font-size: 20px; font-weight: 600;
letter-spacing: -.015em;
color: var(--accent-black);
}
.dma-main-h .stats {
margin-left: auto;
display: flex; gap: 6px;
font-family: var(--font-mono); font-size: 11px;
color: var(--black-alpha-48); letter-spacing: .02em;
}
.dma-main-h .stats b { color: var(--accent-black); font-weight: 600; }
.dma-main-h .stats .sep { color: var(--black-alpha-24); }
/* 主区:参数 + 结果横向二栏 */
.dma-body {
flex: 1; min-height: 0;
display: grid;
grid-template-columns: 320px 1fr;
gap: 0;
}
/* 左侧参数面板 */
.dma-form {
border-right: 1px solid var(--border-faint);
background: var(--surface);
display: flex; flex-direction: column;
min-height: 0;
}
.dma-form-scroll {
flex: 1; min-height: 0; overflow-y: auto;
padding: 16px 18px;
}
.dma-field { margin-bottom: 16px; }
.dma-field-h {
font-size: 12px; font-weight: 600;
color: var(--accent-black);
margin-bottom: 8px;
}
.dma-field-h .opt {
font-weight: 400; font-size: 11px;
color: var(--black-alpha-48); margin-left: 4px;
}
.dma-models {
display: grid; grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.dma-model {
aspect-ratio: 3 / 4;
border: 1px solid var(--border-faint);
background: var(--background-lighter);
border-radius: var(--r-sm);
position: relative; cursor: pointer;
overflow: hidden;
transition: border-color var(--t-base);
}
.dma-model:hover { border-color: var(--black-alpha-32); }
.dma-model.selected { border-color: var(--heat); border-width: 2px; }
.dma-model .ph {
position: absolute; inset: 0;
display: grid; place-items: center;
font-family: var(--font-mono); font-size: 10px;
color: var(--black-alpha-32);
background: repeating-linear-gradient(135deg, transparent 0 6px, rgba(0,0,0,.03) 6px 7px);
}
.dma-model .nm {
position: absolute; bottom: 0; left: 0; right: 0;
padding: 4px 6px;
background: linear-gradient(transparent, rgba(0,0,0,.5));
font-size: 10px; color: #fff; font-weight: 500;
}
.dma-model.selected::after {
content: ''; position: absolute; top: 4px; right: 4px;
width: 14px; height: 14px;
background: var(--heat) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E") no-repeat center / 9px;
border-radius: 50%;
}
.dma-chip-row { display: flex; flex-wrap: wrap; gap: 6px; }
.dma-chip {
display: inline-flex; align-items: center;
height: 28px; padding: 0 11px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
font-size: 12px; color: var(--black-alpha-72);
font-family: inherit; cursor: pointer;
transition: border-color var(--t-base), color var(--t-base), background var(--t-base);
}
.dma-chip:hover { border-color: var(--black-alpha-32); color: var(--accent-black); }
.dma-chip.active { background: var(--heat-12); color: var(--heat); border-color: var(--heat-20); font-weight: 600; }
.dma-form-cta {
flex-shrink: 0;
padding: 14px 18px;
border-top: 1px solid var(--border-faint);
background: var(--surface);
}
.dma-cost {
display: flex; align-items: center; justify-content: space-between;
margin-bottom: 10px;
font-family: var(--font-mono); font-size: 11px;
color: var(--black-alpha-48); letter-spacing: .02em;
}
.dma-cost .v { color: var(--accent-black); font-weight: 600; }
.dma-gen {
width: 100%; height: 42px;
background: var(--heat); color: #fff;
border: 1px solid var(--heat); border-radius: var(--r-md);
font-size: 14px; font-weight: 600; font-family: inherit;
cursor: pointer;
display: inline-flex; align-items: center; justify-content: center; gap: 8px;
box-shadow: var(--shadow-cta);
}
.dma-gen svg { width: 15px; height: 15px; }
/* 右侧结果区 */
.dma-result {
background: var(--background-base);
min-height: 0; overflow-y: auto;
padding: 22px 24px;
}
.dma-result-h {
display: flex; align-items: baseline; gap: 10px;
margin-bottom: 14px;
}
.dma-result-h .ti { font-size: 15px; font-weight: 600; color: var(--accent-black); }
.dma-result-h .sub { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; }
/* 批次卡 */
.dma-batch {
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
padding: 14px 16px;
margin-bottom: 14px;
}
.dma-batch-h {
display: flex; align-items: center; gap: 10px;
margin-bottom: 12px;
padding-bottom: 10px;
border-bottom: 1px solid var(--border-faint);
}
.dma-batch-h .pic {
width: 28px; height: 28px;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
display: grid; place-items: center;
color: var(--heat);
font-family: var(--font-mono); font-size: 11px; font-weight: 600;
}
.dma-batch-h .meta { flex: 1; min-width: 0; }
.dma-batch-h .nm { font-size: 13px; font-weight: 600; color: var(--accent-black); }
.dma-batch-h .info { margin-top: 2px; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; }
.dma-batch-h .info .sep { color: var(--black-alpha-24); }
.dma-batch-h .ops { display: flex; gap: 4px; }
.dma-batch-h .ops button {
width: 28px; height: 28px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
color: var(--black-alpha-56); cursor: pointer;
display: grid; place-items: center;
}
.dma-batch-h .ops button:hover { border-color: var(--heat-20); color: var(--heat); }
.dma-batch-h .ops button svg { width: 13px; height: 13px; }
.dma-batch-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }
@media (max-width: 1400px) { .dma-batch-grid { grid-template-columns: repeat(3, 1fr); } }
.dma-cell {
aspect-ratio: 3 / 4;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
overflow: hidden; position: relative;
cursor: pointer;
}
.dma-cell .ph {
position: absolute; inset: 0;
display: grid; place-items: center;
font-family: var(--font-mono); font-size: 11px;
color: var(--black-alpha-32);
background: repeating-linear-gradient(135deg, transparent 0 6px, rgba(0,0,0,.03) 6px 7px);
}
.dma-cell .tag {
position: absolute; top: 6px; left: 6px;
padding: 2px 6px;
background: rgba(0,0,0,.65);
color: #fff;
border-radius: var(--r-sm);
font-size: 10px; font-weight: 500;
backdrop-filter: blur(4px);
}
/* 空态 */
.dma-empty {
height: 100%;
display: flex; flex-direction: column; align-items: center; justify-content: center;
gap: 12px;
color: var(--black-alpha-48);
text-align: center;
}
.dma-empty .ic {
width: 56px; height: 56px;
background: var(--surface); border: 1px solid var(--border-faint);
border-radius: var(--r-md);
display: grid; place-items: center;
color: var(--heat);
}
.dma-empty .ic svg { width: 22px; height: 22px; }
.dma-empty .mono { font-family: var(--font-mono); font-size: 11px; letter-spacing: .04em; }
/* 顶部"提示条" · 这是 demo */
.dma-banner {
margin: 12px 28px 0;
padding: 8px 12px;
background: var(--heat-12);
border: 1px dashed var(--heat-20);
border-radius: var(--r-sm);
font-size: 12px;
color: var(--accent-black);
font-family: var(--font-mono); letter-spacing: .02em;
}
.dma-banner b { color: var(--heat); }
</style>
</head>
<body>
<div id="page">
<div class="dma-banner">// DEMO · 方案 A · <b>商品 = 项目空间</b>(Q1: A+B)。左栏仅商品空间:🔍 搜索 + 最近 6 条 + <b>全部商品</b>兜底入口;历史任务已挪进主区。主区:模特卡 + 张数 + 比例 + 立即生成,生成结果自动绑定到当前商品。</div>
<div class="dma">
<!-- ===== 左侧栏 · 仅商品空间 (搜索 + 列表 + 全部入口) ===== -->
<aside class="dma-side">
<div class="dma-side-h">
<div class="ti-row">
<span class="ti">商品空间</span>
<button class="add" type="button" title="新建商品">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
</button>
</div>
<!-- Q1-A · 搜索框 -->
<div class="dma-search">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
<input type="text" placeholder="搜索商品 / 分类">
</div>
</div>
<!-- 商品列表 · 最近 6 条 -->
<div class="dma-prod-list">
<div class="dma-prod active">
<div class="thumb">主图</div>
<div class="body">
<div class="nm">透真补水面膜</div>
<div class="sub">// 美妆个护 · 6 批</div>
</div>
</div>
<div class="dma-prod">
<div class="thumb">主图</div>
<div class="body">
<div class="nm">透真清透防晒霜</div>
<div class="sub">// 美妆个护 · 3 批</div>
</div>
</div>
<div class="dma-prod">
<div class="thumb">主图</div>
<div class="body">
<div class="nm">南卡 Lite Pro 蓝牙耳机</div>
<div class="sub">// 数码 3C · 2 批</div>
</div>
</div>
<div class="dma-prod">
<div class="thumb">主图</div>
<div class="body">
<div class="nm">滋啦速食牛肉面</div>
<div class="sub">// 食品饮料 · 1 批</div>
</div>
</div>
<div class="dma-prod">
<div class="thumb">主图</div>
<div class="body">
<div class="nm">三顿半同款冻干咖啡</div>
<div class="sub">// 食品饮料 · 1 批</div>
</div>
</div>
<div class="dma-prod">
<div class="thumb">主图</div>
<div class="body">
<div class="nm">小熊 4L 可视空气炸锅</div>
<div class="sub">// 家居家电 · 0 批</div>
</div>
</div>
</div>
<!-- Q1-B · 全部商品入口(贴底) -->
<button class="dma-all" type="button">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M3 9h18M9 4v16"/></svg>
全部商品
<span class="ct">24 个</span>
<svg style="margin-left:4px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
</button>
</aside>
<!-- ===== 右侧主区 ===== -->
<section class="dma-main">
<!-- 顶部 · 商品名 + 统计 -->
<div class="dma-main-h">
<div>
<div class="crumb">// 商品空间</div>
<h2>透真补水面膜</h2>
</div>
<div class="stats">
<span>本商品 <b>6</b></span>
<span class="sep">·</span>
<span>累计 <b>22</b> 张图</span>
<span class="sep">·</span>
<span>最近 <b>3 分钟前</b></span>
</div>
</div>
<!-- 中间:参数面板 + 结果 -->
<div class="dma-body">
<!-- 参数面板(单一职责:挑模特 + 张数) -->
<div class="dma-form">
<div class="dma-form-scroll">
<div class="dma-field">
<div class="dma-field-h">选择模特<span class="opt">(已锁定商品 · 透真补水面膜)</span></div>
<div class="dma-models">
<div class="dma-model selected">
<div class="ph">Ava · 3:4</div>
<div class="nm">Ava</div>
</div>
<div class="dma-model">
<div class="ph">Zoe · 3:4</div>
<div class="nm">Zoe</div>
</div>
<div class="dma-model">
<div class="ph">Ben · 3:4</div>
<div class="nm">Ben</div>
</div>
<div class="dma-model">
<div class="ph">Lin · 3:4</div>
<div class="nm">Lin</div>
</div>
<div class="dma-model">
<div class="ph">Mia · 3:4</div>
<div class="nm">Mia</div>
</div>
<div class="dma-model" style="border-style:dashed;display:flex;align-items:center;justify-content:center;color:var(--black-alpha-48);">
<span style="font-size:18px">+</span>
</div>
</div>
</div>
<div class="dma-field">
<div class="dma-field-h">生成张数</div>
<div class="dma-chip-row">
<button class="dma-chip" type="button">1 张</button>
<button class="dma-chip" type="button">2 张</button>
<button class="dma-chip active" type="button">4 张</button>
<button class="dma-chip" type="button">8 张</button>
</div>
</div>
<div class="dma-field">
<div class="dma-field-h">画面比例</div>
<div class="dma-chip-row">
<button class="dma-chip" type="button">1:1</button>
<button class="dma-chip active" type="button">3:4</button>
<button class="dma-chip" type="button">9:16</button>
<button class="dma-chip" type="button">16:9</button>
</div>
</div>
<div class="dma-field">
<div class="dma-field-h">补充提示词<span class="opt">(选填)</span></div>
<textarea style="width:100%;min-height:60px;padding:8px 10px;background:var(--background-lighter);border:1px solid var(--black-alpha-12);border-radius:var(--r-sm);font-family:inherit;font-size:12.5px;outline:none;resize:vertical" placeholder="例:户外阳光、敷面膜的特写、白底产品摄影"></textarea>
</div>
</div>
<div class="dma-form-cta">
<div class="dma-cost">
<span>预估扣费 <span class="v">≈ ¥1.20</span></span>
<span>余额 ¥327.40</span>
</div>
<button class="dma-gen" type="button">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" 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>
立即生成 · 透真补水面膜 × Ava
</button>
</div>
</div>
<!-- 右侧结果 · 当前商品全部批次 -->
<div class="dma-result">
<div class="dma-result-h">
<span class="ti">最近批次 · Ava × 4 张</span>
<span class="sub">// 3 分钟前 · 已完成</span>
</div>
<!-- 批次 1 -->
<div class="dma-batch">
<div class="dma-batch-h">
<div class="pic">4×</div>
<div class="meta">
<div class="nm">Ava × 4 张</div>
<div class="info">透真补水面膜 <span class="sep">·</span> 3:4 <span class="sep">·</span> 3 分钟前 <span class="sep">·</span> ¥1.20</div>
</div>
<div class="ops">
<button 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="M21 12a9 9 0 11-3-6.7L21 8M21 3v5h-5"/></svg></button>
<button 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="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg></button>
<button 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="M19 21l-7-5-7 5V5a2 2 0 012-2h10a2 2 0 012 2z"/></svg></button>
</div>
</div>
<div class="dma-batch-grid">
<div class="dma-cell"><div class="ph">Ava · #1</div><span class="tag">3:4</span></div>
<div class="dma-cell"><div class="ph">Ava · #2</div><span class="tag">3:4</span></div>
<div class="dma-cell"><div class="ph">Ava · #3</div><span class="tag">3:4</span></div>
<div class="dma-cell"><div class="ph">Ava · #4</div><span class="tag">3:4</span></div>
</div>
</div>
<!-- 批次 2 -->
<div class="dma-batch">
<div class="dma-batch-h">
<div class="pic">4×</div>
<div class="meta">
<div class="nm">Zoe × 4 张</div>
<div class="info">透真补水面膜 <span class="sep">·</span> 3:4 <span class="sep">·</span> 12 分钟前 <span class="sep">·</span> ¥1.20</div>
</div>
<div class="ops">
<button 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="M21 12a9 9 0 11-3-6.7L21 8M21 3v5h-5"/></svg></button>
<button 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="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg></button>
<button 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="M19 21l-7-5-7 5V5a2 2 0 012-2h10a2 2 0 012 2z"/></svg></button>
</div>
</div>
<div class="dma-batch-grid">
<div class="dma-cell"><div class="ph">Zoe · #1</div><span class="tag">3:4</span></div>
<div class="dma-cell"><div class="ph">Zoe · #2</div><span class="tag">3:4</span></div>
<div class="dma-cell"><div class="ph">Zoe · #3</div><span class="tag">3:4</span></div>
<div class="dma-cell"><div class="ph">Zoe · #4</div><span class="tag">3:4</span></div>
</div>
</div>
<!-- 批次 3 -->
<div class="dma-batch">
<div class="dma-batch-h">
<div class="pic">2×</div>
<div class="meta">
<div class="nm">Ben × 2 张</div>
<div class="info">透真补水面膜 <span class="sep">·</span> 3:4 <span class="sep">·</span> 刚刚 <span class="sep">·</span> 生成中</div>
</div>
<div class="ops">
<button 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="M6 6l12 12M6 18L18 6"/></svg></button>
</div>
</div>
<div class="dma-batch-grid">
<div class="dma-cell"><div class="ph">生成中…</div></div>
<div class="dma-cell"><div class="ph">生成中…</div></div>
</div>
</div>
</div>
</div>
</section>
</div>
</div>
<script src="assets/shell.js?v=202605211643"></script>
<script>
Shell.render({
active: 'asset-factory',
crumbs: [
{ label: '工作台', href: 'index.html' },
{ label: '图片生成', href: 'asset-factory.html' },
{ label: '模特上身图 · 方案 A · Demo' }
]
});
// 简单交互:点击商品 / 任务切换激活态
document.querySelectorAll('.dma-prod').forEach(el => {
el.addEventListener('click', () => {
document.querySelectorAll('.dma-prod').forEach(x => x.classList.remove('active'));
el.classList.add('active');
});
});
document.querySelectorAll('.dma-task').forEach(el => {
el.addEventListener('click', () => {
document.querySelectorAll('.dma-task').forEach(x => x.classList.remove('active'));
el.classList.add('active');
});
});
document.querySelectorAll('.dma-model').forEach(el => {
el.addEventListener('click', () => {
document.querySelectorAll('.dma-model').forEach(x => x.classList.remove('selected'));
el.classList.add('selected');
});
});
document.querySelectorAll('.dma-chip').forEach(el => {
el.addEventListener('click', () => {
// 同组互斥
el.parentElement.querySelectorAll('.dma-chip').forEach(x => x.classList.remove('active'));
el.classList.add('active');
});
});
</script>
</body>
</html>

View File

@ -0,0 +1,655 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>方案 A · v2 · 模特上身图 · 流·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>
.app { height: 100vh; overflow: hidden; }
main { display: flex; flex-direction: column; min-height: 0; }
#page-content { flex: 1; min-height: 0; display: flex; flex-direction: column; padding: 0; }
/* ===== 两栏:左栏(纯商品空间) + 主区 ===== */
.dmb {
flex: 1; min-height: 0;
display: grid;
grid-template-columns: 260px 1fr;
}
/* ── 左栏 · 商品空间 ── */
.dmb-side {
border-right: 1px solid var(--border-faint);
background: var(--surface);
display: flex; flex-direction: column;
min-height: 0;
}
.dmb-side-h {
padding: 14px 16px 10px;
flex-shrink: 0;
}
.dmb-side-h .ti {
font-size: 11px; font-family: var(--font-mono);
color: var(--black-alpha-48); letter-spacing: .08em;
text-transform: uppercase;
margin-bottom: 10px;
}
/* 搜索框 */
.dmb-search {
display: flex; align-items: center; gap: 8px;
height: 32px; padding: 0 10px;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
transition: border-color var(--t-base);
}
.dmb-search:focus-within { border-color: var(--heat-40); background: var(--surface); }
.dmb-search svg { width: 13px; height: 13px; color: var(--black-alpha-48); flex-shrink: 0; }
.dmb-search input {
flex: 1; min-width: 0; height: 100%;
border: 0; outline: 0; background: transparent;
font-size: 12.5px; color: var(--accent-black); font-family: inherit;
}
/* 商品列表 */
.dmb-list {
flex: 1; min-height: 0;
overflow-y: auto;
padding: 8px 10px 8px;
display: flex; flex-direction: column; gap: 6px;
}
.dmb-prod {
display: flex; align-items: center; gap: 10px;
padding: 8px;
border: 1px solid transparent;
border-radius: var(--r-sm);
cursor: pointer;
transition: background var(--t-base), border-color var(--t-base);
}
.dmb-prod:hover { background: var(--background-lighter); }
.dmb-prod.active { background: var(--heat-12); border-color: var(--heat-20); }
.dmb-prod .thumb {
flex-shrink: 0;
width: 44px; height: 44px;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
overflow: hidden; position: relative;
display: grid; place-items: center;
font-family: var(--font-mono); font-size: 9px;
color: var(--black-alpha-32);
background: repeating-linear-gradient(135deg, transparent 0 4px, rgba(0,0,0,.04) 4px 5px);
}
.dmb-prod.active .thumb { border-color: var(--heat); }
.dmb-prod .body { flex: 1; min-width: 0; }
.dmb-prod .nm {
font-size: 12.5px;
color: var(--accent-black); font-weight: 500;
line-height: 1.3;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.dmb-prod.active .nm { color: var(--heat); font-weight: 600; }
.dmb-prod .sub {
margin-top: 2px;
font-family: var(--font-mono); font-size: 10px;
color: var(--black-alpha-48); letter-spacing: .02em;
}
/* 底部 · 全部商品入口 */
.dmb-all {
flex-shrink: 0;
margin: 0 10px 12px;
padding: 10px 12px;
background: var(--background-lighter);
border: 1px dashed var(--border-faint);
border-radius: var(--r-sm);
color: var(--black-alpha-72);
font-size: 12px;
cursor: pointer;
display: flex; align-items: center; gap: 6px;
transition: border-color var(--t-base), color var(--t-base), background var(--t-base);
}
.dmb-all:hover { border-color: var(--heat); color: var(--heat); background: var(--heat-12); }
.dmb-all .ct { color: var(--black-alpha-48); font-family: var(--font-mono); font-size: 10.5px; margin-left: auto; }
.dmb-all svg { width: 12px; height: 12px; }
/* ── 主区 ── */
.dmb-main {
display: flex; flex-direction: column;
min-height: 0; overflow: hidden;
position: relative;
}
.dmb-main-h {
flex-shrink: 0;
padding: 16px 28px 12px;
border-bottom: 1px solid var(--border-faint);
background: var(--surface);
}
.dmb-main-h .crumb {
font-family: var(--font-mono); font-size: 10.5px;
color: var(--black-alpha-48); letter-spacing: .04em;
margin-bottom: 4px;
}
.dmb-main-h h2 {
font-size: 22px; font-weight: 600;
letter-spacing: -.015em;
color: var(--accent-black);
}
.dmb-main-h .row {
display: flex; align-items: center; gap: 16px;
margin-top: 6px;
}
.dmb-main-h .stats {
font-family: var(--font-mono); font-size: 11px;
color: var(--black-alpha-48); letter-spacing: .02em;
display: flex; gap: 4px;
}
.dmb-main-h .stats b { color: var(--accent-black); font-weight: 600; }
.dmb-main-h .stats .sep { color: var(--black-alpha-24); }
.dmb-main-h .spacer { flex: 1; }
/* 主区 toolbar (筛选条 · 仿 image-optimize 顶部) */
.dmb-main-tb { display: flex; gap: 8px; align-items: center; }
.dmb-main-tb .icbtn {
width: 30px; height: 30px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
color: var(--black-alpha-72);
cursor: pointer; display: grid; place-items: center;
}
.dmb-main-tb .icbtn:hover { border-color: var(--heat-20); color: var(--heat); }
.dmb-main-tb .icbtn svg { width: 13px; height: 13px; }
.dmb-main-tb .chip {
display: inline-flex; align-items: center; gap: 6px;
height: 30px; padding: 0 10px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
font-size: 12px; color: var(--black-alpha-72);
font-family: inherit; cursor: pointer;
}
.dmb-main-tb .chip:hover { border-color: var(--heat-20); color: var(--heat); }
.dmb-main-tb .chip svg { width: 10px; height: 10px; opacity: .6; }
/* 主区滚动体 · 任务流 */
.dmb-stream {
flex: 1; min-height: 0;
overflow-y: auto;
padding: 22px 28px 200px; /* 底部留出参数面板高度 */
background: var(--background-base);
}
.dmb-day-h {
display: flex; align-items: baseline; gap: 8px;
margin: 6px 0 10px;
font-family: var(--font-mono); font-size: 10.5px;
color: var(--black-alpha-48); letter-spacing: .06em;
text-transform: uppercase;
}
.dmb-day-h::before {
content: ''; width: 14px; height: 1px;
background: var(--black-alpha-24);
display: inline-block; margin-right: 2px;
}
.dmb-day-h .ct {
color: var(--black-alpha-72); font-weight: 500;
margin-left: auto;
text-transform: none; letter-spacing: 0;
}
/* 批次卡 */
.dmb-batch {
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
padding: 14px 16px;
margin-bottom: 14px;
}
.dmb-batch-h {
display: flex; align-items: center; gap: 10px;
margin-bottom: 12px;
}
.dmb-batch-h .pic {
flex-shrink: 0;
width: 30px; height: 30px;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
display: grid; place-items: center;
color: var(--heat);
font-family: var(--font-mono); font-size: 11px; font-weight: 600;
}
.dmb-batch-h .meta { flex: 1; min-width: 0; }
.dmb-batch-h .nm { font-size: 13.5px; font-weight: 600; color: var(--accent-black); }
.dmb-batch-h .info {
margin-top: 2px;
display: flex; flex-wrap: wrap; gap: 4px;
font-family: var(--font-mono); font-size: 10.5px;
color: var(--black-alpha-48); letter-spacing: .02em;
}
.dmb-batch-h .info .sep { color: var(--black-alpha-24); }
.dmb-batch-h .ops { display: flex; gap: 4px; }
.dmb-batch-h .ops button {
width: 28px; height: 28px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
color: var(--black-alpha-56); cursor: pointer;
display: grid; place-items: center;
transition: border-color var(--t-base), color var(--t-base);
}
.dmb-batch-h .ops button:hover { border-color: var(--heat-20); color: var(--heat); }
.dmb-batch-h .ops button svg { width: 13px; height: 13px; }
/* 状态 pill 紧贴标题右侧 */
.dmb-batch-h .stat-pill {
margin-left: 8px;
padding: 2px 7px;
font-size: 10px;
}
.dmb-batch-h .stat-pill .dot { width: 4px; height: 4px; }
.dmb-batch-grid {
display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px;
}
@media (max-width: 1400px) { .dmb-batch-grid { grid-template-columns: repeat(3, 1fr); } }
.dmb-cell {
aspect-ratio: 3 / 4;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
overflow: hidden; position: relative;
cursor: pointer;
}
.dmb-cell .ph {
position: absolute; inset: 0;
display: grid; place-items: center;
font-family: var(--font-mono); font-size: 10.5px;
color: var(--black-alpha-32);
background: repeating-linear-gradient(135deg, transparent 0 6px, rgba(0,0,0,.03) 6px 7px);
}
.dmb-cell.gen .ph { animation: dmb-pulse 1.4s ease-in-out infinite; }
@keyframes dmb-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .55; } }
.dmb-cell.err { border-color: var(--accent-crimson, #c43d3d); }
.dmb-cell.err .ph { color: var(--accent-crimson, #c43d3d); background: rgba(196,61,61,.05); }
.dmb-cell .tag {
position: absolute; top: 6px; left: 6px;
padding: 2px 6px;
background: rgba(0,0,0,.65); color: #fff;
border-radius: var(--r-sm);
font-size: 10px; font-weight: 500; backdrop-filter: blur(4px);
}
/* ── 底部 fixed 参数面板 ── */
.dmb-param-wrap {
position: absolute; left: 0; right: 0; bottom: 0;
padding: 14px 28px 22px;
background: linear-gradient(to bottom, transparent 0, var(--background-base) 24px);
z-index: 5;
}
.dmb-param {
max-width: 1180px; margin: 0 auto;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: 14px;
padding: 10px 14px;
display: flex; align-items: center; gap: 10px;
box-shadow: 0 6px 24px rgba(0,0,0,.06);
}
.dmb-param .label-mono {
font-family: var(--font-mono); font-size: 10.5px;
color: var(--black-alpha-48); letter-spacing: .04em;
flex-shrink: 0;
}
.dmb-param .pchip {
display: inline-flex; align-items: center; gap: 6px;
height: 30px; padding: 0 12px;
background: var(--background-lighter);
border: 1px solid transparent;
border-radius: var(--r-pill);
font-size: 12px; color: var(--black-alpha-72);
cursor: pointer; font-family: inherit;
transition: border-color var(--t-base), background var(--t-base), color var(--t-base);
}
.dmb-param .pchip:hover { background: var(--surface); border-color: var(--border-faint); }
.dmb-param .pchip.active { background: var(--heat-12); color: var(--heat); }
.dmb-param .pchip svg { width: 10px; height: 10px; opacity: .6; }
.dmb-param .pchip .lbl-mono {
font-family: var(--font-mono); font-size: 10px;
color: var(--black-alpha-48); letter-spacing: .02em;
}
.dmb-param .pchip.active .lbl-mono { color: var(--heat); }
.dmb-param .spacer { flex: 1; }
.dmb-param .meta-right {
font-family: var(--font-mono); font-size: 10.5px;
color: var(--black-alpha-48); letter-spacing: .02em;
margin-right: 6px;
text-align: right;
}
.dmb-param .meta-right .v { color: var(--accent-black); font-weight: 600; }
.dmb-param .gen-btn {
height: 38px; padding: 0 20px;
background: var(--heat); color: #fff;
border: 0; border-radius: var(--r-md);
font-size: 13.5px; font-weight: 600; font-family: inherit;
cursor: pointer;
display: inline-flex; align-items: center; gap: 8px;
box-shadow: var(--shadow-cta);
}
.dmb-param .gen-btn svg { width: 14px; height: 14px; }
/* 顶部 demo 提示条 */
.dmb-banner {
margin: 12px 28px 0;
padding: 8px 12px;
background: var(--heat-12);
border: 1px dashed var(--heat-20);
border-radius: var(--r-sm);
font-size: 12px; color: var(--accent-black);
font-family: var(--font-mono); letter-spacing: .02em;
}
.dmb-banner b { color: var(--heat); }
</style>
</head>
<body>
<div id="page">
<div class="dmb-banner">// DEMO v2 · 方案 A · <b>商品空间(A+B) + 任务流主区</b>。左栏只保留商品空间(搜索+最近6条+全部入口),任务列表搬到主区,筛选放主区顶部 toolbar,参数面板底部 fixed 化(类 image-optimize)。</div>
<div class="dmb">
<!-- ===== 左栏 · 商品空间 ===== -->
<aside class="dmb-side">
<div class="dmb-side-h">
<div class="ti">商品空间</div>
<div class="dmb-search">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
<input type="text" placeholder="搜索商品 / 分类">
</div>
</div>
<div class="dmb-list">
<div class="dmb-prod active">
<div class="thumb">主图</div>
<div class="body">
<div class="nm">透真补水面膜</div>
<div class="sub">美妆个护 · 6 批</div>
</div>
</div>
<div class="dmb-prod">
<div class="thumb">主图</div>
<div class="body">
<div class="nm">透真清透防晒霜</div>
<div class="sub">美妆个护 · 3 批</div>
</div>
</div>
<div class="dmb-prod">
<div class="thumb">主图</div>
<div class="body">
<div class="nm">南卡 Lite Pro 蓝牙耳机</div>
<div class="sub">数码 3C · 2 批</div>
</div>
</div>
<div class="dmb-prod">
<div class="thumb">主图</div>
<div class="body">
<div class="nm">滋啦速食牛肉面</div>
<div class="sub">食品饮料 · 1 批</div>
</div>
</div>
<div class="dmb-prod">
<div class="thumb">主图</div>
<div class="body">
<div class="nm">三顿半同款冻干咖啡</div>
<div class="sub">食品饮料 · 1 批</div>
</div>
</div>
<div class="dmb-prod">
<div class="thumb">主图</div>
<div class="body">
<div class="nm">小熊 4L 可视空气炸锅</div>
<div class="sub">家居家电 · 0 批</div>
</div>
</div>
</div>
<button class="dmb-all" type="button">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M3 9h18M9 4v16"/></svg>
全部商品
<span class="ct">24 个</span>
<svg style="margin-left:4px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
</button>
</aside>
<!-- ===== 主区 ===== -->
<section class="dmb-main">
<!-- 顶部 标题 + stats + toolbar -->
<div class="dmb-main-h">
<div class="crumb">// 商品空间 · 模特上身图</div>
<h2>透真补水面膜</h2>
<div class="row">
<div class="stats">
<span>美妆个护</span><span class="sep">·</span>
<span>本商品 <b>6</b></span><span class="sep">·</span>
<span>累计 <b>22</b> 张图</span><span class="sep">·</span>
<span>最近 <b>3 分钟前</b></span>
</div>
<div class="spacer"></div>
<div class="dmb-main-tb">
<button class="icbtn" type="button" title="搜索批次">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
</button>
<button class="chip" type="button">时间 <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg></button>
<button class="chip" type="button">状态 <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg></button>
<button class="chip" type="button">模特 <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg></button>
</div>
</div>
</div>
<!-- 任务流 -->
<div class="dmb-stream">
<div class="dmb-day-h">
<span>今天</span>
<span class="ct">3 批 · 10 张</span>
</div>
<!-- 批次 1 -->
<div class="dmb-batch">
<div class="dmb-batch-h">
<div class="pic">4×</div>
<div class="meta">
<div class="nm">Ava × 4 张 <span class="pill ok stat-pill"><span class="dot"></span>已完成</span></div>
<div class="info">
<span>3:4</span><span class="sep">·</span>
<span>3 分钟前</span><span class="sep">·</span>
<span>¥1.20</span>
</div>
</div>
<div class="ops">
<button 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="M21 12a9 9 0 11-3-6.7L21 8M21 3v5h-5"/></svg></button>
<button 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="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg></button>
<button 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="M19 21l-7-5-7 5V5a2 2 0 012-2h10a2 2 0 012 2z"/></svg></button>
</div>
</div>
<div class="dmb-batch-grid">
<div class="dmb-cell"><div class="ph">Ava · #1</div><span class="tag">3:4</span></div>
<div class="dmb-cell"><div class="ph">Ava · #2</div><span class="tag">3:4</span></div>
<div class="dmb-cell"><div class="ph">Ava · #3</div><span class="tag">3:4</span></div>
<div class="dmb-cell"><div class="ph">Ava · #4</div><span class="tag">3:4</span></div>
</div>
</div>
<!-- 批次 2 -->
<div class="dmb-batch">
<div class="dmb-batch-h">
<div class="pic">4×</div>
<div class="meta">
<div class="nm">Zoe × 4 张 <span class="pill ok stat-pill"><span class="dot"></span>已完成</span></div>
<div class="info">
<span>3:4</span><span class="sep">·</span>
<span>12 分钟前</span><span class="sep">·</span>
<span>¥1.20</span>
</div>
</div>
<div class="ops">
<button 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="M21 12a9 9 0 11-3-6.7L21 8M21 3v5h-5"/></svg></button>
<button 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="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg></button>
<button 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="M19 21l-7-5-7 5V5a2 2 0 012-2h10a2 2 0 012 2z"/></svg></button>
</div>
</div>
<div class="dmb-batch-grid">
<div class="dmb-cell"><div class="ph">Zoe · #1</div><span class="tag">3:4</span></div>
<div class="dmb-cell"><div class="ph">Zoe · #2</div><span class="tag">3:4</span></div>
<div class="dmb-cell"><div class="ph">Zoe · #3</div><span class="tag">3:4</span></div>
<div class="dmb-cell"><div class="ph">Zoe · #4</div><span class="tag">3:4</span></div>
</div>
</div>
<!-- 批次 3 · 生成中 -->
<div class="dmb-batch">
<div class="dmb-batch-h">
<div class="pic">2×</div>
<div class="meta">
<div class="nm">Ben × 2 张 <span class="pill info stat-pill"><span class="dot"></span>生成中</span></div>
<div class="info">
<span>3:4</span><span class="sep">·</span>
<span>刚刚</span><span class="sep">·</span>
<span>¥0.60</span>
</div>
</div>
<div class="ops">
<button 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="M6 6l12 12M6 18L18 6"/></svg></button>
</div>
</div>
<div class="dmb-batch-grid">
<div class="dmb-cell gen"><div class="ph">生成中…</div></div>
<div class="dmb-cell gen"><div class="ph">生成中…</div></div>
</div>
</div>
<!-- 昨天 -->
<div class="dmb-day-h">
<span>昨天</span>
<span class="ct">2 批 · 8 张</span>
</div>
<div class="dmb-batch">
<div class="dmb-batch-h">
<div class="pic">4×</div>
<div class="meta">
<div class="nm">Lin × 4 张 <span class="pill ok stat-pill"><span class="dot"></span>已完成</span></div>
<div class="info">
<span>3:4</span><span class="sep">·</span>
<span>昨天 18:24</span><span class="sep">·</span>
<span>¥1.20</span>
</div>
</div>
<div class="ops">
<button 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="M21 12a9 9 0 11-3-6.7L21 8M21 3v5h-5"/></svg></button>
<button 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="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg></button>
<button 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="M19 21l-7-5-7 5V5a2 2 0 012-2h10a2 2 0 012 2z"/></svg></button>
</div>
</div>
<div class="dmb-batch-grid">
<div class="dmb-cell"><div class="ph">Lin · #1</div><span class="tag">3:4</span></div>
<div class="dmb-cell"><div class="ph">Lin · #2</div><span class="tag">3:4</span></div>
<div class="dmb-cell"><div class="ph">Lin · #3</div><span class="tag">3:4</span></div>
<div class="dmb-cell"><div class="ph">Lin · #4</div><span class="tag">3:4</span></div>
</div>
</div>
<!-- 更早 -->
<div class="dmb-day-h">
<span>更早</span>
<span class="ct">1 批 · 2 张 · 含 1 失败</span>
</div>
<div class="dmb-batch">
<div class="dmb-batch-h">
<div class="pic">2×</div>
<div class="meta">
<div class="nm">Ava × 2 张 <span class="pill err stat-pill"><span class="dot"></span>失败</span></div>
<div class="info">
<span>3:4</span><span class="sep">·</span>
<span>2 天前</span>
</div>
</div>
<div class="ops">
<button 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="M21 12a9 9 0 11-3-6.7L21 8M21 3v5h-5"/></svg></button>
<button 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"/></svg></button>
</div>
</div>
<div class="dmb-batch-grid">
<div class="dmb-cell err"><div class="ph">失败 · 点重跑</div></div>
<div class="dmb-cell err"><div class="ph">失败 · 点重跑</div></div>
</div>
</div>
</div>
<!-- 底部 fixed 参数面板 -->
<div class="dmb-param-wrap">
<div class="dmb-param">
<button class="pchip active" type="button">
<span class="lbl-mono">模特</span>
<span>Ava</span>
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg>
</button>
<button class="pchip" type="button">
<span class="lbl-mono">张数</span>
<span>4</span>
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg>
</button>
<button class="pchip" type="button">
<span class="lbl-mono">比例</span>
<span>3:4</span>
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg>
</button>
<button class="pchip" type="button">
<span class="lbl-mono">补充提示词</span>
<span style="color:var(--black-alpha-48)">+ 添加</span>
</button>
<span class="spacer"></span>
<span class="meta-right">预估 <span class="v">¥1.20</span> · 余额 <span class="v">¥327.40</span></span>
<button class="gen-btn" type="button">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" 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>
生成 · 透真补水面膜 × Ava
</button>
</div>
</div>
</section>
</div>
</div>
<script src="assets/shell.js?v=202605211643"></script>
<script>
Shell.render({
active: 'asset-factory',
crumbs: [
{ label: '工作台', href: 'index.html' },
{ label: '图片生成', href: 'asset-factory.html' },
{ label: '模特上身图 · 方案 A · v2' }
]
});
// 商品 / chip 切换
document.querySelectorAll('.dmb-prod').forEach(el => {
el.addEventListener('click', () => {
document.querySelectorAll('.dmb-prod').forEach(x => x.classList.remove('active'));
el.classList.add('active');
});
});
document.querySelectorAll('.dmb-param .pchip').forEach(el => {
el.addEventListener('click', () => {
// demo:只切自己 active(不互斥,每个 chip 都可独立切下拉)
el.classList.toggle('active');
});
});
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -62,7 +62,49 @@
.ai-avatar { width: 26px; height: 26px; background: var(--heat); color: var(--accent-white); display: grid; place-items: center; font-size: 11px; font-weight: 700; border: 1px solid var(--heat); border-radius: 50%; }
.del { text-decoration: line-through; color: var(--black-alpha-48); }
.ins { background: var(--forest-bg); color: var(--accent-forest); padding: 0 3px; }
.chat-input { padding: 14px 18px; border-top: 1px solid var(--border-faint); }
.chat-input { padding: 14px 18px 18px; border-top: 1px solid var(--border-faint); }
.chat-input-card {
background: var(--background-base);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
padding: 12px 14px 10px;
transition: border-color var(--t-base), box-shadow var(--t-base);
}
.chat-input-card:focus-within { border-color: var(--accent-black); box-shadow: 0 0 0 3px rgba(0,0,0,.04); }
.chat-input-area {
width: 100%; border: none; outline: none; background: transparent;
font-family: var(--font-sans); font-size: 13px; color: var(--accent-black);
line-height: 1.55; resize: none; padding: 0; min-height: 42px;
}
.chat-input-area::placeholder { color: var(--black-alpha-40); }
.chat-input-foot { display: flex; align-items: center; gap: 8px; margin-top: 10px; }
.chat-input-foot .hint { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-40); letter-spacing: .02em; }
.chat-input-foot .spacer { flex: 1; }
.chat-icon-btn {
width: 28px; height: 28px; display: grid; place-items: center;
background: transparent; border: 1px solid var(--border-faint);
border-radius: 50%; color: var(--black-alpha-56); cursor: pointer;
transition: border-color var(--t-base), color var(--t-base), background var(--t-base);
}
.chat-icon-btn:hover { border-color: var(--accent-black); color: var(--accent-black); }
.chat-send-btn {
width: 32px; height: 32px; display: grid; place-items: center;
background: var(--accent-black); border: 1px solid var(--accent-black);
border-radius: 50%; color: var(--accent-white); cursor: pointer;
transition: background var(--t-base), border-color var(--t-base), transform var(--t-base);
}
.chat-send-btn:hover { background: var(--heat); border-color: var(--heat); }
.chat-send-btn:active { transform: scale(.95); }
.chat-send-btn:disabled { background: var(--black-alpha-12); border-color: var(--black-alpha-12); color: var(--black-alpha-40); cursor: not-allowed; transform: none; }
.chat-attach-row { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; }
.chat-attach-chip {
display: inline-flex; align-items: center; gap: 6px;
padding: 3px 6px 3px 8px; background: var(--surface);
border: 1px solid var(--border-faint); border-radius: var(--r-sm);
font-family: var(--font-mono); font-size: 11px; color: var(--accent-black);
}
.chat-attach-chip .x { width: 14px; height: 14px; display: grid; place-items: center; background: transparent; border: none; color: var(--black-alpha-48); cursor: pointer; border-radius: 50%; }
.chat-attach-chip .x:hover { background: var(--black-alpha-08); color: var(--accent-black); }
.shot-list { display: flex; flex-direction: column; }
.shots-body { padding: 12px 16px; flex: 1; overflow-y: auto; max-height: 540px; display: flex; flex-direction: column; gap: 0; }
@ -193,20 +235,195 @@
display: grid; place-items: center; overflow: hidden;
}
.prod-preview-history .h-thumb:hover { border-color: var(--heat-40); }
.prod-preview-history .h-thumb.active { border-color: var(--heat); border-width: 2px; box-shadow: 0 0 0 2px var(--heat-12); }
/* 已采用版本:主橙描边 + 「已采用」徽标 */
.prod-preview-history .h-thumb.adopted { border-color: var(--heat); border-width: 2px; box-shadow: 0 0 0 2px var(--heat-12); }
/* 仅预览(未采用):黑色描边,无徽标 */
.prod-preview-history .h-thumb.previewing { border-color: var(--accent-black); border-width: 2px; }
.prod-preview-history .h-thumb .v { font-family: var(--font-mono); font-size: 10px; color: var(--black-alpha-56); letter-spacing: .02em; }
.prod-preview-history .h-thumb.active .v { color: var(--heat); font-weight: 600; }
.prod-preview-history .h-thumb.adopted .v { color: var(--heat); font-weight: 600; }
.prod-preview-history .h-thumb.previewing .v { color: var(--accent-black); font-weight: 600; }
.prod-preview-history .h-thumb .badge { position: absolute; top: 2px; left: 2px; font-family: var(--font-mono); font-size: 8.5px; padding: 0 4px; line-height: 12px; background: var(--heat); color: var(--accent-white); border-radius: 2px; letter-spacing: .02em; display: none; }
.prod-preview-history .h-thumb.active .badge { display: block; }
.asset-card-2 { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); cursor: pointer; transition: border-color var(--t-base), box-shadow var(--t-base); }
.prod-preview-history .h-thumb.adopted .badge { display: block; }
/* 「已采用」状态 · 浅橙 + 主橙文字,与已采用徽标视觉呼应 */
#prod-preview-adopt:disabled,
#prod-preview-adopt:disabled:hover {
color: var(--heat);
border-color: var(--heat-40);
background: var(--heat-12);
cursor: not-allowed;
opacity: 1;
}
/* 主图可点击放大 */
.prod-preview-img.is-zoomable { cursor: zoom-in; transition: border-color var(--t-base); position: relative; }
.prod-preview-img.is-zoomable:hover { border-color: var(--heat-40); }
.prod-preview-img.is-zoomable::after {
content: '';
position: absolute; top: 8px; right: 8px;
width: 22px; height: 22px;
background: rgba(21,20,15,.72) url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><circle cx='11' cy='11' r='7'/><path d='M21 21l-4.3-4.3M8 11h6M11 8v6'/></svg>") center/14px no-repeat;
border-radius: var(--r-sm);
opacity: 0; transition: opacity var(--t-base);
pointer-events: none;
}
.prod-preview-img.is-zoomable:hover::after { opacity: 1; }
/* 三视图放大查看 lightbox */
#tri-lightbox-bg { z-index: 80; }
#tri-lightbox-bg .tri-lightbox {
position: relative;
width: min(1100px, 92vw);
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
padding: 18px 20px 20px;
display: flex; flex-direction: column; gap: 12px;
box-shadow: 0 24px 64px rgba(0,0,0,.24);
}
.tri-lightbox-head {
display: flex; align-items: center; gap: 8px;
font-family: var(--font-mono);
font-size: 11px; letter-spacing: .04em; text-transform: uppercase;
color: var(--black-alpha-56);
padding-right: 32px;
}
.tri-lightbox-head .lb-ver { color: var(--heat); font-weight: 600; }
.tri-lightbox-head .lb-tag {
margin-left: 6px;
padding: 2px 6px;
background: var(--heat-12); color: var(--heat);
border-radius: 3px;
font-size: 10px;
}
.tri-lightbox-close {
position: absolute;
top: 12px; right: 12px;
width: 28px; height: 28px;
display: grid; place-items: center;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
color: var(--black-alpha-56);
cursor: pointer;
transition: background var(--t-base), color var(--t-base), border-color var(--t-base);
z-index: 2;
}
.tri-lightbox-close:hover { background: var(--black-alpha-08); color: var(--accent-black); border-color: var(--black-alpha-12); }
.tri-lightbox-close svg { width: 14px; height: 14px; }
.tri-lightbox-img { aspect-ratio: 16/9; width: 100%; }
.tri-lightbox-foot { display: flex; align-items: center; gap: 8px; font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); }
.tri-lightbox-foot .spc { flex: 1; }
.tri-lightbox-foot kbd {
display: inline-block;
padding: 1px 5px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-bottom-width: 2px;
border-radius: 3px;
font-family: var(--font-mono);
font-size: 10px;
color: var(--black-alpha-72);
}
.asset-card-2 { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); cursor: pointer; transition: border-color var(--t-base), box-shadow var(--t-base); overflow: hidden; display: flex; flex-direction: column; }
.asset-card-2:hover { border-color: var(--heat-40); box-shadow: 0 1px 3px rgba(0,0,0,.04); }
.asset-card-2 .thumb-2 { aspect-ratio: 1; }
.asset-card-2 .body-2 { padding: 12px 14px; }
.asset-card-2 .body-2 .btn-apply { background: var(--heat); color: var(--accent-white); border: 1px solid var(--heat); }
.asset-card-2 .body-2 .btn-apply:hover { box-shadow: var(--shadow-cta-hover); }
/* stage2 商品卡 · 与商品库 .product-card 视觉一致 */
.asset-card-2.prod-lib-card:hover { background: var(--background-lighter); border-color: var(--black-alpha-48); }
.asset-card-2.prod-lib-card .prod-thumb { aspect-ratio: 1.4 / 1; position: relative; }
.asset-card-2.prod-lib-card .prod-body { padding: 14px 14px 12px; flex: 1; }
.asset-card-2.prod-lib-card .prod-name {
font-size: 14px; font-weight: 600;
color: var(--accent-black);
line-height: 1.3;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.asset-card-2.prod-lib-card .prod-cat {
display: inline-flex; align-items: center;
margin-top: 8px;
padding: 2px 8px;
background: var(--background-lighter);
color: var(--black-alpha-72);
border-radius: var(--r-sm);
font-size: 11.5px;
}
.asset-card-2.prod-lib-card .prod-date {
font-family: var(--font-mono);
font-size: 11px;
color: var(--black-alpha-48);
margin-top: 10px;
letter-spacing: .02em;
}
.asset-card-2.prod-lib-card .prod-footer {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
column-gap: 8px;
padding: 10px 12px;
border-top: 1px solid var(--border-faint);
font-size: 11.5px;
color: var(--black-alpha-56);
background: var(--background-base);
}
.asset-card-2.prod-lib-card .prod-footer .stat {
display: inline-flex; align-items: center; justify-content: center; gap: 5px;
padding: 3px 8px;
border-radius: var(--r-sm);
font-family: var(--font-mono);
letter-spacing: .02em;
white-space: nowrap;
justify-self: center;
}
.asset-card-2.prod-lib-card .prod-footer .stat svg { width: 13px; height: 13px; color: var(--black-alpha-48); flex-shrink: 0; }
.asset-card-2.prod-lib-card .prod-footer .stat b { color: var(--accent-black); font-weight: 600; }
.asset-card-2.prod-lib-card .prod-footer .sep { color: var(--black-alpha-24); font-family: var(--font-mono); flex-shrink: 0; }
.asset-card-2.prod-lib-card .prod-action {
padding: 10px 12px;
border-top: 1px solid var(--border-faint);
background: var(--surface);
}
.asset-card-2.prod-lib-card .prod-action[hidden] { display: none; }
.asset-card-2.prod-lib-card .prod-action .btn-aigen {
width: 100%;
display: inline-flex; align-items: center; justify-content: center; gap: 6px;
height: 34px; padding: 0 14px;
background: var(--heat);
color: var(--accent-white);
border: 1px solid var(--heat);
border-radius: var(--r-sm);
font-size: 13px; font-weight: 500;
cursor: pointer;
font-family: inherit;
box-shadow:
inset 0 -2px 4px rgba(250, 93, 25, 0.20),
0 1px 1px rgba(250, 93, 25, 0.12),
0 2px 4px rgba(250, 93, 25, 0.10);
transition: background var(--t-base), box-shadow var(--t-base), transform var(--t-base);
}
.asset-card-2.prod-lib-card .prod-action .btn-aigen:hover {
background: #FB6E2E;
box-shadow:
inset 0 -2px 4px rgba(250, 93, 25, 0.24),
0 2px 4px rgba(250, 93, 25, 0.20),
0 4px 12px rgba(250, 93, 25, 0.18);
transform: translateY(-1px);
}
.asset-card-2.prod-lib-card .prod-action .btn-aigen:active { transform: translateY(0); }
.asset-card-2.prod-lib-card .prod-action .btn-aigen:disabled {
opacity: .65; cursor: not-allowed; transform: none;
box-shadow: inset 0 -2px 4px rgba(250, 93, 25, 0.20);
}
.asset-card-2.prod-lib-card .prod-action .btn-aigen .ai-spark {
width: 14px; height: 14px;
flex-shrink: 0;
}
/* 通用资产详情 modal · 参考布局 v2 */
.asset-modal-bg { position: fixed; inset: 0; background: rgba(0,0,0,.4); z-index: 1000; display: none; align-items: center; justify-content: center; padding: 40px; }
.asset-modal-bg { position: fixed; inset: 0; background: rgba(0,0,0,.4); z-index: 1010; display: none; align-items: center; justify-content: center; padding: 40px; }
.asset-modal-bg.show { display: flex; }
.asset-modal { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); width: min(1040px, 100%); max-height: calc(100vh - 80px); overflow: hidden; display: flex; flex-direction: column; box-shadow: 0 16px 48px rgba(0,0,0,.18); }
.asset-modal-h { display: flex; align-items: center; gap: 10px; padding: 14px 20px; border-bottom: 1px solid var(--border-faint); }
@ -393,16 +610,57 @@
.ml-toolbar .chip:hover { color: var(--accent-black); }
.ml-toolbar .chip.active { background: var(--heat-12); color: var(--heat); border-color: var(--heat-40); font-weight: 600; }
.ml-scroll { flex: 1; min-height: 0; overflow-y: auto; padding: 20px 28px 28px; }
.ml-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 14px; }
.ml-card { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 10px; cursor: pointer; transition: border-color var(--t-base), box-shadow var(--t-base); display: flex; flex-direction: column; gap: 8px; }
.ml-card:hover { border-color: var(--heat-40); box-shadow: 0 1px 3px rgba(0,0,0,.04); }
.ml-card .placeholder { aspect-ratio: 3/4; }
.ml-card .ml-card-nm { font-size: 13px; font-weight: 500; color: var(--accent-black); }
.ml-card .ml-card-sub { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); }
.ml-card .ml-card-foot { display: flex; align-items: center; gap: 6px; margin-top: auto; }
.ml-card .ml-card-foot .pill { font-size: 10.5px; padding: 1px 7px; }
.ml-card .ml-card-foot .btn-apply { margin-left: auto; height: 26px; padding: 0 12px; background: var(--heat); color: var(--accent-white); border: 1px solid var(--heat); border-radius: var(--r-sm); font-size: 11.5px; cursor: pointer; font-family: inherit; }
.ml-card .ml-card-foot .btn-apply:hover { box-shadow: var(--shadow-cta-hover); }
/* 卡片 · 视觉对齐 model-photo .model-card (padding 8 / gap 6 / 无 foot 行) */
.ml-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); gap: 12px; }
.ml-card {
position: relative;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
padding: 8px;
cursor: pointer;
display: flex; flex-direction: column; gap: 6px;
transition: background var(--t-base), border-color var(--t-base);
}
.ml-card:hover { background: var(--surface); }
.ml-card .placeholder { aspect-ratio: 3/4; border-radius: var(--r-sm); }
.ml-card .ml-card-nm { font-size: 12.5px; font-weight: 500; color: var(--accent-black); }
.ml-card .ml-card-sub { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; }
/* 「添加演员/场景」入口卡 · 与 model-photo 模特库视觉一致 */
.ml-card.ml-upload-card { border: 1.5px dashed var(--black-alpha-24); background: var(--surface); display: flex; flex-direction: column; gap: 8px; transition: border-color var(--t-base), background var(--t-base); }
.ml-card.ml-upload-card:hover { border-color: var(--heat); background: var(--heat-12); box-shadow: none; }
.ml-card.ml-upload-card:focus-visible { outline: 2px solid var(--heat); outline-offset: 2px; }
.ml-card.ml-upload-card .up-thumb { aspect-ratio: 3/4; border-radius: var(--r-sm); background: transparent; display: grid; place-items: center; }
.ml-card.ml-upload-card .up-plus { width: 44px; height: 44px; border-radius: 50%; background: var(--surface); border: 1px solid var(--black-alpha-12); color: var(--black-alpha-56); display: grid; place-items: center; transition: background var(--t-base), color var(--t-base), border-color var(--t-base), transform var(--t-base); }
.ml-card.ml-upload-card:hover .up-plus { background: var(--heat); border-color: var(--heat); color: var(--accent-white); transform: scale(1.06); }
.ml-card.ml-upload-card .up-plus svg { width: 22px; height: 22px; }
.ml-card.ml-upload-card .ml-card-nm { color: var(--accent-black); }
.ml-card.ml-upload-card:hover .ml-card-nm { color: var(--heat); }
/* ─── 添加来源 · 选择 modal (AI 生成 / 本地上传) ─── */
.ml-up-choice-bg { position: fixed; inset: 0; z-index: 1200; background: rgba(21, 20, 15, .42); display: none; place-items: center; padding: 16px; }
.ml-up-choice-bg.show { display: grid; }
.ml-up-choice { width: min(560px, 92vw); background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); box-shadow: 0 16px 48px rgba(21, 20, 15, .18); overflow: hidden; position: relative; }
.ml-up-choice .uc-h { display: flex; align-items: center; gap: 12px; padding: 18px 22px 14px; border-bottom: 1px solid var(--border-faint); }
.ml-up-choice .uc-h .ic-m { width: 36px; height: 36px; border-radius: var(--r-md); background: var(--heat-12); color: var(--heat); display: grid; place-items: center; flex-shrink: 0; }
.ml-up-choice .uc-h .ic-m svg { width: 18px; height: 18px; }
.ml-up-choice .uc-h .ti { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.ml-up-choice .uc-h .ti strong { font-size: 15px; color: var(--accent-black); font-weight: 600; }
.ml-up-choice .uc-h .ti .mono { font-family: var(--font-mono); font-size: 11.5px; color: var(--black-alpha-48); letter-spacing: .02em; }
.ml-up-choice .uc-h .uc-x { margin-left: auto; width: 28px; height: 28px; background: transparent; border: 0; border-radius: var(--r-sm); color: var(--black-alpha-56); cursor: pointer; display: grid; place-items: center; }
.ml-up-choice .uc-h .uc-x:hover { background: var(--background-lighter); color: var(--accent-black); }
.ml-up-choice .uc-h .uc-x svg { width: 14px; height: 14px; }
.ml-up-choice .uc-body { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; padding: 20px 22px 22px; }
.ml-up-choice .uc-option { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 18px 16px; text-align: left; cursor: pointer; font-family: inherit; display: flex; flex-direction: column; gap: 10px; transition: border-color var(--t-base), background var(--t-base); }
.ml-up-choice .uc-option:hover { border-color: var(--heat); background: var(--heat-12); }
.ml-up-choice .uc-option .opt-ic { width: 40px; height: 40px; border-radius: var(--r-md); background: var(--background-lighter); color: var(--heat); border: 1px solid var(--heat-20); display: grid; place-items: center; transition: background var(--t-base), color var(--t-base); }
.ml-up-choice .uc-option:hover .opt-ic { background: var(--heat); color: var(--accent-white); border-color: var(--heat); }
.ml-up-choice .uc-option .opt-ic svg { width: 18px; height: 18px; }
.ml-up-choice .uc-option .opt-t { font-size: 14px; font-weight: 600; color: var(--accent-black); }
.ml-up-choice .uc-option .opt-d { font-family: var(--font-mono); font-size: 11.5px; color: var(--black-alpha-56); letter-spacing: .02em; line-height: 1.55; }
.ml-up-choice .uc-option .opt-tag { margin-top: auto; align-self: flex-start; font-family: var(--font-mono); font-size: 10.5px; padding: 2px 8px; border-radius: var(--r-sm); background: var(--background-lighter); color: var(--black-alpha-72); letter-spacing: .04em; }
.ml-up-choice .uc-option:hover .opt-tag { background: var(--surface); color: var(--heat); }
/* 新增人物 modal · 立绘 + 三视图 上传区 */
.upload-zone { aspect-ratio: 3/4; background: var(--background-lighter); border: 1px dashed var(--border-faint); border-radius: var(--r-md); display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; cursor: pointer; transition: border-color var(--t-base), background var(--t-base); padding: 16px; text-align: center; color: var(--black-alpha-56); font-size: 12px; }
@ -586,6 +844,8 @@
<div class="pane-h">
<strong>镜头脚本</strong>
<span class="muted-2 mono" id="shots-meta" style="font-size:11px;">· 空 · 待生成</span>
<span class="spacer"></span>
<button class="btn btn-ghost btn-sm" id="chat-regen-btn">↻ 整体重写</button>
</div>
<div class="shots-body" id="shots-body">
<!-- JS 注入空态/镜头卡片 -->
@ -604,12 +864,20 @@
<!-- JS 注入空态/对话内容 -->
</div>
<div class="chat-input">
<textarea class="textarea" id="chat-textarea" placeholder="对脚本的修改诉求 · 比如:让第 3 场更夸张一点、整体加一场结尾……" rows="2"></textarea>
<div class="hstack" style="margin-top:8px;">
<button class="btn btn-ghost btn-sm" id="chat-regen-btn">↻ 整体重写</button>
<span class="spacer"></span>
<button class="btn btn-primary" id="chat-send-btn">发送 ⌘↵</button>
<div class="chat-input-card">
<div class="chat-attach-row" id="chat-attach-row" hidden></div>
<textarea class="chat-input-area" id="chat-textarea" placeholder='聊聊你的脚本想法,或输入 "@" 引用镜头……' rows="2"></textarea>
<div class="chat-input-foot">
<button class="chat-icon-btn" id="chat-upload-btn" title="上传脚本附件" aria-label="上传脚本附件">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
</button>
<span class="spacer"></span>
<button class="chat-send-btn" id="chat-send-btn" title="发送" aria-label="发送">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 6l6 6-6 6"/></svg>
</button>
</div>
</div>
<input type="file" id="chat-upload-input" hidden accept=".txt,.md,.docx,.doc,.pdf,.srt,.json">
</div>
</div>
</div>
@ -647,14 +915,35 @@
<span class="spacer"></span>
</div>
<div class="prod-row">
<div class="asset-card-2" data-asset-kind="product" data-asset-id="prod-main" id="asset-prod-card">
<div class="placeholder thumb-2"><span class="ph-frame" id="asset-prod-thumb-label">透真补水面膜 · 主图</span></div>
<div class="body-2">
<div class="hstack"><strong style="font-size:13.5px;" id="asset-prod-card-name">透真补水面膜</strong><span class="spacer"></span><span class="pill info" id="asset-prod-pill"><span class="dot"></span>缺三视图</span></div>
<div class="hstack" style="margin-top:10px;">
<span class="spacer"></span>
<button class="btn btn-sm btn-apply" data-stop id="asset-prod-aigen-btn" style="background: var(--heat); color: var(--accent-white); border-color: var(--heat);">AI 生成三视图</button>
</div>
<div class="asset-card-2 prod-lib-card" data-asset-kind="product" data-asset-id="prod-main" id="asset-prod-card">
<div class="placeholder prod-thumb">
<span class="tri-missing-badge" id="asset-prod-tri-badge" tabindex="0" role="button" aria-label="缺三视图,查看说明">
<span class="ico" aria-hidden="true"></span>
<span class="lbl-mono">缺三视图</span>
<span class="tri-missing-pop" role="tooltip">
<span class="pop-h">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M12 9v4M12 17h.01"/><path d="M10.3 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/></svg>
MISSING TRI-VIEW
</span>
<span class="pop-body">该商品还未生成 <b>正 / 侧 / 背</b> 三视图。直接生成图片或视频,模型缺少多角度参考,角色一致性、姿态稳定性可能下降。</span>
<span class="pop-tip">建议:点右下 <b>AI 生成三视图</b> 先补齐三视图,再发起后续生成。</span>
</span>
</span>
<span class="ph-frame" id="asset-prod-thumb-label">透真补水面膜 · 主图</span>
</div>
<div class="prod-body">
<div class="prod-name" id="asset-prod-card-name">透真补水面膜</div>
<div class="prod-cat">美妆个护</div>
<div class="prod-date">2026-05-15 创建</div>
</div>
<div class="prod-action" id="asset-prod-action">
<button class="btn-aigen" type="button" data-stop id="asset-prod-aigen-btn">
<svg class="ai-spark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M12 3l1.6 4.4L18 9l-4.4 1.6L12 15l-1.6-4.4L6 9l4.4-1.6L12 3z"/>
<path d="M19 14l.7 1.8L21.5 16.5l-1.8.7L19 19l-.7-1.8L16.5 16.5l1.8-.7L19 14z"/>
</svg>
AI 生成三视图
</button>
</div>
</div>
<div class="prod-preview" id="asset-prod-preview">
@ -1191,6 +1480,43 @@
</div>
</div>
<!-- ===== 添加演员 / 场景 · 选择来源 modal ===== -->
<div class="ml-up-choice-bg" id="ml-up-choice-bg">
<div class="ml-up-choice" role="dialog" aria-label="添加来源">
<div class="uc-h">
<div class="ic-m">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="8" r="4"/><path d="M3 21c0-3.5 3-6 6-6s6 2.5 6 6"/><path d="M19 8v6M22 11h-6"/></svg>
</div>
<div class="ti">
<strong id="ml-up-title">添加</strong>
<span class="mono">// 选择来源 · AI 生成或本地上传</span>
</div>
<button class="uc-x" type="button" id="ml-up-x" aria-label="关闭">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"><path d="M18 6L6 18M6 6l12 12"/></svg>
</button>
</div>
<div class="uc-body">
<button type="button" class="uc-option" id="ml-up-ai">
<span class="opt-ic">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2l1.7 4.6L18 9l-4.3 1.4L12 15l-1.7-4.6L6 9l4.3-1.4L12 2z"/><path d="M19 16l.85 2.3L22 19l-2.15.7L19 22l-.85-2.3L16 19l2.15-.7L19 16z"/></svg>
</span>
<div class="opt-t">AI 生成</div>
<div class="opt-d" id="ml-up-ai-desc">描述外形 + 风格,AI 自动生成新形象与三视图</div>
<span class="opt-tag">[ AI · STUDIO ]</span>
</button>
<button type="button" class="uc-option" id="ml-up-local">
<span class="opt-ic">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
</span>
<div class="opt-t">本地上传</div>
<div class="opt-d" id="ml-up-local-desc">上传真人 / 既有素材,后续可生成三视图统一镜头</div>
<span class="opt-tag">[ UPLOAD ]</span>
</button>
</div>
</div>
</div>
<input type="file" id="ml-up-file" accept="image/*" multiple hidden>
<!-- ===== 额度预检 modal · PRD §10.3 四层预检 ===== -->
<div class="modal-bg" id="quota-bg" onclick="if(event.target===this)Shell.closeModal('quota-bg')">
<div class="modal" id="quota-modal" style="width: min(440px, 92vw);">
@ -1466,12 +1792,50 @@ const Stage1 = (function () {
});
const sendBtn = document.getElementById('chat-send-btn');
const ta = document.getElementById('chat-textarea');
const attachRow = document.getElementById('chat-attach-row');
let attachments = [];
const renderAttach = () => {
if (!attachRow) return;
if (!attachments.length) { attachRow.hidden = true; attachRow.innerHTML = ''; return; }
attachRow.hidden = false;
attachRow.innerHTML = attachments.map((f, i) => `
<span class="chat-attach-chip">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/></svg>
${f.name.replace(/</g, '&lt;')}
<button class="x" data-rm="${i}" aria-label="移除">
<svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6L6 18M6 6l12 12"/></svg>
</button>
</span>`).join('');
attachRow.querySelectorAll('button[data-rm]').forEach(b => {
b.addEventListener('click', () => {
attachments.splice(+b.dataset.rm, 1);
renderAttach();
});
});
};
const upBtn = document.getElementById('chat-upload-btn');
const upInput = document.getElementById('chat-upload-input');
if (upBtn && upInput) {
upBtn.addEventListener('click', () => upInput.click());
upInput.addEventListener('change', (e) => {
const files = Array.from(e.target.files || []);
if (!files.length) return;
attachments.push(...files);
renderAttach();
Shell.toast('已附加脚本文件', files.map(f => f.name).join('、'));
upInput.value = '';
});
}
if (sendBtn && ta) {
const send = () => {
const v = ta.value.trim();
if (!v) return;
pushMsg('user', v.replace(/</g, '&lt;'));
if (!v && !attachments.length) return;
const fileTags = attachments.length
? `<div class="hstack" style="gap:6px; flex-wrap:wrap; margin-bottom:6px;">${attachments.map(f => `<span class="pill" style="font-family:var(--font-mono); font-size:10.5px;">📎 ${f.name.replace(/</g, '&lt;')}</span>`).join('')}</div>`
: '';
pushMsg('user', fileTags + (v ? v.replace(/</g, '&lt;') : '<span class="muted-2">(已附加文件)</span>'));
ta.value = '';
attachments = []; renderAttach();
renderChat();
setTimeout(() => {
pushMsg('ai', '收到。我会按这个方向调整脚本(静态演示;实际接 LLM API)。');
@ -1688,30 +2052,164 @@ const Stage2 = (function () {
applyLabel: '应用到当前项目',
});
}
// 用户上传的演员 / 场景(分别累积,source='own')
const MODEL_OWN = [];
const SCENE_OWN = [];
let _curLibKind = 'model';
let _curLibSource = 'all';
function _libItemsForSource(kind, src) {
const isModel = kind === 'model';
const presets = isModel ? MODEL_LIB : SCENE_LIB;
const owns = isModel ? MODEL_OWN : SCENE_OWN;
if (src === 'preset') return presets;
if (src === 'own') return owns;
return [...owns, ...presets];
}
function _renderLibGrid() {
const isModel = _curLibKind === 'model';
const items = _libItemsForSource(_curLibKind, _curLibSource);
const grid = document.getElementById('ml-grid');
// 「添加演员 / 添加场景」入口卡 · 平台预设是只读素材,不展示入口
const uploadCardHTML = (_curLibSource === 'preset') ? '' : `
<div class="ml-card ml-upload-card" id="ml-upload-card" role="button" tabindex="0" aria-label="${isModel ? '添加演员' : '添加场景'}">
<div class="up-thumb">
<div class="up-plus">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
</div>
</div>
<div class="ml-card-nm">${isModel ? '添加演员' : '添加场景'}</div>
<div class="ml-card-sub">// AI 生成 / 本地上传</div>
</div>
`;
grid.innerHTML = uploadCardHTML + items.map(it => `
<div class="ml-card" data-name="${it.name}" data-sub="${it.sub}">
<div class="placeholder"><span class="ph-frame">${it.name}</span></div>
<div class="ml-card-nm">${it.name}</div>
<div class="ml-card-sub">${it.sub}</div>
</div>
`).join('');
const upCard = grid.querySelector('#ml-upload-card');
if (upCard) {
upCard.addEventListener('click', () => _openLibUploadChoice());
upCard.addEventListener('keydown', e => {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); _openLibUploadChoice(); }
});
}
// 普通卡片 click → 应用 / 详情
grid.querySelectorAll('.ml-card:not(.ml-upload-card)').forEach(card => {
card.addEventListener('click', (e) => {
const name = card.dataset.name;
const sub = card.dataset.sub;
if (e.target.closest('[data-apply]')) {
e.stopPropagation();
Shell.toast('已应用「' + name + '」', isModel ? '演员库 · 来自' + (_curLibSource === 'own' ? '我的上传' : '预设') : '场景库 · 来自' + (_curLibSource === 'own' ? '我的上传' : '预设'));
document.getElementById('ml-modal-bg').classList.remove('show');
return;
}
openStripDetail(name, sub, _curLibKind);
});
});
}
/* ─── 添加演员/场景 · 选择来源 modal ─── */
function _openLibUploadChoice() {
const isModel = _curLibKind === 'model';
document.getElementById('ml-up-title').textContent = isModel ? '添加演员' : '添加场景';
document.getElementById('ml-up-ai-desc').textContent = isModel
? '描述外形 + 风格,AI 自动生成新演员形象与三视图'
: '描述类型 + 氛围,AI 自动生成新场景图与三视图';
document.getElementById('ml-up-local-desc').textContent = isModel
? '上传真人 / 既有演员素材,后续可生成三视图统一镜头'
: '上传商家自有场景图,后续可生成三视图统一镜头';
document.getElementById('ml-up-choice-bg').classList.add('show');
}
function _closeLibUploadChoice() {
document.getElementById('ml-up-choice-bg').classList.remove('show');
}
(function _bindLibUploadChoice() {
const bg = document.getElementById('ml-up-choice-bg');
if (!bg) return;
document.getElementById('ml-up-x').addEventListener('click', _closeLibUploadChoice);
bg.addEventListener('click', e => { if (e.target === bg) _closeLibUploadChoice(); });
document.addEventListener('keydown', e => {
if (e.key === 'Escape' && bg.classList.contains('show')) _closeLibUploadChoice();
});
// AI 生成 → 待新增独立工作台,先 toast 不跳转
document.getElementById('ml-up-ai').addEventListener('click', () => {
const isModel = _curLibKind === 'model';
_closeLibUploadChoice();
Shell.toast(isModel ? 'AI 演员工作台' : 'AI 场景工作台', '页面待新增 · 暂未跳转');
});
// 本地上传 → 触发文件选择
const fileInput = document.getElementById('ml-up-file');
document.getElementById('ml-up-local').addEventListener('click', () => {
_closeLibUploadChoice();
fileInput.click();
});
fileInput.addEventListener('change', e => {
const files = [...(e.target.files || [])].filter(f => /^image\//.test(f.type));
if (!files.length) return;
const isModel = _curLibKind === 'model';
const ownsArr = isModel ? MODEL_OWN : SCENE_OWN;
files.forEach((f, i) => {
const baseName = (f.name || (isModel ? '我的演员' : '我的场景')).replace(/\.[^.]+$/, '').slice(0, 12);
ownsArr.unshift({
id: 'up-' + Date.now().toString(36) + i,
name: baseName,
sub: isModel ? '我的上传 · 待生成三视图' : '我的上传 · 待生成三视图',
source: 'own',
});
});
e.target.value = '';
// 切到「我的上传」让用户立即看到
_curLibSource = 'own';
const side = document.getElementById('ml-side');
side.querySelectorAll('.ml-side-item').forEach(x => x.classList.toggle('active', x.dataset.source === 'own'));
// 更新左侧计数
const ownCt = side.querySelector('.ml-side-item[data-source="own"] .ct');
if (ownCt) ownCt.textContent = ownsArr.length;
const allCt = side.querySelector('.ml-side-item[data-source="all"] .ct');
if (allCt) allCt.textContent = ownsArr.length + (isModel ? MODEL_LIB.length : SCENE_LIB.length);
_renderLibGrid();
Shell.toast('已上传', `+ ${files.length} 张 · 来源 我的上传`);
});
})();
function openLib(kind) {
_curLibKind = kind;
_curLibSource = 'all';
const isModel = kind === 'model';
const title = isModel ? '演员库' : '场景库';
const items = isModel ? MODEL_LIB : SCENE_LIB;
const presets = isModel ? MODEL_LIB : SCENE_LIB;
const owns = isModel ? MODEL_OWN : SCENE_OWN;
document.getElementById('ml-modal-title').textContent = title;
document.getElementById('ml-modal-ct').textContent = '// 共 ' + items.length + ' 个预设';
document.getElementById('ml-modal-ct').textContent = '// 共 ' + (presets.length + owns.length) + ' 个';
// 侧栏 · 来源
const side = document.getElementById('ml-side');
side.innerHTML = `
<div class="ml-side-h">来源</div>
<div class="ml-side-item active" data-source="all">全部 <span class="ct">${items.length}</span></div>
<div class="ml-side-item" data-source="preset">平台预设 <span class="ct">${items.length}</span></div>
<div class="ml-side-item" data-source="own">我的上传 <span class="ct">0</span></div>
<div class="ml-side-item active" data-source="all">全部 <span class="ct">${presets.length + owns.length}</span></div>
<div class="ml-side-item" data-source="preset">平台预设 <span class="ct">${presets.length}</span></div>
<div class="ml-side-item" data-source="own">我的上传 <span class="ct">${owns.length}</span></div>
`;
side.querySelectorAll('.ml-side-item').forEach(it => {
it.addEventListener('click', () => {
side.querySelectorAll('.ml-side-item').forEach(x => x.classList.remove('active'));
it.classList.add('active');
_curLibSource = it.dataset.source;
_renderLibGrid();
});
});
// toolbar · chip groups + 上传按钮
// toolbar · chip groups (去掉了 btn-up 上传按钮,改用网格内入口卡)
const toolbar = document.getElementById('ml-toolbar');
if (isModel) {
toolbar.innerHTML = `
@ -1727,10 +2225,6 @@ const Stage2 = (function () {
<button class="chip" type="button">青年</button>
<button class="chip" type="button">中年</button>
</div>
<button class="btn-up" type="button" style="margin-left:auto">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M17 8l-5-5-5 5M12 3v12"/></svg>
上传我的演员
</button>
`;
} else {
toolbar.innerHTML = `
@ -1746,10 +2240,6 @@ const Stage2 = (function () {
<button class="chip" type="button"></button>
<button class="chip" type="button"></button>
</div>
<button class="btn-up" type="button" style="margin-left:auto">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M17 8l-5-5-5 5M12 3v12"/></svg>
上传我的场景
</button>
`;
}
toolbar.querySelectorAll('.chip-group').forEach(group => {
@ -1760,36 +2250,9 @@ const Stage2 = (function () {
});
});
});
toolbar.querySelector('.btn-up')?.addEventListener('click', () => {
Shell.toast('上传我的' + (isModel ? '演员' : '场景'), '占位 · 选择本地文件');
});
// 卡片网格
const grid = document.getElementById('ml-grid');
grid.innerHTML = items.map(it => `
<div class="ml-card" data-name="${it.name}" data-sub="${it.sub}">
<div class="placeholder"><span class="ph-frame">${it.name}</span></div>
<div class="ml-card-nm">${it.name}</div>
<div class="ml-card-sub">${it.sub}</div>
<div class="ml-card-foot">
<span class="pill info"><span class="dot"></span>预设</span>
<button class="btn-apply" type="button" data-apply>应用</button>
</div>
</div>
`).join('');
grid.querySelectorAll('.ml-card').forEach(card => {
card.addEventListener('click', (e) => {
const name = card.dataset.name;
const sub = card.dataset.sub;
if (e.target.closest('[data-apply]')) {
e.stopPropagation();
Shell.toast('已应用「' + name + '」', isModel ? '演员库 · 来自预设' : '场景库 · 来自预设');
document.getElementById('ml-modal-bg').classList.remove('show');
return;
}
openStripDetail(name, sub, kind);
});
});
// 卡片网格(含 + 入口 + apply 绑定都在 _renderLibGrid 内完成)
_renderLibGrid();
document.getElementById('ml-modal-bg').classList.add('show');
}
@ -1860,11 +2323,12 @@ const Stage2 = (function () {
el.addEventListener('click', () => bg.classList.remove('show'));
});
});
// 详情 modal · 应用
// 详情 modal · 应用 → 关详情 + 关演员/场景库 → 回到项目页
document.getElementById('asset-detail-apply-btn')?.addEventListener('click', () => {
const name = document.getElementById('asset-detail-title').textContent;
Shell.toast('已应用「' + name + '」', '已加入当前项目');
document.getElementById('asset-detail-modal').classList.remove('show');
document.getElementById('ml-modal-bg')?.classList.remove('show');
Shell.toast('已应用「' + name + '」', '已加入当前项目');
});
// 详情 modal · AI 生成三视图
document.querySelectorAll('.ai-gen-btn').forEach(btn => {
@ -1892,21 +2356,24 @@ const Stage2 = (function () {
});
const thumbLbl = document.getElementById('asset-prod-thumb-label');
if (thumbLbl) thumbLbl.textContent = CURRENT_PRODUCT_NAME + ' · 主图';
// 商品卡 · AI 生成三视图 → 右侧 prod-preview 显示单张 16:9 三视图 + 历史版本
// 商品卡 · AI 生成三视图 → 右侧 prod-preview · 预览/采用 双状态 + 点击主图放大
(function setupProdPreview() {
const aigenBtn = document.getElementById('asset-prod-aigen-btn');
const pane = document.getElementById('asset-prod-preview');
const img = document.getElementById('prod-preview-img');
const statusEl = document.getElementById('prod-preview-status');
const foot = document.getElementById('prod-preview-foot');
const pill = document.getElementById('asset-prod-pill');
const triBadge = document.getElementById('asset-prod-tri-badge');
const prodAction = document.getElementById('asset-prod-action');
const history = document.getElementById('prod-preview-history');
const historyRow = document.getElementById('prod-preview-history-row');
const historyCount = document.getElementById('prod-preview-history-count');
if (!aigenBtn || !pane || !img || !statusEl || !foot || !history || !historyRow) return;
const versions = []; // [{ ts, label }]
let currentIdx = -1; // 当前选中的版本:主图显示 + 缩略图高亮 + 商品资产即应用此版
let previewIdx = -1; // 主图正在「预览」哪一版(浏览态,不动采用状态)
let adoptedIdx = -1; // 真正被「采用」的那一版,决定商品资产生效版本
let generating = false;
function prodName() {
return CURRENT_PRODUCT_NAME || (document.getElementById('asset-prod-card-name')?.textContent ?? '商品');
@ -1919,43 +2386,73 @@ const Stage2 = (function () {
}
history.classList.add('show');
historyCount.textContent = versions.length;
historyRow.innerHTML = versions.map((ver, i) => `
<div class="h-thumb${i === currentIdx ? ' active' : ''}" data-idx="${i}" title="${ver.label} · ${ver.ts}${i === currentIdx ? ' · 当前应用' : ''}">
<span class="badge">当前</span>
<span class="v">${ver.label}</span>
</div>
`).join('');
historyRow.innerHTML = versions.map((ver, i) => {
const isAdopted = i === adoptedIdx;
const isPreview = i === previewIdx;
const cls = [
isAdopted ? 'adopted' : '',
isPreview && !isAdopted ? 'previewing' : '',
].filter(Boolean).join(' ');
const titleParts = [ver.label, ver.ts];
if (isAdopted) titleParts.push('已采用');
else if (isPreview) titleParts.push('预览中');
return `
<div class="h-thumb ${cls}" data-idx="${i}" title="${titleParts.join(' · ')}">
<span class="badge">已采用</span>
<span class="v">${ver.label}</span>
</div>
`;
}).join('');
historyRow.querySelectorAll('.h-thumb').forEach(el => {
el.addEventListener('click', () => {
const idx = Number(el.dataset.idx);
if (idx === currentIdx) return;
setCurrent(idx, /* fromClick */ true);
if (idx === previewIdx) return;
setPreview(idx);
});
});
}
function renderMain() {
if (currentIdx < 0) return;
const ver = versions[currentIdx];
if (previewIdx < 0) return;
const ver = versions[previewIdx];
const isAdopted = previewIdx === adoptedIdx;
img.innerHTML = `<span class="ph-frame">${prodName()} · 三视图(正/侧/背) · ${ver.label}</span>`;
statusEl.textContent = `${ver.label} · 已应用,不满意可重跑`;
img.classList.add('is-zoomable');
img.title = '点击放大查看';
statusEl.textContent = isAdopted
? `${ver.label} · 已采用,不满意可重跑`
: `${ver.label} · 预览中(未采用)`;
foot.innerHTML = `
<button class="btn btn-ghost btn-sm" id="prod-preview-rerun">↻ 重跑</button>
<button class="btn btn-sm ${isAdopted ? '' : 'btn-primary'}" id="prod-preview-adopt" ${isAdopted ? 'disabled title="此版本已采用"' : 'title="将此版本设为商品采用版本,其余转为不通过"'} style="display:inline-flex; align-items:center; gap:4px;">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l5 5L20 6"/></svg>
${isAdopted ? '已采用' : '采用此版本'}
</button>
<span class="spacer"></span>
<span class="muted-2 mono" style="font-size:11px;">~¥0.30 / 次</span>
`;
document.getElementById('prod-preview-rerun')?.addEventListener('click', start);
document.getElementById('prod-preview-adopt')?.addEventListener('click', adoptPreview);
}
function setCurrent(idx, fromClick) {
currentIdx = idx;
const ver = versions[idx];
// 商品状态徽标:选中任意版本即视为已三视图
if (pill) {
pill.className = 'pill ok';
pill.innerHTML = '<span class="dot"></span>已三视图';
}
// 同步到商品详情 modal 的数据源 ASSET_DETAILS['prod-main']
// 仅切预览主图,不动采用/不动商品资产
function setPreview(idx) {
previewIdx = idx;
renderHistory();
renderMain();
}
// 显式「采用」当前预览版本 · 同步商品资产 + 隐藏缺三视图徽标
function adoptPreview() {
if (previewIdx < 0) return;
if (previewIdx === adoptedIdx) return;
adoptedIdx = previewIdx;
applyAdoption(/* fromClick */ true);
}
function applyAdoption(fromClick) {
const ver = versions[adoptedIdx];
if (triBadge) triBadge.hidden = true;
const detail = ASSET_DETAILS['prod-main'];
if (detail) {
detail.hasTri = true;
@ -1963,34 +2460,57 @@ const Stage2 = (function () {
detail.info = [
['类别', '商品 · 当前项目'],
['名称', prodName()],
['三视图', '已生成 · ' + ver.label],
['三视图', '已采用 · ' + ver.label],
['状态', '已三视图'],
];
}
renderHistory();
renderMain();
if (fromClick) Shell.toast('已切换 ' + ver.label, prodName() + ' · 商品资产已更新');
if (fromClick) Shell.toast('已采用 ' + ver.label, prodName() + ' · 商品资产已更新为该版本');
}
function renderLoading() {
img.innerHTML = `<div style="display:flex;flex-direction:column;gap:6px;align-items:center;"><div class="spinner"></div><span class="ph-frame" style="font-size:10.5px;">生成中</span></div>`;
img.classList.remove('is-zoomable');
img.removeAttribute('title');
statusEl.textContent = '生成中 · 约 12s';
foot.innerHTML = '<span class="muted-2 mono" style="font-size:11px;">// POST /assets/tri-view</span>';
aigenBtn.disabled = true;
}
function start() {
if (generating) return;
generating = true;
pane.classList.add('show');
renderLoading();
setTimeout(() => {
generating = false;
aigenBtn.disabled = false;
const now = new Date();
const ts = String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0');
versions.push({ ts, label: 'v' + (versions.length + 1) });
setCurrent(versions.length - 1, /* fromClick */ false);
const newVer = { ts, label: 'v' + (versions.length + 1) };
versions.push(newVer);
const newIdx = versions.length - 1;
previewIdx = newIdx;
if (adoptedIdx === -1) {
adoptedIdx = newIdx;
applyAdoption(/* fromClick */ false);
} else {
renderHistory();
renderMain();
Shell.toast('三视图已生成 ' + newVer.label, prodName() + ' · 预览中,满意请点「采用此版本」');
}
}, 1800);
}
// 主图点击 → 放大查看
img.addEventListener('click', (e) => {
if (!img.classList.contains('is-zoomable')) return;
if (previewIdx < 0) return;
e.stopPropagation();
openTriLightbox(versions[previewIdx], previewIdx === adoptedIdx, prodName());
});
aigenBtn.addEventListener('click', (e) => {
e.stopPropagation();
start();
@ -2224,6 +2744,49 @@ window.Quota = (function () {
return { preflight };
})();
/* ============================================================
三视图 · 放大查看 lightbox · setupProdPreview 共用
============================================================ */
function openTriLightbox(ver, isAdopted, prodName) {
let bg = document.getElementById('tri-lightbox-bg');
if (!bg) {
bg = document.createElement('div');
bg.id = 'tri-lightbox-bg';
bg.className = 'modal-bg';
bg.innerHTML = `
<div class="tri-lightbox" role="dialog" aria-label="三视图放大查看">
<button class="tri-lightbox-close" type="button" aria-label="关闭">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6L6 18M6 6l12 12"/></svg>
</button>
<div class="tri-lightbox-head">
// 三视图(正/侧/背) · <span class="lb-ver" id="tri-lightbox-label">v1</span>
<span class="lb-tag" id="tri-lightbox-tag" hidden>已采用</span>
</div>
<div class="placeholder tri-lightbox-img" id="tri-lightbox-img"></div>
<div class="tri-lightbox-foot">
<span id="tri-lightbox-meta">// 生成于 --:--</span>
<span class="spc"></span>
<span><kbd>Esc</kbd> 关闭</span>
</div>
</div>
`;
document.body.appendChild(bg);
bg.addEventListener('click', (e) => {
if (e.target === bg) Shell.closeModal('tri-lightbox-bg');
});
bg.querySelector('.tri-lightbox-close')?.addEventListener('click', () => {
Shell.closeModal('tri-lightbox-bg');
});
}
bg.querySelector('#tri-lightbox-img').innerHTML =
`<span class="ph-frame">${prodName} · 三视图(正/侧/背) · ${ver.label}</span>`;
bg.querySelector('#tri-lightbox-label').textContent = ver.label;
const tag = bg.querySelector('#tri-lightbox-tag');
tag.hidden = !isAdopted;
bg.querySelector('#tri-lightbox-meta').textContent = `// 生成于 ${ver.ts}`;
Shell.openModal('tri-lightbox-bg');
}
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -78,6 +78,9 @@
white-space: nowrap;
}
.ov-edit.primary:hover { filter: brightness(1.05); background: var(--heat); color: var(--accent-white); }
.ov-edit:disabled { cursor: not-allowed; color: var(--heat); border-color: var(--heat-40); background: var(--heat-12); opacity: 1; }
.ov-edit:disabled:hover { color: var(--heat); border-color: var(--heat-40); background: var(--heat-12); }
.ov-edit:disabled svg { color: var(--heat); }
/* 编辑模式按钮组 (重置 + 取消 + 保存) */
.ov-edit-group {
@ -89,6 +92,154 @@
.ov-card.editing .ov-edit-single { display: none; }
.ov-card.editing .ov-edit-group { display: inline-flex; }
/* AI 生成三视图 · 按钮 + 弹出 panel(布局复刻 pipeline.html stage 2 三视图预览) */
.ov-tri-wrap { position: relative; margin-left: auto; }
/* 当 AI 入口存在时,编辑信息按钮不再独占 ml-auto,与 AI 按钮紧贴 */
.ov-tri-wrap + .ov-edit-single { margin-left: 0; }
.ov-tri-trigger { white-space: nowrap; }
.ov-tri-trigger.is-open { border-color: var(--heat); color: var(--heat); background: var(--heat-12); }
.ov-card.editing .ov-tri-wrap { display: none; }
.ov-tri-pop {
position: absolute;
top: calc(100% + 6px); right: 0;
width: 360px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
box-shadow: 0 8px 24px rgba(0,0,0,.10), 0 2px 6px rgba(0,0,0,.06);
padding: 14px 14px 12px;
display: none;
flex-direction: column;
gap: 10px;
z-index: 40;
}
.ov-tri-pop.show { display: flex; }
.ov-tri-pop::before {
content: ''; position: absolute;
top: -5px; right: 36px;
width: 9px; height: 9px;
background: var(--surface);
border-left: 1px solid var(--border-faint);
border-top: 1px solid var(--border-faint);
transform: rotate(45deg);
}
.ov-tri-close {
position: absolute;
top: 8px; right: 8px;
width: 22px; height: 22px;
display: grid; place-items: center;
background: transparent;
border: 1px solid transparent;
border-radius: var(--r-sm);
color: var(--black-alpha-56);
cursor: pointer;
transition: background var(--t-base), color var(--t-base), border-color var(--t-base);
z-index: 2;
}
.ov-tri-close:hover { background: var(--black-alpha-08); color: var(--accent-black); border-color: var(--border-faint); }
.ov-tri-close svg { width: 12px; height: 12px; }
/* 复刻 pipeline.html .prod-preview-* 内部样式 */
.ov-tri-pop .prod-preview-h { display: flex; align-items: center; gap: 8px; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-56); letter-spacing: .04em; text-transform: uppercase; padding-right: 28px; }
.ov-tri-pop .prod-preview-img { aspect-ratio: 16/9; }
.ov-tri-pop .prod-preview-foot { display: flex; align-items: center; gap: 8px; min-height: 30px; }
.ov-tri-pop .prod-preview-history { display: none; flex-direction: column; gap: 6px; }
.ov-tri-pop .prod-preview-history.show { display: flex; }
.ov-tri-pop .prod-preview-history .h-lbl { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .04em; text-transform: uppercase; }
.ov-tri-pop .prod-preview-history .h-lbl .ct { color: var(--accent-black); font-weight: 600; }
.ov-tri-pop .prod-preview-history .h-row { display: flex; gap: 6px; overflow-x: auto; padding: 2px; scrollbar-width: thin; }
.ov-tri-pop .prod-preview-history .h-row::-webkit-scrollbar { height: 4px; }
.ov-tri-pop .prod-preview-history .h-row::-webkit-scrollbar-thumb { background: var(--border-faint); border-radius: 2px; }
.ov-tri-pop .prod-preview-history .h-thumb {
flex: 0 0 auto;
width: 72px; aspect-ratio: 16/9;
background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-sm);
position: relative; cursor: pointer; transition: border-color var(--t-base);
display: grid; place-items: center; overflow: hidden;
}
.ov-tri-pop .prod-preview-history .h-thumb:hover { border-color: var(--heat-40); }
/* 已采用版本:主橙描边 + 「已采用」徽标 */
.ov-tri-pop .prod-preview-history .h-thumb.adopted { border-color: var(--heat); border-width: 2px; box-shadow: 0 0 0 2px var(--heat-12); }
/* 仅预览(未采用):黑色描边,无徽标 */
.ov-tri-pop .prod-preview-history .h-thumb.previewing { border-color: var(--accent-black); border-width: 2px; }
.ov-tri-pop .prod-preview-history .h-thumb .v { font-family: var(--font-mono); font-size: 10px; color: var(--black-alpha-56); letter-spacing: .02em; }
.ov-tri-pop .prod-preview-history .h-thumb.adopted .v { color: var(--heat); font-weight: 600; }
.ov-tri-pop .prod-preview-history .h-thumb.previewing .v { color: var(--accent-black); font-weight: 600; }
.ov-tri-pop .prod-preview-history .h-thumb .badge { position: absolute; top: 2px; left: 2px; font-family: var(--font-mono); font-size: 8.5px; padding: 0 4px; line-height: 12px; background: var(--heat); color: var(--accent-white); border-radius: 2px; letter-spacing: .02em; display: none; }
.ov-tri-pop .prod-preview-history .h-thumb.adopted .badge { display: block; }
/* 主图可点击放大 */
.ov-tri-pop .prod-preview-img.is-zoomable { cursor: zoom-in; transition: border-color var(--t-base); position: relative; }
.ov-tri-pop .prod-preview-img.is-zoomable:hover { border-color: var(--heat-40); }
.ov-tri-pop .prod-preview-img.is-zoomable::after {
content: '';
position: absolute; top: 8px; right: 8px;
width: 22px; height: 22px;
background: rgba(21,20,15,.72) url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><circle cx='11' cy='11' r='7'/><path d='M21 21l-4.3-4.3M8 11h6M11 8v6'/></svg>") center/14px no-repeat;
border-radius: var(--r-sm);
opacity: 0; transition: opacity var(--t-base);
pointer-events: none;
}
.ov-tri-pop .prod-preview-img.is-zoomable:hover::after { opacity: 1; }
/* 三视图放大查看 lightbox */
#ov-tri-lightbox-bg { z-index: 80; }
#ov-tri-lightbox-bg .tri-lightbox {
position: relative;
width: min(1100px, 92vw);
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
padding: 18px 20px 20px;
display: flex; flex-direction: column; gap: 12px;
box-shadow: 0 24px 64px rgba(0,0,0,.24);
}
.tri-lightbox-head {
display: flex; align-items: center; gap: 8px;
font-family: var(--font-mono);
font-size: 11px; letter-spacing: .04em; text-transform: uppercase;
color: var(--black-alpha-56);
padding-right: 32px;
}
.tri-lightbox-head .lb-ver { color: var(--heat); font-weight: 600; }
.tri-lightbox-head .lb-tag {
margin-left: 6px;
padding: 2px 6px;
background: var(--heat-12); color: var(--heat);
border-radius: 3px;
font-size: 10px;
}
.tri-lightbox-close {
position: absolute;
top: 12px; right: 12px;
width: 28px; height: 28px;
display: grid; place-items: center;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
color: var(--black-alpha-56);
cursor: pointer;
transition: background var(--t-base), color var(--t-base), border-color var(--t-base);
z-index: 2;
}
.tri-lightbox-close:hover { background: var(--black-alpha-08); color: var(--accent-black); border-color: var(--black-alpha-12); }
.tri-lightbox-close svg { width: 14px; height: 14px; }
.tri-lightbox-img { aspect-ratio: 16/9; width: 100%; }
.tri-lightbox-foot { display: flex; align-items: center; gap: 8px; font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); }
.tri-lightbox-foot .spc { flex: 1; }
.tri-lightbox-foot kbd {
display: inline-block;
padding: 1px 5px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-bottom-width: 2px;
border-radius: 3px;
font-family: var(--font-mono);
font-size: 10px;
color: var(--black-alpha-72);
}
/* 字段 view ↔ edit 状态切换 */
.v-edit { display: none; }
.ov-card.editing .v-static { display: none; }
@ -771,6 +922,32 @@
<div class="ov-card ov-main" id="ov-main-card">
<div class="ov-h">
<span class="ti">商品信息</span>
<!-- AI 生成三视图 · 按钮 + 弹出 panel(view 模式可见) -->
<div class="ov-tri-wrap">
<button class="ov-edit ov-tri-trigger" type="button" id="ov-tri-btn" title="AI 生成商品三视图" aria-haspopup="dialog" aria-expanded="false">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l1.8 4.2L18 9l-4.2 1.8L12 15l-1.8-4.2L6 9l4.2-1.8L12 3z"/><path d="M19 14l.9 2.1L22 17l-2.1.9L19 20l-.9-2.1L16 17l2.1-.9L19 14z"/></svg>
AI 生成三视图
</button>
<div class="ov-tri-pop" id="ov-tri-pop" role="dialog" aria-label="AI 生成三视图">
<button class="ov-tri-close" type="button" id="ov-tri-close" aria-label="关闭">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6L6 18M6 6l12 12"/></svg>
</button>
<div class="prod-preview-h">// 三视图预览 · <span id="ov-tri-status">待生成</span></div>
<div class="placeholder prod-preview-img" id="ov-tri-img"><span class="ph-frame">// 尚未生成 · 点击下方按钮开始</span></div>
<div class="prod-preview-foot" id="ov-tri-foot">
<button class="ov-edit primary" type="button" id="ov-tri-start" style="height:28px;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l1.8 4.2L18 9l-4.2 1.8L12 15l-1.8-4.2L6 9l4.2-1.8L12 3z"/></svg>
生成
</button>
<span style="flex:1;"></span>
<span class="mono" style="font-size:11px; color: var(--black-alpha-56);">~¥0.30 / 次</span>
</div>
<div class="prod-preview-history" id="ov-tri-history">
<div class="h-lbl">// 历史版本 · <span class="ct" id="ov-tri-history-count">0</span></div>
<div class="h-row" id="ov-tri-history-row"></div>
</div>
</div>
</div>
<!-- view 模式: 单个 [编辑信息] -->
<button class="ov-edit ov-edit-single" type="button" id="ov-edit-btn" title="编辑商品信息">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 013 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
@ -875,7 +1052,7 @@
</div>
<div class="qa-item" data-go="image-optimize" role="button" tabindex="0">
<span class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v18M3 12h18M5 5l14 14M5 19l14-14"/></svg></span>
图片优化
图片创作
</div>
</div>
</div>
@ -896,7 +1073,7 @@
<div class="pd-tabs">
<button class="tab active" type="button" data-tab="assets">AI 生成素材</button>
<button class="tab" type="button" data-tab="videos">视频项目</button>
<button class="tab" type="button" data-tab="tasks">任务记录</button>
<button class="tab" type="button" data-tab="tasks" hidden>任务记录</button>
</div>
<!-- ===== AI 生成素材 ===== -->
@ -1143,6 +1320,25 @@
</div>
<!-- 三视图 · 放大查看 lightbox -->
<div class="modal-bg" id="ov-tri-lightbox-bg" onclick="if(event.target===this)Shell.closeModal('ov-tri-lightbox-bg')">
<div class="tri-lightbox" role="dialog" aria-label="三视图放大查看">
<button class="tri-lightbox-close" type="button" onclick="Shell.closeModal('ov-tri-lightbox-bg')" aria-label="关闭">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6L6 18M6 6l12 12"/></svg>
</button>
<div class="tri-lightbox-head">
// 三视图(正/侧/背) · <span class="lb-ver" id="ov-tri-lightbox-label">v1</span>
<span class="lb-tag" id="ov-tri-lightbox-tag" hidden>已采用</span>
</div>
<div class="placeholder tri-lightbox-img" id="ov-tri-lightbox-img"></div>
<div class="tri-lightbox-foot">
<span id="ov-tri-lightbox-meta">// 生成于 --:--</span>
<span class="spc"></span>
<span><kbd>Esc</kbd> 关闭</span>
</div>
</div>
</div>
<script src="assets/shell.js?v=202605211643"></script>
<script>
// 从 URL ?product= 读出商品名,注入 crumb / h1 / 商品名字段
@ -1181,10 +1377,10 @@
document.querySelectorAll('.qa-item[data-go]').forEach(item => {
item.style.cursor = 'pointer';
let go = item.dataset.go;
// 图片优化暂复用 model-photo.html?mode=tri (后端实际生成人物 / 商品三视图)
// 图片创作 → 独立工作台 image-optimize.html(自由创作),带 product 作为提示词种子
let url;
if (go === 'image-optimize') {
url = 'model-photo.html?mode=tri&t=' + Date.now() + '&product=' + encodeURIComponent(name);
url = 'image-optimize.html?t=' + Date.now() + '&prompt=' + encodeURIComponent(name);
} else {
url = go + '.html?t=' + Date.now() + '&product=' + encodeURIComponent(name);
}
@ -1669,9 +1865,348 @@
}
})();
// 从 create 跳来时显示 toast
// AI 生成三视图 · 按钮悬浮 panel(可重复打开 / X 关闭 / 点击外部关闭 / Esc 关闭)
(function initTriView() {
const btn = document.getElementById('ov-tri-btn');
const pop = document.getElementById('ov-tri-pop');
const closeBtn = document.getElementById('ov-tri-close');
const startBtn = document.getElementById('ov-tri-start');
const img = document.getElementById('ov-tri-img');
const statusEl = document.getElementById('ov-tri-status');
const foot = document.getElementById('ov-tri-foot');
const history = document.getElementById('ov-tri-history');
const historyRow = document.getElementById('ov-tri-history-row');
const historyCount = document.getElementById('ov-tri-history-count');
if (!btn || !pop || !closeBtn || !startBtn) return;
const versions = []; // [{ ts, label }]
let previewIdx = -1; // 主图当前正在「预览」哪一版(浏览态)
let adoptedIdx = -1; // 真正被「采用」的那一版 · 与素材库通过状态联动
let generating = false;
function prodName() {
return (document.getElementById('pd-name')?.textContent || '商品').trim();
}
function open() {
pop.classList.add('show');
btn.classList.add('is-open');
btn.setAttribute('aria-expanded', 'true');
}
function close() {
pop.classList.remove('show');
btn.classList.remove('is-open');
btn.setAttribute('aria-expanded', 'false');
}
function toggle() {
if (pop.classList.contains('show')) close(); else open();
}
function renderHistory() {
if (versions.length === 0) { history.classList.remove('show'); return; }
history.classList.add('show');
historyCount.textContent = versions.length;
historyRow.innerHTML = versions.map((ver, i) => {
const isAdopted = i === adoptedIdx;
const isPreview = i === previewIdx;
const cls = [
isAdopted ? 'adopted' : '',
isPreview && !isAdopted ? 'previewing' : '',
].filter(Boolean).join(' ');
const titleParts = [ver.label, ver.ts];
if (isAdopted) titleParts.push('已采用');
else if (isPreview) titleParts.push('预览中');
return `
<div class="h-thumb ${cls}" data-idx="${i}" title="${titleParts.join(' · ')}">
<span class="badge">已采用</span>
<span class="v">${ver.label}</span>
</div>
`;
}).join('');
historyRow.querySelectorAll('.h-thumb').forEach(el => {
el.addEventListener('click', () => {
const idx = Number(el.dataset.idx);
if (idx === previewIdx) return;
setPreview(idx);
});
});
}
function renderMain() {
if (previewIdx < 0) return;
const ver = versions[previewIdx];
const isAdopted = previewIdx === adoptedIdx;
img.innerHTML = `<span class="ph-frame">${prodName()} · 三视图(正/侧/背) · ${ver.label}</span>`;
img.classList.add('is-zoomable');
img.title = '点击放大查看';
statusEl.textContent = isAdopted
? `${ver.label} · 已采用,不满意可重跑`
: `${ver.label} · 预览中(未采用)`;
foot.innerHTML = `
<button class="ov-edit" type="button" id="ov-tri-rerun" style="height:28px;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 3-6.7"/><path d="M3 4v5h5"/></svg>
重跑
</button>
<button class="ov-edit ${isAdopted ? '' : 'primary'}" type="button" id="ov-tri-adopt" style="height:28px;" ${isAdopted ? 'disabled title="此版本已采用"' : 'title="将此版本设为唯一通过版本,其他版本变为不通过"'}>
${isAdopted
? '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l5 5L20 6"/></svg> 已采用'
: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l5 5L20 6"/></svg> 采用此版本'}
</button>
<span style="flex:1;"></span>
<span class="mono" style="font-size:11px; color: var(--black-alpha-56);">~¥0.30 / 次</span>
`;
document.getElementById('ov-tri-rerun')?.addEventListener('click', start);
document.getElementById('ov-tri-adopt')?.addEventListener('click', adoptPreview);
}
function syncLibraryStatus() {
const grid = document.querySelector('.tab-pane[data-pane="assets"] .asset-grid');
if (!grid) return;
const adoptedLabel = versions[adoptedIdx]?.label;
grid.querySelectorAll('.asset-card[data-tri-version]').forEach(c => {
const pill = c.querySelector('.pill');
if (!pill) return;
const isAdopted = c.dataset.triVersion === adoptedLabel;
pill.className = 'pill ' + (isAdopted ? 'pass' : 'fail');
pill.textContent = isAdopted ? '通过' : '不通过';
pill.setAttribute('data-status', isAdopted ? 'pass' : 'fail');
pill.setAttribute('title', isAdopted ? '当前采用版本' : '未被采用');
});
}
function appendLibraryCard(ver) {
const grid = document.querySelector('.tab-pane[data-pane="assets"] .asset-grid');
if (!grid) return;
const now = new Date();
const pad = n => String(n).padStart(2, '0');
const dateStr = `${now.getFullYear()}-${pad(now.getMonth()+1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}`;
const card = document.createElement('div');
card.className = 'asset-card';
card.dataset.triVersion = ver.label;
// 新生成的版本默认 `不通过`,等用户点「采用此版本」才转为通过
card.innerHTML = `
<div class="thumb placeholder"><span class="type-pill">三视图</span><span class="ph-frame">${prodName()} · ${ver.label}</span></div>
<div class="meta"><span class="pill fail" data-status="fail" title="未被采用">不通过</span><span class="date">${dateStr}</span></div>
`;
grid.prepend(card);
// 更新「全部 AI 素材 (N)」计数
const ct = document.querySelector('.pd-toolbar .total .ct');
if (ct) {
const m = ct.textContent.match(/(\d+)/);
const n = m ? Number(m[1]) + 1 : 1;
ct.textContent = `(${n})`;
}
}
// 切换主图预览(不动采用状态、不动素材库)
function setPreview(idx) {
previewIdx = idx;
renderHistory();
renderMain();
}
// 显式「采用」当前预览版本 · 同步素材库通过/不通过
function adoptPreview() {
if (previewIdx < 0) return;
if (previewIdx === adoptedIdx) return;
adoptedIdx = previewIdx;
renderHistory();
renderMain();
syncLibraryStatus();
if (window.Shell?.toast) {
Shell.toast('已采用 ' + versions[adoptedIdx].label,
prodName() + ' · 该版本通过,其余版本转为不通过');
}
}
function renderLoading() {
img.innerHTML = `<div style="display:flex;flex-direction:column;gap:6px;align-items:center;"><div class="spinner"></div><span class="ph-frame" style="font-size:10.5px;">生成中</span></div>`;
img.classList.remove('is-zoomable');
img.removeAttribute('title');
statusEl.textContent = '生成中 · 约 12s';
foot.innerHTML = '<span class="mono" style="font-size:11px; color: var(--black-alpha-48);">// POST /assets/tri-view</span>';
}
function openLightbox() {
if (previewIdx < 0) return;
const ver = versions[previewIdx];
const isAdopted = previewIdx === adoptedIdx;
const lbImg = document.getElementById('ov-tri-lightbox-img');
const lbLabel = document.getElementById('ov-tri-lightbox-label');
const lbTag = document.getElementById('ov-tri-lightbox-tag');
const lbMeta = document.getElementById('ov-tri-lightbox-meta');
if (lbImg) lbImg.innerHTML = `<span class="ph-frame">${prodName()} · 三视图(正/侧/背) · ${ver.label}</span>`;
if (lbLabel) lbLabel.textContent = ver.label;
if (lbTag) {
lbTag.hidden = !isAdopted;
lbTag.textContent = '已采用';
}
if (lbMeta) lbMeta.textContent = `// 生成于 ${ver.ts}`;
window.Shell?.openModal?.('ov-tri-lightbox-bg');
}
function start() {
if (generating) return;
generating = true;
open();
renderLoading();
setTimeout(() => {
generating = false;
const now = new Date();
const ts = String(now.getHours()).padStart(2,'0') + ':' + String(now.getMinutes()).padStart(2,'0');
const newVer = { ts, label: 'v' + (versions.length + 1) };
versions.push(newVer);
appendLibraryCard(newVer);
const newIdx = versions.length - 1;
previewIdx = newIdx;
// 第一次生成 · 自动采用新版本(无选择可言);之后只切预览,不动采用
if (adoptedIdx === -1) {
adoptedIdx = newIdx;
syncLibraryStatus();
}
renderHistory();
renderMain();
if (window.Shell?.toast) {
const tip = (adoptedIdx === newIdx)
? `${newVer.label} · 已采用并同步到素材库`
: `${newVer.label} · 预览中 · 满意请点「采用此版本」`;
Shell.toast('三视图已生成', `${prodName()} · ${tip}`);
}
}, 1800);
}
btn.addEventListener('click', (e) => { e.stopPropagation(); toggle(); });
closeBtn.addEventListener('click', (e) => { e.stopPropagation(); close(); });
startBtn.addEventListener('click', (e) => { e.stopPropagation(); start(); });
pop.addEventListener('click', (e) => e.stopPropagation());
img.addEventListener('click', (e) => {
if (!img.classList.contains('is-zoomable')) return;
e.stopPropagation();
openLightbox();
});
document.addEventListener('click', (e) => {
if (!pop.classList.contains('show')) return;
if (pop.contains(e.target) || btn.contains(e.target)) return;
close();
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && pop.classList.contains('show')) close();
});
})();
// 从 create 跳来时显示 toast + 清空三个 tab 数据(新商品没有素材/项目/任务)
if (location.search.includes('id=new')) {
setTimeout(() => Shell.toast('商品已创建', '开始创建 AI 资产'), 200);
// 从 sessionStorage 读出 drawer 刚保存的完整 product,注入到「商品信息」卡
(function injectFromSession() {
let p = null;
try {
const raw = sessionStorage.getItem('npd-last-created');
if (raw) p = JSON.parse(raw);
sessionStorage.removeItem('npd-last-created'); // 读完即清,避免污染
} catch (e) { /* ignore */ }
if (!p) return;
const esc = s => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
// 商品名称(URL 已经设过,这里兜底)
if (p.name) {
const nm = document.querySelector('[data-field="name"] .v-static');
if (nm) nm.textContent = p.name;
const nmI = document.querySelector('[data-field="name"] .v-input');
if (nmI) nmI.value = p.name;
}
// 品类
if (p.cat) {
const catRow = document.querySelector('[data-field="cat"] .v-static');
if (catRow) catRow.textContent = p.cat;
const catSel = document.querySelector('[data-field="cat"] .v-select');
if (catSel) {
// 找到匹配 option 并 select;若没有则插入到首位
let matched = false;
[...catSel.options].forEach(o => { if (o.value === p.cat || o.textContent === p.cat) { o.selected = true; matched = true; } });
if (!matched) {
const opt = document.createElement('option');
opt.value = p.cat; opt.textContent = p.cat; opt.selected = true;
catSel.insertBefore(opt, catSel.firstChild);
}
}
}
// 目标人群(可空)
const tgtRow = document.querySelector('[data-field="target"] .v-static');
const tgtIn = document.querySelector('[data-field="target"] .v-input');
if (tgtRow) tgtRow.textContent = p.target || '—';
if (tgtIn) tgtIn.value = p.target || '';
// 卖点
if (Array.isArray(p.points)) {
const blStatic = document.querySelector('[data-field="bullets"] .v-static');
if (blStatic) {
blStatic.innerHTML = p.points.length
? p.points.map(t => `<span class="bullet">${esc(t)}</span>`).join('')
: '<span class="bullet" style="color:var(--black-alpha-48)"></span>';
}
}
// 商品图片 — 替换 6 张占位为真实 dataUrl;数量按上传数定
const grid = document.getElementById('ov-images-grid');
const addBtn = document.getElementById('ov-img-add');
if (grid) {
// 移除现有所有 .thumb 占位(保留末尾 #ov-img-add)
[...grid.querySelectorAll('.thumb')].forEach(t => t.remove());
if (Array.isArray(p.images) && p.images.length) {
p.images.forEach(img => {
const t = document.createElement('div');
t.className = 'thumb';
t.style.cssText = 'background-image:url(' + img.dataUrl + ');background-size:cover;background-position:center;';
if (addBtn) grid.insertBefore(t, addBtn);
else grid.appendChild(t);
});
}
// 同步计数
const ct = document.querySelector('.ov-images-sub .sub-h .ct');
if (ct) ct.textContent = '(' + ((p.images || []).length) + ')';
}
})();
const EMPTY_HTML = (txt) => `<div class="empty-filter">// NO DATA<br><span style="margin-top:6px;display:inline-block">${txt}</span></div>`;
// AI 生成素材
const assetsPane = document.querySelector('.tab-pane[data-pane="assets"]');
if (assetsPane) {
const grid = assetsPane.querySelector('.asset-grid');
if (grid) grid.outerHTML = EMPTY_HTML('还没有 AI 素材,使用右上角「图片生成」开始创建');
const ct = assetsPane.querySelector('.total .ct');
if (ct) ct.textContent = '(0)';
const more = assetsPane.querySelector('.pd-more');
if (more) more.remove();
}
// 视频项目
const videosPane = document.querySelector('.tab-pane[data-pane="videos"]');
if (videosPane) {
const grid = videosPane.querySelector('.asset-grid');
if (grid) grid.outerHTML = EMPTY_HTML('还没有视频项目,前往工作台「新建项目」开始');
const ct = videosPane.querySelector('.total .ct');
if (ct) ct.textContent = '(0)';
const more = videosPane.querySelector('.pd-more');
if (more) more.remove();
}
// 任务记录
const tasksPane = document.querySelector('.tab-pane[data-pane="tasks"]');
if (tasksPane) {
tasksPane.querySelectorAll('.task-stat .v').forEach(el => {
const small = el.querySelector('small');
el.textContent = '0';
if (small) el.appendChild(small);
});
const tbl = tasksPane.querySelector('.task-table');
if (tbl) tbl.outerHTML = EMPTY_HTML('暂无任务记录');
const ct = tasksPane.querySelector('.total .ct');
if (ct) ct.textContent = '(0)';
const more = tasksPane.querySelector('.pd-more');
if (more) more.remove();
}
}
</script>
</body>

View File

@ -140,7 +140,10 @@
/* ─── 卡片底栏 · V2.1 克制版 (线图标 + mono 文本) ─── */
.product-footer {
display: flex; align-items: center; gap: 8px;
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
column-gap: 8px;
padding: 10px 12px;
border-top: 1px solid var(--border-faint);
font-size: 11.5px;
@ -148,16 +151,16 @@
background: var(--background-base);
}
.product-footer .stat {
display: inline-flex; align-items: center; gap: 5px;
display: inline-flex; align-items: center; justify-content: center; gap: 5px;
padding: 3px 8px;
border-radius: var(--r-sm);
font-family: var(--font-mono);
letter-spacing: .02em;
white-space: nowrap; /* 防止"素材"等中文被挤压成竖排 */
flex-shrink: 0;
border: 1px solid transparent;
transition: background var(--t-base), color var(--t-base), border-color var(--t-base);
}
.product-footer .stat { justify-self: center; }
.product-footer .stat[data-type] { cursor: pointer; }
.product-footer .stat[data-type]:hover {
background: var(--heat-12);
@ -1019,9 +1022,11 @@ Shell.render({ active: 'products', crumbs: [{ label: '工作台', href: 'index.h
// ============== 注入用户新建的商品(来自工作台 / 商品库 drawer,写入 localStorage)==============
// 必须在 const cards / TOTAL / CAT_COUNT 之前执行,让后续逻辑把它们当普通商品处理
(function injectExtraProducts() {
// 一次性清掉历史遗留的 localStorage 旧数据(用户上次会话误持久化的占位商品)
try { localStorage.removeItem('fs-extra-products'); } catch (e) {}
let pending;
try {
pending = JSON.parse(localStorage.getItem('fs-extra-products') || '[]');
pending = JSON.parse(sessionStorage.getItem('fs-extra-products') || '[]');
} catch (e) { return; }
if (!Array.isArray(pending) || !pending.length) return;
const grid = document.getElementById('product-grid');
@ -1046,7 +1051,18 @@ Shell.render({ active: 'products', crumbs: [{ label: '工作台', href: 'index.h
<span class="card-check" aria-hidden="true"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg></span>
<button class="card-del-btn" type="button" title="删除商品" onclick="event.stopPropagation();" data-action="delete-product"><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 product-thumb">
<span class="tri-missing-badge" title="该商品尚未生成商品三视图,进入详情可在工作流第一步补齐"><span class="lbl-mono">缺三视图</span></span>
<span class="tri-missing-badge" tabindex="0" role="button" aria-label="缺三视图,查看说明">
<span class="ico" aria-hidden="true"></span>
<span class="lbl-mono">缺三视图</span>
<span class="tri-missing-pop" role="tooltip">
<span class="pop-h">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M12 9v4M12 17h.01"/><path d="M10.3 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/></svg>
MISSING TRI-VIEW
</span>
<span class="pop-body">该商品还未生成 <b>正 / 侧 / 背</b> 三视图。直接生成图片或视频,模型缺少多角度参考,角色一致性、姿态稳定性可能下降。</span>
<span class="pop-tip">建议:进入 <b>商品详情</b> 先补齐三视图,再发起后续生成。</span>
</span>
</span>
<span class="ph-frame">${esc(p.name)} · 新建</span>
</div>
<div class="product-body">
@ -1753,11 +1769,11 @@ saveBtn.addEventListener('click', () => {
return;
}
const bullets = confirmedBullets;
// 持久化到 localStorage('fs-extra-products'),让 products.html 下次加载时
// 自动从 storage 读出并 prepend 到 grid
// 持久化到 sessionStorage('fs-extra-products') · 仅在当前标签页生命周期内有效
// 关闭标签页/浏览器后自动清空,不会跨会话累积演示残留
try {
const KEY = 'fs-extra-products';
const list = JSON.parse(localStorage.getItem(KEY) || '[]');
const list = JSON.parse(sessionStorage.getItem(KEY) || '[]');
list.push({
id: 'pp-' + Date.now(),
name, cat,
@ -1768,7 +1784,7 @@ saveBtn.addEventListener('click', () => {
date: new Date().toISOString().slice(0, 10),
createdAt: Date.now(),
});
localStorage.setItem(KEY, JSON.stringify(list));
sessionStorage.setItem(KEY, JSON.stringify(list));
} catch (e) { /* storage 不可用降级到只跳转 */ }
// 不 close drawer · 跳转期间 drawer 仍覆盖 host 页面 → 视觉上彻底消除"闪商品库"

View File

@ -21,7 +21,7 @@
.product-pick-row .product-pick { flex: 0 0 240px; min-width: 240px; }
.product-pick-row .product-pick.lib { flex: 0 0 152px; min-width: 152px; min-height: 96px; }
/* 底部开始 CTA */
.wiz-start-bar { display: flex; justify-content: center; padding: 20px 0 8px; }
.wiz-start-bar { display: flex; justify-content: flex-end; padding: 20px 0 8px; }
.wiz-start-bar .btn-start { height: 44px; padding: 0 36px; background: var(--heat); color: var(--accent-white); border: 1px solid var(--heat); border-radius: 999px; font-size: 14px; font-weight: 600; cursor: pointer; box-shadow: var(--shadow-cta); display: inline-flex; align-items: center; gap: 8px; font-family: inherit; transition: box-shadow var(--t-base), opacity var(--t-base); }
.wiz-start-bar .btn-start:hover:not(.disabled) { box-shadow: var(--shadow-cta-hover); }
.wiz-start-bar .btn-start.disabled { opacity: .4; cursor: not-allowed; }
@ -121,6 +121,234 @@
.pick-section-h { display: flex; align-items: baseline; gap: 8px; margin: 14px 0 8px; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .08em; text-transform: uppercase; }
.pick-section-h .count { background: var(--background-lighter); border: 1px solid var(--border-faint); padding: 1px 6px; color: var(--black-alpha-48); font-size: 10px; }
/* ============================================================
第 1 步 · 商品选择器(沿用 products.html 商品库的卡片与 toolbar 视觉)
─ 命名空间 .pp- 前缀,避免与 products.html 冲突
============================================================ */
.pp-toolbar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; }
.pp-toolbar .search-inline {
flex: 1; min-width: 220px; max-width: 340px;
display: inline-flex; align-items: center; gap: 8px;
height: 34px; padding: 0 12px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
transition: border-color var(--t-base);
}
.pp-toolbar .search-inline:focus-within { border-color: var(--heat-40); }
.pp-toolbar .search-inline svg { width: 14px; height: 14px; color: var(--black-alpha-48); flex-shrink: 0; }
.pp-toolbar .search-inline input { flex: 1; min-width: 0; height: 100%; border: 0; outline: 0; background: transparent; font-size: 13px; color: var(--accent-black); font-family: inherit; }
.pp-toolbar .pp-chip-wrap { position: relative; }
.pp-toolbar .pp-chip {
display: inline-flex; align-items: center; gap: 6px;
height: 34px; padding: 0 12px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
font-size: 13px; font-family: inherit;
color: var(--black-alpha-72);
cursor: pointer;
transition: border-color var(--t-base), color var(--t-base);
}
.pp-toolbar .pp-chip:hover { border-color: var(--black-alpha-32); color: var(--accent-black); }
.pp-toolbar .pp-chip.active { background: var(--heat-12); color: var(--heat); border-color: var(--heat-20); }
.pp-toolbar .pp-chip svg { width: 11px; height: 11px; opacity: .6; }
.pp-toolbar .pp-menu {
position: absolute; top: calc(100% + 4px); left: 0;
min-width: 160px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
box-shadow: 0 6px 20px rgba(0,0,0,.06);
padding: 4px;
display: none;
z-index: 20;
}
.pp-toolbar .pp-chip-wrap.open .pp-menu { display: block; }
.pp-toolbar .pp-menu .mi {
display: flex; align-items: center; gap: 6px;
padding: 7px 10px;
border-radius: var(--r-sm);
font-size: 12.5px;
color: var(--accent-black);
cursor: pointer;
}
.pp-toolbar .pp-menu .mi:hover { background: var(--background-lighter); }
.pp-toolbar .pp-menu .mi.selected { color: var(--heat); font-weight: 600; }
.pp-toolbar .pp-menu .mi-check { width: 12px; height: 12px; opacity: 0; }
.pp-toolbar .pp-menu .mi.selected .mi-check { opacity: 1; }
.pp-toolbar .pp-clear {
display: inline-flex; align-items: center; gap: 4px;
height: 30px; padding: 0 10px;
background: transparent; border: 0; border-radius: var(--r-sm);
color: var(--black-alpha-56); font-size: 12.5px; font-family: inherit;
cursor: pointer;
}
.pp-toolbar .pp-clear:hover { color: var(--accent-crimson); background: var(--crimson-bg, #fdebea); }
.pp-toolbar .pp-clear svg { width: 11px; height: 11px; }
.pp-toolbar .spacer { flex: 1; }
.pp-view-tog { display: inline-flex; border: 1px solid var(--border-faint); border-radius: var(--r-md); overflow: hidden; }
.pp-view-tog button {
width: 34px; height: 34px;
background: var(--surface);
border: 0; border-right: 1px solid var(--border-faint);
cursor: pointer;
color: var(--black-alpha-48);
display: grid; place-items: center;
transition: background var(--t-base), color var(--t-base);
}
.pp-view-tog button:last-child { border-right: 0; }
.pp-view-tog button:hover { background: var(--background-lighter); color: var(--accent-black); }
.pp-view-tog button.active { background: var(--heat-12); color: var(--heat); }
.pp-view-tog button svg { width: 14px; height: 14px; }
.pp-result-meta {
font-family: var(--font-mono); font-size: 11.5px;
color: var(--black-alpha-48); letter-spacing: .02em;
margin: 4px 0 12px;
}
/* 网格 — 沿用 products.html 商品库卡片样式(复制必要部分)
固定 4 列 → 每页 8 tile(createCard + 7 商品)正好两行 */
.pp-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px; }
@media (max-width: 1100px) { .pp-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 800px) { .pp-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.pp-grid .product-card {
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
cursor: pointer; position: relative; overflow: hidden;
display: flex; flex-direction: column;
transition: background .15s, border-color .15s, transform .15s;
}
.pp-grid .product-card:hover { background: var(--background-lighter); border-color: var(--black-alpha-48); }
.pp-grid .product-card.selected { border-color: var(--heat); background: var(--heat-12); }
.pp-grid .product-card.selected::after {
content: ''; position: absolute; top: 0; right: 0;
width: 0; height: 0;
border-top: 28px solid var(--heat);
border-left: 28px solid transparent;
z-index: 2;
}
.pp-grid .product-card.selected::before {
content: ''; position: absolute; top: 4px; right: 4px;
width: 10px; height: 10px;
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23ffffff' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='3 8 7 12 13 4'/%3E%3C/svg%3E") no-repeat center / contain;
z-index: 3;
}
.pp-grid .product-thumb { aspect-ratio: 1.4 / 1; }
.pp-grid .product-body { padding: 14px 14px 12px; flex: 1; }
.pp-grid .product-name {
font-size: 14px; font-weight: 600; color: var(--accent-black);
line-height: 1.3; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pp-grid .product-cat {
display: inline-flex; align-items: center;
margin-top: 8px; padding: 2px 8px;
background: var(--background-lighter); color: var(--black-alpha-72);
border-radius: var(--r-sm); font-size: 11.5px;
}
.pp-grid .product-date {
font-family: var(--font-mono);
font-size: 11px; color: var(--black-alpha-48);
margin-top: 10px; letter-spacing: .02em;
}
.pp-grid .product-card.selected .product-cat { background: var(--surface); color: var(--heat); }
/* 创建新商品 空卡 */
.pp-grid .pp-create-card {
border: 1.5px dashed var(--black-alpha-24);
border-radius: var(--r-md);
background: transparent;
cursor: pointer;
display: flex; flex-direction: column; align-items: center; justify-content: center;
gap: 10px; min-height: 220px;
color: var(--black-alpha-48);
transition: border-color var(--t-base), color var(--t-base), background var(--t-base);
}
.pp-grid .pp-create-card:hover { border-color: var(--heat); color: var(--heat); background: var(--heat-12); }
.pp-grid .pp-create-card .pc-plus {
width: 44px; height: 44px;
border-radius: 50%;
background: var(--heat); color: var(--accent-white);
display: grid; place-items: center;
transition: filter var(--t-base);
}
.pp-grid .pp-create-card:hover .pc-plus { filter: brightness(1.06); }
.pp-grid .pp-create-card .pc-plus svg { width: 18px; height: 18px; }
.pp-grid .pp-create-card .pc-t { font-size: 13px; font-weight: 600; }
.pp-grid .pp-create-card .pc-d { font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .02em; }
/* 列表视图 */
.pp-grid.list-view { display: flex; flex-direction: column; gap: 6px; }
.pp-grid.list-view .product-card {
flex-direction: row; align-items: center;
}
.pp-grid.list-view .product-thumb { width: 96px; aspect-ratio: 1.4 / 1; flex-shrink: 0; }
.pp-grid.list-view .product-body { flex: 1; padding: 10px 14px; }
.pp-grid.list-view .pp-create-card { flex-direction: row; min-height: 56px; gap: 12px; }
.pp-grid.list-view .pp-create-card .pc-plus { width: 32px; height: 32px; }
/* 空筛选结果 */
.pp-empty {
grid-column: 1 / -1;
padding: 48px 24px; text-align: center;
border: 1px dashed var(--border-faint);
border-radius: var(--r-md);
background: var(--background-lighter);
color: var(--black-alpha-48);
font-size: 12.5px; font-family: var(--font-mono); letter-spacing: .02em;
}
.pp-empty .reset { display: inline-block; margin-top: 8px; color: var(--heat); cursor: pointer; text-decoration: underline; }
/* 分页 */
.pp-pager {
display: flex; align-items: center; gap: 16px;
margin-top: 18px; padding-top: 14px;
border-top: 1px solid var(--border-faint);
font-size: 12.5px; color: var(--black-alpha-56);
}
.pp-pager .total { font-family: var(--font-mono); letter-spacing: .02em; }
.pp-pager .pages { display: inline-flex; gap: 4px; margin-left: auto; }
.pp-pager .pages button {
min-width: 28px; height: 28px; padding: 0 8px;
border: 1px solid var(--border-faint); background: var(--surface);
border-radius: var(--r-sm);
cursor: pointer; font-size: 12.5px; color: var(--black-alpha-72); font-family: inherit;
transition: border-color var(--t-base), background var(--t-base), color var(--t-base);
}
.pp-pager .pages button:hover:not(.active):not(:disabled) { border-color: var(--black-alpha-32); color: var(--accent-black); }
.pp-pager .pages button.active { background: var(--heat); color: var(--accent-white); border-color: var(--heat); font-weight: 600; }
.pp-pager .pages button:disabled { opacity: .4; cursor: not-allowed; }
.pp-pager .pages .ellipsis {
min-width: 22px; height: 28px;
display: inline-flex; align-items: center; justify-content: center;
color: var(--black-alpha-48); font-family: var(--font-mono);
}
.pp-pager .page-size {
display: inline-flex; align-items: center; gap: 4px;
height: 28px; padding: 0 10px;
background: var(--surface); border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
cursor: pointer; font-family: inherit; font-size: 12.5px; color: var(--black-alpha-72);
}
.pp-pager .page-size:hover { border-color: var(--black-alpha-32); color: var(--accent-black); }
.pp-pager .page-size svg { width: 10px; height: 10px; opacity: .6; }
/* 底部提示 */
.pp-bottom-tip {
margin-top: 14px;
padding: 10px 14px;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
font-size: 12.5px; color: var(--black-alpha-56);
display: flex; align-items: center; gap: 8px;
}
.pp-bottom-tip svg { width: 14px; height: 14px; flex-shrink: 0; color: var(--black-alpha-48); }
.pp-bottom-tip a { color: var(--heat); cursor: pointer; text-decoration: none; }
.pp-bottom-tip a:hover { text-decoration: underline; }
/* ── Step 1 · product picker grid ── */
.product-pick-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
@media (max-width: 1100px) { .product-pick-grid { grid-template-columns: repeat(2, 1fr); } }
@ -391,13 +619,13 @@
/* ---------- data ---------- */
const PRODUCTS = [
{ id: 'mask', name: '透真玻尿酸补水面膜', cat: '美妆个护', price: 39.9, imgs: 3, points: ['透明质酸 + B5', '30g 大容量精华', '0 香精 0 酒精'], tags: ['熬夜党', '敏感肌'] },
{ id: 'earphone', name: '南卡 Lite Pro 蓝牙耳机', cat: '数码 3C', price: 199, imgs: 5, points: ['主动降噪', '32 小时续航', 'IP55 防水'], tags: ['通勤', '运动'] },
{ id: 'noodle', name: '滋啦速食牛肉面 · 6 桶装', cat: '食品饮料', price: 49.9, imgs: 4, points: ['3 分钟出餐', '真材实料牛肉', '0 防腐剂'], tags: ['加班', '独居'] },
{ id: 'sun', name: '透真清透物理防晒霜', cat: '美妆个护', price: 69, imgs: 4, points: ['SPF50 PA+++', '纯物理防晒', '不泛白不假面'], tags: ['SPF50', '通勤'] },
{ id: 'coffee', name: '三顿半同款冻干咖啡粉', cat: '食品饮料', price: 89, imgs: 6, points: ['冷热水秒溶', '意式深烘', '24 颗轻便装'], tags: ['提神', '早八'] },
{ id: 'fryer', name: '小熊 4L 可视空气炸锅', cat: '家居家电', price: 159, imgs: 5, points: ['可视化窗口', '4L 大容量', '低脂少油'], tags: ['小户型', '健康'] },
{ id: 'yoga', name: '露露同款裸感瑜伽裤', cat: '运动户外', price: 119, imgs: 8, points: ['裸感面料', '高弹回弹', '随心动随心穿'], tags: ['健身房', '通勤'] },
{ id: 'mask', name: '透真玻尿酸补水面膜', cat: '美妆个护', price: 39.9, imgs: 3, points: ['透明质酸 + B5', '30g 大容量精华', '0 香精 0 酒精'], tags: ['熬夜党', '敏感肌'], date: '2026-05-15' },
{ id: 'earphone', name: '南卡 Lite Pro 蓝牙耳机', cat: '数码 3C', price: 199, imgs: 5, points: ['主动降噪', '32 小时续航', 'IP55 防水'], tags: ['通勤', '运动'], date: '2026-05-12' },
{ id: 'noodle', name: '滋啦速食牛肉面 · 6 桶装', cat: '食品饮料', price: 49.9, imgs: 4, points: ['3 分钟出餐', '真材实料牛肉', '0 防腐剂'], tags: ['加班', '独居'], date: '2026-05-10' },
{ id: 'sun', name: '透真清透物理防晒霜', cat: '美妆个护', price: 69, imgs: 4, points: ['SPF50 PA+++', '纯物理防晒', '不泛白不假面'], tags: ['SPF50', '通勤'], date: '2026-05-08' },
{ id: 'coffee', name: '三顿半同款冻干咖啡粉', cat: '食品饮料', price: 89, imgs: 6, points: ['冷热水秒溶', '意式深烘', '24 颗轻便装'], tags: ['提神', '早八'], date: '2026-05-05' },
{ id: 'fryer', name: '小熊 4L 可视空气炸锅', cat: '家居家电', price: 159, imgs: 5, points: ['可视化窗口', '4L 大容量', '低脂少油'], tags: ['小户型', '健康'], date: '2026-05-03' },
{ id: 'yoga', name: '露露同款裸感瑜伽裤', cat: '运动户外', price: 119, imgs: 8, points: ['裸感面料', '高弹回弹', '随心动随心穿'], tags: ['健身房', '通勤'], date: '2026-04-30' },
];
const RECENT_IDS = ['mask', 'sun', 'coffee', 'earphone'];
@ -434,6 +662,37 @@
{ id: 'genz', name: '学生党', sub: 'Z 世代 18-24', metric: '平价快消', defaults: { duration: '0-10', style: 'compare' } },
];
/* ---------- 合并其它页面创建的商品 ---------- */
// 商品库 / 工作台 / 新建项目 都共用同一 drawer(assets/new-product-drawer.js),
// 该 drawer 在 save() 时把新商品写入 sessionStorage['fs-extra-products']。
// 这里在 PRODUCTS hardcoded 数据后,把 storage 中尚不在 PRODUCTS 的商品 unshift
// 到列表头部 → 用户跨页面新建的商品在 step 1 也能立即看到。
(function mergeExtraProducts() {
try {
const raw = sessionStorage.getItem('fs-extra-products');
if (!raw) return;
const list = JSON.parse(raw);
if (!Array.isArray(list) || !list.length) return;
const existingIds = new Set(PRODUCTS.map(p => p.id));
// 按 createdAt 倒序(最新在前),逐个 unshift
list.slice().sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0)).forEach(x => {
if (existingIds.has(x.id)) return;
existingIds.add(x.id);
PRODUCTS.unshift({
id: x.id,
name: x.name || '未命名商品',
cat: x.cat || '未分类',
price: null, // 表单未采集价格
imgs: Math.max(1, x.assets || 0), // 用素材数兜底,至少 1
points: Array.isArray(x.bullets) ? x.bullets : [],
tags: x.target ? String(x.target).split(/[ ,、、]+/).filter(Boolean).slice(0, 3) : [],
date: x.date || (x.createdAt ? new Date(x.createdAt).toISOString().slice(0, 10) : new Date().toISOString().slice(0, 10)),
createdAt: x.createdAt || Date.now(),
});
});
} catch (e) { /* storage 不可用就静默放弃 */ }
})();
const USER_EMAIL = 'li@shop.com';
const ACCOUNT_BALANCE = 327.40;
@ -444,6 +703,10 @@
productId: null,
pickSearch: '',
pickCat: '全部',
pickSort: 'recent', // recent | name | added
pickView: 'grid', // grid | list
pickPage: 1,
pickPageSize: 7, // 固定 4 列 × 2 行 = 8 tile,首页含 createCard 占 1 位 → 7 商品
sourceId: null,
themeText: '',
manualScript: '',
@ -523,7 +786,7 @@
state.projectName = p.name.split(' ')[0] + ' · 痛点种草 · v1';
}
state.points = {};
p.points.forEach((pt, i) => { state.points[pt] = i < 2; });
p.points.forEach(pt => { state.points[pt] = false; });
render();
}
@ -598,6 +861,7 @@
onSave: function (p) {
// p = { id, name, cat, target, points: string[], images: [...], imgs: N }
// 适配 wizard 数据结构
const now = Date.now();
const product = {
id: p.id,
name: p.name,
@ -606,6 +870,8 @@
imgs: p.imgs,
points: p.points,
tags: p.target ? p.target.split(/[ ,、、]+/).filter(Boolean).slice(0, 3) : [],
date: new Date(now).toISOString().slice(0, 10),
createdAt: now,
};
// 置顶插入,让用户立刻看到
PRODUCTS.unshift(product);
@ -683,8 +949,13 @@
window._wiz = {
selectProduct, selectSource, goNext, goPrev, jumpTo, applyPreset, startGenerate, openNewProduct,
openProdLib, closeProdLib,
setSearch: v => { state.pickSearch = v; renderStep1Only(); },
setCat: v => { state.pickCat = v; renderStep1Only(); },
setSearch: v => { state.pickSearch = v; state.pickPage = 1; renderStep1Only(); },
setCat: v => { state.pickCat = v; state.pickPage = 1; renderStep1Only(); },
setSort: v => { state.pickSort = v; state.pickPage = 1; renderStep1Only(); },
setView: v => { state.pickView = v; renderStep1Only(); },
setPage: v => { state.pickPage = Math.max(1, v); renderStep1Only(); },
setPageSize: v => { state.pickPageSize = v; state.pickPage = 1; renderStep1Only(); },
clearPickFilters: () => { state.pickSearch=''; state.pickCat='全部'; state.pickPage=1; renderStep1Only(); },
setTheme: v => { state.themeText = v; updateFootOnly(); updatePreviewLive(); },
setScript: v => { state.manualScript = v; updateFootOnly(); },
setName: v => { state.projectName = v; updatePreviewLive(); updateFootOnly(); updateRailOnly(); },
@ -749,24 +1020,108 @@
$('#rail').innerHTML = html;
}
function productPickHTML(p) {
function productCardHTML(p) {
const selected = state.productId === p.id;
return `<div class="product-pick${selected ? ' selected' : ''}" onclick="_wiz.selectProduct('${p.id}')">
<div class="placeholder thumb"><span class="ph-frame">9:16</span></div>
<div class="body">
<div class="name">${esc(p.name)}</div>
<div class="meta">${esc(p.cat)}${p.price != null ? ' · <b>¥' + p.price + '</b>' : ''} · ${p.imgs} 张图</div>
<div class="tags">${p.tags.map(t => `<span class="tag-s">${esc(t)}</span>`).join('')}</div>
return `<div class="product-card${selected ? ' selected' : ''}" onclick="_wiz.selectProduct('${p.id}')">
<div class="placeholder product-thumb"><span class="ph-frame">${esc(p.name)} · 1200×800</span></div>
<div class="product-body">
<div class="product-name">${esc(p.name)}</div>
<div class="product-cat">${esc(p.cat)}</div>
<div class="product-date">${esc(p.date || '')} 创建</div>
</div>
</div>`;
}
function pickerFilteredProducts() {
const q = (state.pickSearch || '').trim().toLowerCase();
let list = PRODUCTS.filter(p => {
if (state.pickCat !== '全部' && p.cat !== state.pickCat) return false;
if (q) {
const blob = (p.name + ' ' + p.cat + ' ' + (p.tags || []).join(' ')).toLowerCase();
if (!blob.includes(q)) return false;
}
return true;
});
if (state.pickSort === 'name') {
list = list.slice().sort((a, b) => a.name.localeCompare(b.name, 'zh'));
} else if (state.pickSort === 'added') {
list = list.slice();
} else {
// recent 排序优先级:
// 1) 用户新建的商品(有 createdAt) — 按 createdAt 倒序,最新在最前(紧挨 createCard 按钮)
// 2) RECENT_IDS 命中的商品 — 按命中顺序
// 3) 其余 — 保持原始顺序
const recent = new Map(RECENT_IDS.map((id, i) => [id, i]));
function weight(p) {
if (p.createdAt) return -p.createdAt; // 负值 → 最大的负数(最近创建)排最前
if (recent.has(p.id)) return recent.get(p.id);
return Number.POSITIVE_INFINITY;
}
list = list.slice().sort((a, b) => weight(a) - weight(b));
}
return list;
}
function pickerPagerHTML(total) {
const pageSize = state.pickPageSize;
const totalPages = Math.max(1, Math.ceil(total / pageSize));
const cur = Math.min(state.pickPage, totalPages);
function pageBtn(n) {
const act = n === cur ? ' active' : '';
return `<button type="button" class="${act.trim()}" onclick="_wiz.setPage(${n})">${n}</button>`;
}
const items = [];
items.push(`<button type="button"${cur === 1 ? ' disabled' : ''} onclick="_wiz.setPage(${cur - 1})"></button>`);
if (totalPages <= 7) {
for (let i = 1; i <= totalPages; i++) items.push(pageBtn(i));
} else {
items.push(pageBtn(1));
if (cur > 3) items.push('<span class="ellipsis"></span>');
const lo = Math.max(2, cur - 1), hi = Math.min(totalPages - 1, cur + 1);
for (let i = lo; i <= hi; i++) items.push(pageBtn(i));
if (cur < totalPages - 2) items.push('<span class="ellipsis"></span>');
items.push(pageBtn(totalPages));
}
items.push(`<button type="button"${cur === totalPages ? ' disabled' : ''} onclick="_wiz.setPage(${cur + 1})"></button>`);
return `<div class="pp-pager">
<span class="total">共 ${total} 条</span>
<div class="pages">${items.join('')}</div>
<span class="page-size">每页 ${pageSize} 条</span>
</div>`;
}
function renderStep1() {
// 已选 + 最近使用,合并去重
const ids = new Set();
if (state.productId) ids.add(state.productId);
RECENT_IDS.forEach(id => ids.add(id));
const recent = [...ids].map(id => PRODUCTS.find(p => p.id === id)).filter(Boolean);
const list = pickerFilteredProducts();
const total = list.length;
const pageSize = state.pickPageSize;
const totalPages = Math.max(1, Math.ceil(total / pageSize));
const cur = Math.min(state.pickPage, totalPages);
const pageList = list.slice((cur - 1) * pageSize, cur * pageSize);
const cats = ['全部', ...new Set(PRODUCTS.map(p => p.cat))];
const catChipActive = state.pickCat !== '全部';
const sortLabels = { recent: '最近使用', name: '商品名称', added: '添加顺序' };
const hasFilter = !!state.pickSearch || state.pickCat !== '全部';
const catMenu = cats.map(c => `<div class="mi${state.pickCat === c ? ' selected' : ''}" onclick="_wiz.setCat('${esc(c)}')">
<svg class="mi-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.6"><polyline points="3 8 7 12 13 4"/></svg>
<span>${esc(c)}</span>
</div>`).join('');
const sortMenu = Object.keys(sortLabels).map(k => `<div class="mi${state.pickSort === k ? ' selected' : ''}" onclick="_wiz.setSort('${k}')">
<svg class="mi-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.6"><polyline points="3 8 7 12 13 4"/></svg>
<span>${sortLabels[k]}</span>
</div>`).join('');
const cards = pageList.map(productCardHTML).join('');
const createCard = `<div class="pp-create-card" onclick="_wiz.openNewProduct()">
<div class="pc-plus"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg></div>
<div class="pc-t">创建新商品</div>
<div class="pc-d">// 在此添加一个新商品</div>
</div>`;
const gridContent = total === 0
? (createCard + `<div class="pp-empty">// NO MATCH<br>没有符合筛选条件的商品 <span class="reset" onclick="_wiz.clearPickFilters()">[ 清空筛选 ]</span></div>`)
: (createCard + cards);
return `<div class="wiz-pane active" data-step="1">
<div class="wiz-step-h">
@ -774,23 +1129,33 @@
<p>从商品库选一个 SKU。它的主图与卖点会被 LLM 作为脚本/资产生成的素材。</p>
</div>
<div class="pick-actions">
<button class="cap-pill" type="button" onclick="_wiz.openNewProduct()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
添加商品
</button>
<button class="cap-pill" type="button" onclick="_wiz.openProdLib()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M3 9h18M9 4v16"/></svg>
去商品库 <span style="font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; margin-left: 2px;">// 共 ${PRODUCTS.length} 个</span>
</button>
<div class="pp-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 type="text" placeholder="搜索商品名称、标签" value="${esc(state.pickSearch)}" oninput="_wiz.setSearch(this.value)">
</div>
<div class="pp-chip-wrap" data-key="cat">
<button class="pp-chip${catChipActive ? ' active' : ''}" type="button" onclick="event.stopPropagation(); this.parentElement.classList.toggle('open'); document.querySelectorAll('.pp-chip-wrap.open').forEach(w=>{if(w!==this.parentElement)w.classList.remove('open')})">
<span>${state.pickCat === '全部' ? '全部分类' : esc(state.pickCat)}</span>
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg>
</button>
<div class="pp-menu">${catMenu}</div>
</div>
${hasFilter ? `<button class="pp-clear" type="button" onclick="_wiz.clearPickFilters()">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 4l8 8M12 4l-8 8"/></svg>
清空筛选
</button>` : ''}
</div>
<div class="pick-section-h">
<span>最近使用</span>
<span class="count">${recent.length}</span>
</div>
<div class="product-pick-row">
${recent.map(productPickHTML).join('')}
<div class="pp-result-meta">// 显示 ${pageList.length} / ${total} 个商品${hasFilter ? ' (已筛选)' : ''}</div>
<div class="pp-grid${state.pickView === 'list' ? ' list-view' : ''}">${gridContent}</div>
${total > pageSize ? pickerPagerHTML(total) : ''}
<div class="pp-bottom-tip">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 8v5M12 16h.01"/></svg>
<span>找不到想要的商品?可<a onclick="_wiz.openNewProduct()">创建新商品</a>,或前往 <a href="products.html">商品库 · 管理商品</a></span>
</div>
</div>`;
}
@ -1170,12 +1535,13 @@
tmp.innerHTML = renderStep1();
active.replaceWith(tmp.firstElementChild);
}
// refocus search input
const inp = body.querySelector('.search-input input');
if (inp && document.activeElement !== inp) {
// refocus search input (旧 .search-input → 新 .pp-toolbar .search-inline)
const inp = body.querySelector('.pp-toolbar .search-inline input')
|| body.querySelector('.search-input input');
if (inp && state.pickSearch && document.activeElement !== inp) {
inp.focus();
const v = inp.value;
inp.setSelectionRange(v.length, v.length);
try { inp.setSelectionRange(v.length, v.length); } catch (e) {}
}
}
@ -1223,6 +1589,13 @@
openNewProduct();
});
// 全局点击 → 关闭 picker chip 菜单
document.addEventListener('click', e => {
if (!e.target.closest('.pp-chip-wrap')) {
document.querySelectorAll('.pp-chip-wrap.open').forEach(w => w.classList.remove('open'));
}
});
// initial render
render();
})();

View File

@ -866,6 +866,21 @@ document.getElementById('search-input').addEventListener('input', e => {
applyFilter();
});
// 从 URL ?filter=wip|done|fail|archived|all 接收外部跳转 (例如工作台 stat 卡)
(function applyUrlFilter() {
try {
const q = new URLSearchParams(location.search);
const f = q.get('filter');
if (!f) return;
const valid = ['all', 'wip', 'done', 'fail', 'archived'];
if (!valid.includes(f)) return;
state.filter = f;
document.querySelectorAll('#status-tabs .tab').forEach(t =>
t.classList.toggle('active', t.dataset.filter === f)
);
} catch (e) {}
})();
applyFilter();
// ============================================================

View File

@ -295,13 +295,6 @@
<span class="static mono" style="font-size: 11.5px; color: var(--black-alpha-48);">短信</span>
</div>
</div>
<div class="form-row">
<div class="lbl">营销 / 产品更新</div>
<div class="val">
<label class="switch"><input type="checkbox" id="n-marketing"><span class="slider"></span></label>
<span class="static mono" style="font-size: 11.5px; color: var(--black-alpha-48);">邮件</span>
</div>
</div>
</section>
<!-- ─── 创作默认 ─── -->

File diff suppressed because it is too large Load Diff