diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..97ed378 --- /dev/null +++ b/CLAUDE.md @@ -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 ` + + + +
+ + + + + +
+ + +
+ // V2 · INTERACTIVE +

流·Studio 设计系统

+

所有 token、组件、状态的可交互参照。基于 Firecrawl Playground 实测规范校准。 hover / 点击下方组件可看到真实交互反馈。

+
+ [ Restraint · v2.0 ] + · + // based on DESIGN_SPEC_V2.md +
+
+ + + + +
+
+ // §1 · COLOR TOKENS · V2.1 · FIRECRAWL-ALIGNED +

色彩系统

+

V2.1 全面对齐 Firecrawl Playground 实测色板:**冷灰底**(非米白)· **#FA5D19 主橙**(更亮一档)· **20 档 black-alpha 阶梯**(替代 11 档 ink-alpha)· **5 色 accent 多彩点**(amethyst / bluetron / crimson / forest / honey)。点击任何色块复制值。

+
+ +
+

表面 / 背景 // 4 档 · 冷灰无色相

+

告别 V2 的暖米白(#FAF9F5),全面切换 Firecrawl 的纯冷灰。--bg/--bg-soft/--card 作为 legacy 别名仍可用。

+
+
--background-base
#f9f9f9 · 页面底
copied
+
--background-lighter
#fbfbfb · 容器底
copied
+
--surface
#ffffff · 卡片
copied
+
--surface-raised
#ffffff · Modal
copied
+
+
+ +
+

边框 // 3 档 · 冷灰 · 差距极小靠语义

+

3 档相差只 1–2 个色阶,肉眼几乎看不出。**用语义,不用视觉对比**——80% 场景用 --border-faint

+
+
--border-faint
#ededed · 默认 ★
copied
+
--border-muted
#e8e8e8 · 略深
copied
+
--border-loud
#e6e6e6 · 强分隔
copied
+
+
+ +
+

主橙 Heat // 单 hue #FA5D19 + 8 档 alpha

+

从 V2 砖红 #E55B26 调亮到 Firecrawl 实测 #FA5D19(更红更饱和)。**全靠 alpha 叠加,绝不换 hue。** hover 不再切换到更深的橙,而是用 90% / 16% 这些档位组合。

+
+
--heat
#fa5d19 · 100% · CTA ★
copied
+
--heat-90
90% · hover
copied
+
--heat-40
40% · ring / edge
copied
+
--heat-20
20% · pill border / selection
copied
+
--heat-16
16% · hover bg
copied
+
--heat-12
12% · tint bg
copied
+
--heat-8
8%
copied
+
--heat-4
4% · 极弱
copied
+
+
+ +
+

Accent 多彩点 // 5 色信号 · 限定语义场景

+

**新增章节 · 取自 Firecrawl 实测**。这 5 色只用于**语义信号**(代码高亮 / info / 状态色),**禁止做大面积装饰**——全场依然只有橙色一个 accent。--accent-black 替代 V2 的 --ink #15140F(更柔和的灰黑)。

+
+
--accent-black
#262626 · 主前景
copied
+
--accent-white
#ffffff · 反色文字
copied
+
--accent-amethyst
#9061ff · 紫 / code property
copied
+
--accent-bluetron
#2a6dfb · 蓝 / info
copied
+
--accent-crimson
#eb3424 · 红 / error ★
copied
+
--accent-forest
#42c366 · 绿 / success ★
copied
+
--accent-honey
#ecb730 · 黄 / warning
copied
+
+
+ +
+

Black-Alpha 阶梯 // 20 档 · 替代 V2 的 11 档 ink-alpha

+

**核心工具尺。** 0–24% 用 rgba(0,0,0,...) 纯黑透明;**32% 起换 rgba(38,38,38,...)**(即 accent-black 作底)避免叠出"灰中带蓝"——这是 Firecrawl 的细节技巧,我们 1:1 复刻。dark mode 时这套 token 自动翻转为 white-alpha。原 --ink-alpha-* 全部作为 legacy 别名映射到对应 black-alpha。

+
+
--black-alpha-1
1%
copied
+
--black-alpha-2
2%
copied
+
--black-alpha-3
3%
copied
+
--black-alpha-4
4% · hover bg ★
copied
+
--black-alpha-5
5% · tab 分隔
copied
+
--black-alpha-6
6%
copied
+
--black-alpha-7
7% · active bg ★
copied
+
--black-alpha-8
8%
copied
+
--black-alpha-10
10%
copied
+
--black-alpha-12
12% · inside-border ★
copied
+
--black-alpha-16
16%
copied
+
--black-alpha-20
20%
copied
+
--black-alpha-24
24%
copied
+
--black-alpha-32
32% · base 切换 →
copied
+
--black-alpha-40
40%
copied
+
--black-alpha-48
48% · 占位字色 ★
copied
+
--black-alpha-56
56% · 次级文字 ★
copied
+
--black-alpha-64
64% · 描述
copied
+
--black-alpha-72
72%
copied
+
--black-alpha-88
88% · 近主前景
copied
+
+
+ +
+

状态色 // 用 accent-forest / crimson 作语义

+

V2 的 --green #3F6B3F(深森林绿)与 --red #B33A2A(暗砖红)替换为 Firecrawl 的 --accent-forest #42c366--accent-crimson #eb3424——更明亮、更接近真实"信号灯"颜色,但仍保持非荧光。

+
+
--green
#42c366 · 成功(=forest)
copied
+
--green-bg
8% · 配套底
copied
+
--green-bd
20% · 配套边
copied
+
--red
#eb3424 · 失败(=crimson)
copied
+
--red-bg
8% · 配套底
copied
+
--red-bd
20% · 配套边
copied
+
+
+ +
+

Selection 选中色 // Firecrawl 签名细节

+

页面内任何文字选中时,底色 --heat-20 + 字色 --heat。**试着选中下面这行字看效果:**

+

本月营收 ¥327,400 较上月增长 33%,有 5 个项目处于"生成中"状态。其中 2 个需要重新调整模特模板。

+
+
+ + + + +
+
+ // §2 · TYPE · V2.1 MIXED STRATEGY +

字体系统 · 中英协作

+

V2.1 改为**中英双字体协作** —— Inter 处理英文/数字,Alibaba PuHuiTi 处理中文,装饰用 JetBrains Mono。浏览器按字符级 fallthrough,Inter 不含 CJK 字形 → 中文自动跳到普惠体。**不需要 JS,中英自然分工。**

+
+ +
+

字体族原理 // browser-level character fallthrough

+
+
+
// 01 · ENGLISH
+
Inter
+
The quick brown fox jumps. 0123456789 — Vercel / Linear / Stripe 御用,屏幕 UI 优化,数字同宽。
+
+
+
// 02 · 中文
+
阿里普惠体
+
为中英混排专门设计的笔画粗细配比,与 Inter 视觉重量贴近,中文版面舒适匀质。
+
+
+
// 03 · 装饰 MONO
+
JetBrains
+
[ 200 OK ] · // 05.14 · /v2 — 仅用于装饰标签,不参与正文。
+
+
+
+
// 混排实测 · MIXED RENDERING
+
本月营收 ¥327,400 较上月 +33%,共 5 个 AI 项目处于 "生成中" 状态
+
↑ 这一行里:中文走 PuHuiTi · "¥" "327,400" "+33%" "5" "AI" 走 Inter · 字重视觉匀质,无错位
+
+
+ +
+

字号 / 字重 / 行高阶梯 // 11 档

+
+
H1 / Hero36 / 500 / -0.024em / 1.2
+
早上好,大莱
+
+
+
区块 H228 / 500 / -0.02em / 1.25
+
设计系统总览
+
+
+
KPI 数值32 / 500 / -0.02em / tabular-nums
+
¥327.40 K
+
+
+
子区 H316 / 500 / -0.01em / 1.4
+
最近项目
+
+
+
卡片标题14 / 500 / normal / 1.4
+
夏季新款蕾丝连衣裙 · 蓝色 / M
+
+
+
正文 body14 / 400 / normal / **1.65**
+
商家可能没有现成的商品图,需要新增一种「AI 生成」模式 —— 商家上传一张随手拍的原图,AI 生成 4 张满意的头图,选 1 张。
+
+
+
Label · 按钮 / Tab13 / 500 / normal
+
查看 Demo
+
+
+
描述次级13 / 400 / --ink-alpha-64 / 1.8
+
本月营收较上月增长 33%,5 个项目处于"生成中"状态,2 个需要重新调整模特模板
+
+
+
Pill 文字11.5 / 500 / normal
+
生成中
+
+
+
Inter Bold · 徽标11.5 / 700 / Inter only
+
Ctrl K · ESC · Enter · ⇧+Tab
+
+
+
Mono 标签11 / 400 / 0.04em · JetBrains
+
[ 200 OK ] [ .MP4 · 9:16 ] [ /v2 ]
+
+
+
Mono 散点装饰8.5 / 400 / 0.04em
+
· · + · +XX+ +XXXX· +X·
+
+
+ +
+

字重档位 // 仅 400 / 500 / 600 / 700

+
+
+
// REGULAR · 400
+
流·Studio · Aa Bb 123
+
+
+
// MEDIUM · 500 ★ 默认强调
+
流·Studio · Aa Bb 123
+
+
+
// SEMIBOLD · 600
+
流·Studio · Aa Bb 123
+
+
+
// BOLD · 700 · 限徽标
+
流·Studio · Aa Bb 123
+
+
+

// Bold 700 仅用于 Ctrl K 这种纯英文徽标场景,**正文严禁 700**(中英字重错位会暴露)

+
+
+ + + + +
+
+ // §3 · RADIUS +

圆角阶梯

+

V2 核心变化:统一 8 px 作为默认主圆角。完全圆 999 px 仅用于 pill 和 dot。微元素降到 4–6 px。

+
+ +
+
x
禁用 V1
0 px
+
progress
2 px
+
kbd / badge
4 px
+
小色块
6 px
+
主默认
8 px ★
+
pill
pill / dot
999 px
+
+
+ + + + +
+
+ // §4 · ICONS +

Icon 系统

+

统一 SVG line icon · stroke 1.5 · linecap round · 颜色通过 currentColor 继承。禁用 emoji / filled icon。

+
+ +
+

尺寸阶梯 // 5 档

+
+
+
+
S · 14 px
+
+
+
+
M · 16 px ★
+
+
+
+
L · 20 px
+
+
+
+
XL · 24 px
+
+
+
+
Hero · 36 px
+
+
+
+ +
+

颜色场景 // 通过 currentColor 继承

+
+
+ + 默认 ink-56 +
+
+ + hover · ink +
+
+ + active · heat +
+
+ + 在主 CTA 内 +
+
+ + disabled +
+
+
+ +
+

Icon Box // 快捷入口/Modal 头部用

+
+
+
+
+ 32×32 · 8 px 圆角 · --heat-12 底 · 16 px line icon +
+
+
+ + + + +
+
+ // §5 · BUTTONS +

按钮 · 3 类型 × 5 状态 × 3 尺寸

+

所有按钮:高 32 / 圆角 8 / 字号 13 / 字重 500。默认按钮用 ::before inside-border,hover 时边框淡出底色淡入,无布局抖动。

+
+ +
+

类型 1 · 默认 // .btn

+
// default
+
// hover
+
// active
+
// focused
+
// disabled
+
+ +
+

类型 2 · 主 CTA // .btn-primary

+

唯一允许阴影的按钮 —— 4 层橙色多重阴影,hover 时阴影向上抬起。

+
// default
+
// hover
+
// active
+
// focused
+
// disabled
+
+ +
+

类型 3 · 无框 // .btn-ghost

+
// default
+
// hover
+
// active
+
// disabled
+
+ +
+

尺寸 // -sm / 默认 / -lg

+
+ + + +
+
+ + + +
+
+
+ + + + +
+
+ // §6 · PILLS +

胶囊 · 严格 3 级分层

+

同级别尺寸必须完全一致。不允许混用。pill 永远是 999 px(完全圆),靠 dot 体现状态。

+
+ +
+

L1 · 大胶囊 // h:28 / fs:13 / dot:8

+

用于项目状态、列表行主标签。

+
+ 生成中 + 已完成 + 失败 +
+
+ +
+

L2 · 中胶囊 // h:22 / fs:11.5 / dot:6 ★ 默认

+

最常用尺寸。卡片内 / 表格内默认。

+
+ 生成中 + 200 OK + 超时 +
+
+ +
+

L3 · 小胶囊 // h:18 / fs:10.5 / dot:5

+

KPI 角标 / 行内 Mono 标签场景。

+
+ NEW + +33% + -1.2% +
+
+ +
+

对比展示 // 同色 3 级并列看大小差距

+
+ L1 · 生成中 + L2 · 生成中 + L3 · 生成中 +
+
+
+ + + + +
+
+ // §7 · INPUTS +

输入框 · 5 状态

+

同样用 inside-border。focused 时橙色 ring 2 px,error 时红色边框 + 红色软底。点击下方各字段可看到真实 focus 反馈。

+
+ +
+
// default
+
// hover
+
// focused
+
// error
+
// disabled
+
+ +
+

带图标 / 搜索 // 含 Ctrl+K 提示

+
+ + + Ctrl K +
+
+
+ + + + +
+
+ // §8 · FORM CONTROLS +

表单控件

+

Checkbox / Radio / Switch 全部可点击。disabled 状态也可演示。

+
+ +
+

Checkbox

+
+ + + + +
+
+ +
+

Radio

+
+ + + + +
+
+ +
+

Switch

+
+ + + +
+
+
+ + + + +
+
+ // §9 · TABS +

Tab · 双层结构

+

主 Tab 在区块顶部,带橙色下划线指示;副 Tab 用于过滤,带灰度→彩色 icon 反馈。点击切换。

+
+ +
+

主 Tab

+
+ + + + +
+
当前显示:全部
+
+ +
+

副 Tab · 灰度→彩色

+
+ +
+ +
+ +
+ +
+
+
+ + + + +
+
+ // §10 · CARDS +

卡片 / 快捷入口

+

所有卡片统一 8 px 圆角、1 px 边框、无阴影。快捷入口含 hover / active 状态。

+
+ + +
+ + + + +
+
+ // §11 · KPI +

统计行 · 4 格 stats

+
+ +
+
+
本月营收+33%
+
¥327.40 K
+
↑ 较上月 +33%
+
+
+
活跃项目
+
12
+
↑ 本月 +3
+
+
+
生成中RUNNING
+
5
+
+
+
+
资产总数
+
847
+
·MP4·JPG·PNG
+
+
+
+ + + + +
+
+ // §12 · LIST +

列表行

+

hover 我看整行底色变化。

+
+ +
+
+
9:16
+
夏季新款蕾丝连衣裙
// 创建于 05.14 · 蓝色 / M
+
+ 生成中 + +
+
+
4:5
+
秋季风衣 · 卡其色
// 创建于 05.12 · M / L
+
+ 已完成 + +
+
+
1:1
+
运动 T 恤 · 黑白款
// 创建于 05.10 · 全色系
+
+ 生成失败 + +
+
+
+ + + + +
+
+ // §13 · TIP +

提示框 / 进度

+
+ +
+
+
小提示
+
使用 Ctrl+K 快速搜索任意项目、商品或资产。Tab 切换不同维度,Enter 直达。
+
+
+ +
+

进度条段位 // 5 段 · 流水线专用

+
+
未开始
+
进行中(2/5)
+
已完成
+
失败
+
+
+
+ + + + + + + + + +
+
+ // §15 · TOAST +

Toast 通知

+

右下角浮出。300ms 弹性入场,2400ms 自动消失。

+
+ +
+ + +
+
+ + + + +
+
+ // §16 · SIGNATURE +

主容器装订线

+

整个工作区被左右两条 1 px 边线包夹,四角放圆弧内凹的 SVG 准星。这是流·Studio 视觉的"图纸"签名。Modal 内不必加。

+
+ +
+ + + + +
+ 这块容器左右贯穿两条 1 px 边线,四角带圆弧内凹的 SVG 准星
+ // container-demo · max-width: 720 · border-x only +
+
+
+ + + + +
+
+ // §17 · DECOR +

Mono 装饰元素 · 品牌签名

+

方括号标签 / 双斜杠注释 / 中点连接 —— 这些是流·Studio 独有的"调试视图感",Firecrawl 没有,绝对保留。

+
+ +
+

方括号标签

+
+ [ 200 OK ] + [ /v2 ] + [ .MP4 · 9:16 ] + [ STUDIO ] + [ ALL · 12 ] → +
+
+ +
+

注释样式时间戳

+
// 05.14 · 周五 · 14:32
+
+ +
+

命令路径

+
/sidebar collapse · /toast dismiss · /modal open
+
+ +
+

ASCII 散点(背景装饰)

+
+· ·  +
+·  +XX+
+ +XXXX·
+   +X· +
+
+ +
+

强调单词上色

+

+ 本月营收较上月增长 +33%,有 5 个项目处于"生成中"状态。其中 2 个需要重新调整模特模板。 +

+

// 关键名词加深一档(不变橙),橙色只留给 CTA

+
+
+ + + + +
+
+ // §18 · GUARDRAILS +

Don't List · 绝对禁止

+

任何 mockup / 代码 review 时,对照此清单。每一条违反都判错。

+
+ +
+
+ DO +
用 8 px 统一圆角 + 准星 + 装订线 + mono 装饰做"图纸感"
+
+
+ DON'T +
用 0 px 硬切角的卡片 —— V1 的做法,V2 起判错
+
+
+ DO +
橙色 hover 用 --heat-90 等 alpha 阶梯
+
+
+ DON'T +
hover 时切换到更深的橙 hex(如 #D04E1F)
+
+
+ DO +
主 CTA 用多层橙色阴影(4 层)制造发光感
+
+
+ DON'T +
用灰色阴影 / 文字阴影 / 通用 box-shadow 装饰
+
+
+ +
+
×渐变背景 —— 只有 hero 区可考虑,首选纯色。绝禁多色渐变。
+
×玻璃拟态 —— backdrop-filter 只用于 modal 遮罩。
+
×彩色 emoji —— 所有图标必须 SVG line(stroke 1.5)。
+
×多 accent 色 —— 全场只有橙色一个 accent。
+
×大圆角容器(>12 px)—— 直接判错。
+
×鲜艳荧光状态色 —— 避免霓虹绿、电光蓝、霓虹粉。
+
×居中对齐大段正文 —— 全部左对齐。
+
×装饰当主角 —— 场记板 / 丝绒 / 霓虹灯都不要。
+
×无意义微动效 —— hover 旋转、缩放、彩虹流光,禁。
+
×同行混用直角+圆角 —— 用户原话:"不要有些是直角,胶囊又是圆角"。
+
+
+ +
+
+ + + + + +
+ + + + diff --git a/电商AI平台/account.html b/电商AI平台/account.html index c465025..6889735 100644 --- a/电商AI平台/account.html +++ b/电商AI平台/account.html @@ -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 @@
- +
+ + +
@@ -256,11 +294,11 @@

消费趋势

- // 近 14 天 · 单位 ¥ + // 近 14 天 · 单位 ¥ - - - + + +
@@ -314,10 +352,20 @@
- - + + + - 8 个项目 · 当月消耗 ¥162.60 + 0 个项目 · 消耗 ¥0.00
@@ -327,22 +375,35 @@ - +
所属成员 当前阶段 状态当月消耗消耗
+
- - + + + - 5 人 · 当月合计 ¥319.00 + 0 人 · 合计 ¥0.00
@@ -350,7 +411,7 @@ - + @@ -358,16 +419,39 @@
成员 角色 已完成项目当月已用 / 月度额度已用 / 月度额度 最近活跃
+
- - - + + + + - 28 条 · + 0
@@ -384,6 +468,9 @@
+
@@ -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 `
`; }).join(''); - xax.innerHTML = TREND_DAYS.map((d, i) => i % 2 === 0 ? `${d.d.slice(3)}` : '').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 ? `${d.d.slice(3)}` : '').join(''); + } else if (_trendGrain === 'week') { + xax.innerHTML = data.map(d => `${d.d}`).join(''); + } else { + xax.innerHTML = data.map(d => `${d.d.slice(5)}`).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 ` ${p.name} @@ -511,25 +651,49 @@ function renderProjects() { ${p.owner}${r.label} ${p.stage} ${p.statusLabel} - ${p.amount === 0 ? '¥0.00' : '-' + fmtMoney(p.amount)} + ${amt === 0 ? '¥0.00' : '-' + fmtMoney(amt)} `; }).join(''); + const sum = list.reduce((s, p) => s + p.amount * mult, 0); + document.getElementById('proj-count').innerHTML = + `共 ${list.length} 个项目 · ${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 ` ${m.av}${m.name} ${r.label} ${m.projectsDone} - ${fmtMoney(m.used)} + ${fmtMoney(used)} / ${fmtMoney(m.monthly)} · ${pct.toFixed(1)}% @@ -537,12 +701,62 @@ function renderMembers() { `; }).join(''); + const sum = list.reduce((s, m) => s + m.used * mult, 0); + document.getElementById('mem-count').innerHTML = + `共 ${list.length} 人 · ${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 ` @@ -555,8 +769,37 @@ function renderBills() { `; }).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(); diff --git a/电商AI平台/asset-factory.html b/电商AI平台/asset-factory.html index b31eb3b..266133c 100644 --- a/电商AI平台/asset-factory.html +++ b/电商AI平台/asset-factory.html @@ -8,7 +8,7 @@ -
+
-
-

L3 · 小胶囊 // h:18 / fs:10.5 / dot:5

-

KPI 角标 / 行内 Mono 标签场景。

+

L3 · 小胶囊 // h:18 / fs:10.5 / dot:5 · KPI 角标

- NEW - +33% - -1.2% + NEW + +33% + -1.2%
-

对比展示 // 同色 3 级并列看大小差距

-
- L1 · 生成中 - L2 · 生成中 - L3 · 生成中 +

对比 // 同色 3 级并列

+
+ L1 · 生成中 + L2 · 生成中 + L3 · 生成中
- - - +
- // §7 · INPUTS + // §4.3 · INPUTS

输入框 · 5 状态

-

同样用 inside-border。focused 时橙色 ring 2 px,error 时红色边框 + 红色软底。点击下方各字段可看到真实 focus 反馈。

+

高 36 px / 圆角 8 / inside-border。focused 时橙色 ring 内嵌 1 px,error 时红边 + 红软底。点击下面字段看真实 focus 反馈。

// default
-
// hover
-
// focused
+
// focused
// error
-
// disabled
+
// disabled
-

带图标 / 搜索 // 含 Ctrl+K 提示

-
+

带图标 / 搜索 // 含 Ctrl K 提示

+
Ctrl K
+

// 关键坑:左侧 icon 必须 z-index: 2(否则被 input 白底盖住)· Ctrl K 用纯文本不用 ⌘(JetBrains Mono 不带该字形)

- - - +
- // §8 · FORM CONTROLS -

表单控件

-

Checkbox / Radio / Switch 全部可点击。disabled 状态也可演示。

+ // §4.4 · FORM CONTROLS +

表单控件 · Checkbox / Radio / Switch

+

全部用真 SVG indicator,禁止 border-width / rotate(45deg) 凑对勾(在缩放/字体渲染下会变形)。点击下面控件试试。

@@ -1301,82 +1474,17 @@ body{
- - - -
-
- // §9 · TABS -

Tab · 双层结构

-

主 Tab 在区块顶部,带橙色下划线指示;副 Tab 用于过滤,带灰度→彩色 icon 反馈。点击切换。

-
- -
-

主 Tab

-
- - - - -
-
当前显示:全部
-
- -
-

副 Tab · 灰度→彩色

-
- -
- -
- -
- -
-
-
- - - - -
-
- // §10 · CARDS -

卡片 / 快捷入口

-

所有卡片统一 8 px 圆角、1 px 边框、无阴影。快捷入口含 hover / active 状态。

-
- - -
- - - - +
- // §11 · KPI -

统计行 · 4 格 stats

+ // §4.5 · KPI STATS +

KPI 统计行 · 4 格

+

1 行 4 格 grid · 共用一个 inside-border 容器 · 无 gap。列与列之间用 1 px 分隔线。

-
+
-
本月营收+33%
+
本月营收+33%
¥327.40 K
↑ 较上月 +33%
@@ -1386,101 +1494,158 @@ body{
↑ 本月 +3
-
生成中RUNNING
+
生成中RUN
5
-
+
资产总数
847
-
·MP4·JPG·PNG
+
· MP4 · JPG · PNG
- - - -
+ +
- // §12 · LIST -

列表行

-

hover 我看整行底色变化。

+ // §4.6 · PROGRESS · 5 段流水线专用 +

进度条段位

+

动 = 在运行,静 = 完成/失败。脉动只给"进行中"(橙)。已完成绿、失败红、未开始灰。

-
-
+
+
未开始
+
进行中(2/5)· 脉动
+
已完成
+
失败
+
+
+ + +
+
+ // §4.7 · LIST ROW +

列表行

+

grid: 56px 1fr auto auto auto · gap 22 · padding 20·24。hover 我看整行底色变化。

+
+ +
+
9:16
夏季新款蕾丝连衣裙
// 创建于 05.14 · 蓝色 / M
-
- 生成中 +
+ 生成中
-
+
4:5
秋季风衣 · 卡其色
// 创建于 05.12 · M / L
- 已完成 + 已完成
-
+
1:1
运动 T 恤 · 黑白款
// 创建于 05.10 · 全色系
-
- 生成失败 +
+ 生成失败
- - - -
+ +
- // §13 · TIP -

提示框 / 进度

+ // §4.8 · TABS +

Tab · 双层结构

+

主 Tab 用下划线激活(2 px --heat 横线)。副 Tab 用灰度→彩色 icon 反馈。点击切换。

-
-
小提示
-
使用 Ctrl+K 快速搜索任意项目、商品或资产。Tab 切换不同维度,Enter 直达。
+

主 Tab

+
+ + + +
+
当前显示:全部
-

进度条段位 // 5 段 · 流水线专用

-
-
未开始
-
进行中(2/5)
-
已完成
-
失败
+

副 Tab · 灰度→彩色

+
+ +
+ +
+ +
+
- - - + +
+
+ // §4.10 · CARDS +

卡片 / 快捷入口

+

所有卡片统一 8 px 圆角 · 1 px 边框 · 无阴影。快捷入口含 hover / active 状态。

+
+ + +
+ + +
+
+ // §4.10 · TIP +

提示框

+

白底 · 1 px 虚线边框(dashed)· 8 px 圆角 · 加粗标题 + 正文。

+
+ +
+
小提示
+
使用 Ctrl K 快速搜索任意项目、商品或资产。Tab 切换不同维度,Enter 直达。
+
+
+ + - - - +
- // §15 · TOAST + // §4.11 · TOAST

Toast 通知

-

右下角浮出。300ms 弹性入场,2400ms 自动消失。

+

右下角浮出 · 300ms 弹性入场 · 2400ms 自动消失。触发演示:

@@ -1489,36 +1654,12 @@ body{
- - - -
-
- // §16 · SIGNATURE -

主容器装订线

-

整个工作区被左右两条 1 px 边线包夹,四角放圆弧内凹的 SVG 准星。这是流·Studio 视觉的"图纸"签名。Modal 内不必加。

-
- -
- - - - -
- 这块容器左右贯穿两条 1 px 边线,四角带圆弧内凹的 SVG 准星
- // container-demo · max-width: 720 · border-x only -
-
-
- - - - +
- // §17 · DECOR -

Mono 装饰元素 · 品牌签名

-

方括号标签 / 双斜杠注释 / 中点连接 —— 这些是流·Studio 独有的"调试视图感",Firecrawl 没有,绝对保留。

+ // §5 · MONO DECOR · 品牌签名 +

Mono 装饰元素

+

方括号标签 / 双斜杠注释 / 中点连接 —— 这些是流·Studio 独有的"调试视图感",Firecrawl 没有,绝对保留

@@ -1539,12 +1680,18 @@ body{

命令路径

-
/sidebar collapse · /toast dismiss · /modal open
+
/sidebar collapse · /toast dismiss · /modal open
+
+ +
+

数值后缀

+
¥327.40 K
+

// 主体大字 + 小字次级用 <small>

ASCII 散点(背景装饰)

-
+
· ·  +
·  +XX+
 +XXXX·
@@ -1554,24 +1701,137 @@ body{

强调单词上色

-

- 本月营收较上月增长 +33%,有 5 个项目处于"生成中"状态。其中 2 个需要重新调整模特模板。 +

+ 本月营收较上月增长 +33%,有 5 个项目处于"生成中"状态。其中 2 个需要重新调整模特模板。

// 关键名词加深一档(不变橙),橙色只留给 CTA

- - - -
+ +
- // §18 · GUARDRAILS -

Don't List · 绝对禁止

-

任何 mockup / 代码 review 时,对照此清单。每一条违反都判错。

+ // §6 · LAYOUT MODES · 5 种 +

五种页面级布局模式

+

页面骨架统一,内容区布局按用途分 5 类。新页面归类后选对应模式。

-
+
+
+
A看板型 · Dashboard
+
KPI 行 + 多个 section + 列表行 / shortcut 网格。
+
[ stats.with-corners · KPI 4 列 ]
+[ section-h ]
+[ ░░ recent-row × N ░░ ]   [ shortcut × N ]
+[ tip · 虚线提示 ]
+
代表页:index.html · projects.html(列表)· library.html
+
+ +
+
B列表 + 筛选型 · List + Filter
+
Toolbar(搜索 + chip 筛选 + 视图切换)+ 卡片网格 + 分页 + 批量栏。
+
[ toolbar: search-inline + chip-wrap × n + view-toggle ]  sticky top
+[ result-meta · 共 N 条 ]
+[ ░░ 卡片网格 / 列表 ░░ ]
+[ pagination ]                                            sticky bottom
+[ bulk-bar(选中后显示) ]                                  吸底浮动
+
代表页:products.html · projects.html · library.html · team.html
+
+ +
+
C编辑器型 · Editor / IDE
+
锁视口高度 · 多栏内部滚 · 大量页面级独有样式。⚠️ 这类定制最多 · 改之前先确认 restraint.css 没现成组件
+
[ 左栏 · 资产/导航 sticky ]  [ 中央 · 画布/参数 ]  [ 右栏 · 预览/AI 助手 ]
+└───────── 整页 viewport 高度锁定 · 不滚 main ────────┘
+
代表页:pipeline.html · model-photo.html · image-optimize.html · studio.html
+
+ +
+
D表单 / 向导型 · Wizard / Form
+
左侧 sticky 步数条 + 右侧多 pane 同时显示(非 Tab 切换)。
+
[ .wizard ]
+  ├─ .steps · 200 px sticky        ← 左步数条
+  └─ .panes · 1fr                  ← 右滚
+      ├─ .wiz-pane #step-1
+      ├─ .wiz-pane #step-2
+      └─ .wiz-pane #step-3
+
代表页:projects-new.html · products.html drawer · account.html · settings.html
+
+ +
+
E单卡型 · Single Screen
+
不渲染 sidebar/topbar · 全屏灰底 + 中心白卡。
+
       ┌─────────────────┐
+       │                 │
+       │   .input × N    │
+       │   .btn-primary  │
+       │                 │
+       └─────────────────┘
+
代表页:login.html · register.html
+
+
+
+ + +
+
+ // §7 · AUDIT · V2.1 落地待统一清单 ★ +

待统一清单

+

扫完 18 个 HTML 发现的偏离点。按影响视觉一致性程度排序。改页面时遇到这些点,顺手改正。

+
+ +
+ + + + + + + + + + + +
#问题现状应改为
1按钮等级混乱 ★各页随意选 .btn / .btn-primary / .btn-ghost,部分页面 2 个一级橙并列,"取消"被做成主橙;尺寸也乱(38/32/44 混)按 §4.1 三级体系:一个区域只 1 个一级(主动作) · 二级(并列动作) · 三级(辅助) · 尺寸默认 36 / lg 40 / sm 28
2输入框高度products 38 / projects search 32 / login 36统一 36
3Tab 激活样式product-detail 下划线 / library 底色填充统一下划线 + bottom 2 px heat
4Hover 底色多数 bg-lighter / 部分 black-alpha-4统一 black-alpha-4
5卡片标题字号projects 13.5 / products 14统一 14 / 500
6卡片网格列宽products 240 / library auto 180 / projects-new 4 列商品 240 · 资产 180 · 通用 auto-fill 220
+
+ +
+ + + + + + + + + + +
#问题现状应改为
7Pill 变体不全restraint.css 4 种 / library 用了 .pill.archive 没定义补 .pill.archive(灰)/ .pill.warn(honey)
8X 关闭按钮drawer 30 / modal 32 / toast 28统一 32×32 / 8 px 圆角
9label 字重products 500 / product-detail 600统一 500
10mono 装饰部分页有 4 角 [ 200 OK ],部分没主工作台型必有 · 编辑器型可省
11section-h 分隔部分 border-bottom / 部分没统一不用(留白即分隔)
+
+ +
+ + + + + + + + + +
#问题现状应改为
12编辑器页 inline style 过大pipeline 795 / model-photo 1393 / platform-cover 1003 行抽 .stepper / .shot-card 到 restraint.css 或 editor.css
13四角准星重复 SVG部分页定义自己的 SVG 背景全部改用 .with-corners + .corner-tr/.corner-bl
14滚动条样式残留部分页 inline scrollbar-width: thinrestraint.css 已全局隐藏 · 清理 inline
15mono 字体里的中文部分页直接写 font-family: monospace全部换 var(--font-mono)
+
+
+ + +
+
+ // §8 · GUARDRAILS +

Don't List · 绝对禁止

+

任何 mockup / 代码 review 时,对照此清单。每一条违反都判错

+
+ +
DO
用 8 px 统一圆角 + 准星 + 装订线 + mono 装饰做"图纸感"
@@ -1582,7 +1842,7 @@ body{
DO -
橙色 hover 用 --heat-90 等 alpha 阶梯
+
橙色 hover 用 --heat-90 等 alpha 阶梯
DON'T @@ -1598,17 +1858,74 @@ body{
-
-
×渐变背景 —— 只有 hero 区可考虑,首选纯色。绝禁多色渐变。
-
×玻璃拟态 —— backdrop-filter 只用于 modal 遮罩。
-
×彩色 emoji —— 所有图标必须 SVG line(stroke 1.5)。
-
×多 accent 色 —— 全场只有橙色一个 accent。
-
×大圆角容器(>12 px)—— 直接判错。
-
×鲜艳荧光状态色 —— 避免霓虹绿、电光蓝、霓虹粉。
-
×居中对齐大段正文 —— 全部左对齐。
-
×装饰当主角 —— 场记板 / 丝绒 / 霓虹灯都不要。
-
×无意义微动效 —— hover 旋转、缩放、彩虹流光,禁。
-
×同行混用直角+圆角 —— 用户原话:"不要有些是直角,胶囊又是圆角"。
+
+
×渐变背景 · 只有 hero 区可考虑,首选纯色。绝禁多色渐变。
+
×玻璃拟态 · backdrop-filter 只用于 modal 遮罩
+
×彩色 emoji / filled icon · 一律 SVG line(stroke 1.5)
+
×多 accent 色 · 全场只有橙色一个 accent
+
×大圆角容器(>12 px) · 直接判错
+
×真 border + hover 边框消失 · 用 inside-border ::before
+
×荧光鲜艳状态色 · 避免霓虹绿、电光蓝、霓虹粉
+
×居中对齐大段正文 · 全部左对齐
+
×装饰当主角 · 场记板 / 丝绒 / 霓虹灯都不要
+
×无意义微动效 · hover 旋转、缩放、彩虹流光,禁
+
×同一行混用直角和圆角 · 用户原话:"不要有些是直角,胶囊又是圆角"
+
×页面 inline style 重写共享类 · 要变体先改 restraint.css
+
×新建色值 · 必须复用现有 token
+
×⌘ Unicode 字符 · JetBrains Mono 不带该字形 · 用 "Ctrl K" 纯文本
+
+
+ + +
+
+ // §9 · CHECKLIST +

新页面 / 改页面 checklist

+

每次开工前过一遍。3 个阶段都不能跳。

+
+ +
+
// STAGE 01写代码前
+
    +
  • 已 Read [电商AI平台/design.md](design.md) §1 §3 §8(至少)
  • +
  • 用 Grep 查 restraint.css 是否已有该组件
  • +
  • 看本 design-system.html 找视觉参考
  • +
  • 不确定的设计点 → 先问用户,不要凭感觉
  • +
+
+ +
+
// STAGE 02写代码时
+
    +
  • HTML 用 app > sidebar + main > topbar + content 骨架
  • +
  • head 含 assets/restraint.css + Inter + JetBrains Mono
  • +
  • body 末尾 assets/shell.js
  • +
  • 标题区用 .page-head > h1 + .sub
  • +
  • 主操作按钮放 .page-head > .actions(自动 40 px 高)
  • +
  • 子区块用 .section-h > h2 + .more
  • +
  • 按钮全 .btn 系列 · 不要自己写
  • +
  • 状态徽标全 .pill.info/.ok/.err/.neutral
  • +
  • 输入框全 .input / .select / .textarea · 字段用 .field
  • +
  • 浮层全用现成 Modal / Drawer / Toast
  • +
  • 图标 SVG line · stroke 1.5 · linecap round · stroke="currentColor"
  • +
  • 时间戳 mono 注释 // 05.22 · 周四
  • +
  • 强调单词加深(不变橙)<b style="color:var(--accent-black)">3 个</b>
  • +
  • 数字加 .numtabular-nums
  • +
  • 状态色按语义选 --heat / --accent-forest / --accent-crimson / --accent-honey
  • +
+
+ +
+
// STAGE 03写完自检
+
    +
  • 对照 §8 Don't List 逐条过
  • +
  • 浏览器打开页面 · 截图 + 跟本 design-system.html 对比
  • +
  • 测 hover / focus / active / disabled 状态都正确
  • +
  • 测 dark mode(给 body 加 .dark class 看是否破)
  • +
  • 移动端缩到 1100 px 以下看响应式
  • +
  • 不在 master/main 上,在 dev 分支
  • +
  • 不带 --no-verify,不跳过 hook
  • +
@@ -1616,25 +1933,20 @@ body{
-
+ +
+ +
+ +
@@ -1389,39 +2314,80 @@ const state = { }; const UNIT_PRICE = 0.30; -// ─── 已选商品 渲染 (单选) ─── -function renderSelectedProds() { - const list = document.getElementById('prod-list'); - const addBtn = document.getElementById('prod-add-btn'); - const id = state.selectedProd; - const p = id ? PRODUCTS.find(x => x.id === id) : null; - if (!p) { - list.innerHTML = ''; // 不显示占位文字, 由 + 按钮提示 - if (addBtn) addBtn.hidden = false; - document.getElementById('pv-prod').textContent = '未选择'; - } else { - list.innerHTML = ` -
-
-
${p.name}
${p.cat}
- - -
- `; - list.querySelector('button.x[data-rm]').addEventListener('click', () => { - state.selectedProd = null; - renderSelectedProds(); - }); - list.querySelector('button.swap[data-swap]').addEventListener('click', () => { - addBtn.click(); // 复用 + 按钮的逻辑: 打开商品库 - }); - if (addBtn) addBtn.hidden = true; - document.getElementById('pv-prod').textContent = p.name; +// ─── 商品空间 (左侧栏) 渲染 ─── +let _psQuery = ''; +function renderProdSpace() { + const listEl = document.getElementById('ps-list'); + const ctEl = document.getElementById('ps-count'); + const allCtEl = document.getElementById('ps-all-ct'); + if (!listEl) return; + if (ctEl) ctEl.textContent = PRODUCTS.length; + if (allCtEl) allCtEl.textContent = PRODUCTS.length + ' 个'; + const q = _psQuery.trim(); + const filtered = q + ? PRODUCTS.filter(p => p.name.includes(q) || p.cat.includes(q)) + : PRODUCTS; + if (!filtered.length) { + listEl.innerHTML = `
// NO MATCH
试试其他关键词
`; + return; } + listEl.innerHTML = filtered.map(p => ` +
+
+
+
${p.name}
+
// ${p.cat}
+
+
+ `).join(''); + listEl.querySelectorAll('.mp-prod-item').forEach(el => { + el.addEventListener('click', () => selectProduct(el.dataset.id)); + }); +} + +// 选中商品 (sidebar 单选 · 同步更新表单/预览/Cost) +function selectProduct(id) { + state.selectedProd = id; + // 商品空间 active 态 + document.querySelectorAll('.mp-prod-item').forEach(el => { + el.classList.toggle('active', el.dataset.id === id); + }); + // 当前商品 header strip + updateCurProdHeader(); + // 预览区: 按商品过滤批次重渲染 + if (typeof renderBatchesForCurrentProd === 'function') renderBatchesForCurrentProd(); + // 同步 pv-summary 商品名 + const p = PRODUCTS.find(x => x.id === id); + document.getElementById('pv-prod').textContent = p ? p.name : '未选择'; + updateCost(); +} + +// 当前商品 header strip +function updateCurProdHeader() { + const nmEl = document.getElementById('cur-prod-nm'); + const statsEl = document.getElementById('cur-prod-stats'); + const batchesEl = document.getElementById('cur-prod-batches'); + if (!nmEl) return; + const p = state.selectedProd ? PRODUCTS.find(x => x.id === state.selectedProd) : null; + if (!p) { + nmEl.textContent = '未选择 · 请在左侧商品空间选一个'; + nmEl.classList.add('placeholder'); + if (statsEl) statsEl.hidden = true; + } else { + nmEl.textContent = p.name; + nmEl.classList.remove('placeholder'); + const ct = (window._countBatchesForProd ? window._countBatchesForProd(p.id) : 0); + if (batchesEl) batchesEl.textContent = ct; + if (statsEl) statsEl.hidden = false; + } +} + +// 保留旧函数名 alias (兼容旧 call site) +function renderSelectedProds() { + renderProdSpace(); + updateCurProdHeader(); + const p = state.selectedProd ? PRODUCTS.find(x => x.id === state.selectedProd) : null; + document.getElementById('pv-prod').textContent = p ? p.name : '未选择'; updateCost(); } @@ -1491,15 +2457,39 @@ function renderProdLib() { // ─── 编辑商品 drawer (在商品库内 prefill 数据) ─── // mock 商品扩展属性 (target + bullets),缺失则给默认值 const PRODUCT_EXTRA = { - p1: { target: '熬夜党 · 25-35 岁女性 · 敏感肌', bullets: ['72h 长效补水', '官方授权正品', '通勤补妆神器'] }, - p2: { target: '通勤党 · 18-30 岁 · 大学生 / 白领', bullets: ['主动降噪 35dB', '蓝牙 5.4 双设备', '32h 续航'] }, - p3: { target: '加班党 · 独居青年 · 一人食场景', bullets: ['一杯水即可', '原切牛肉块充足', '6 桶大箱装'] }, - p4: { target: '通勤防晒 · 油皮 / 敏感肌', bullets: ['SPF50+ PA++++', '物理防晒不刺激', '清透不假白'] }, - p5: { target: '咖啡入门 · 早八党 · 加班族', bullets: ['冷热水即溶', '原产地豆精选', '24 颗精装'] }, - p6: { target: '小户型 · 健康饮食 · 新手厨房', bullets: ['4L 大容量', '可视玻璃观察', '一键预设'] }, - p7: { target: '健身房 · 通勤穿搭 · 18-32 岁女性', bullets: ['裸感面料', '高弹收腹', '亲肤透气'] }, + p1: { target: '熬夜党 · 25-35 岁女性 · 敏感肌', bullets: ['72h 长效补水', '官方授权正品', '通勤补妆神器'], imgs: 6 }, + p2: { target: '通勤党 · 18-30 岁 · 大学生 / 白领', bullets: ['主动降噪 35dB', '蓝牙 5.4 双设备', '32h 续航'], imgs: 6 }, + p3: { target: '加班党 · 独居青年 · 一人食场景', bullets: ['一杯水即可', '原切牛肉块充足', '6 桶大箱装'], imgs: 6 }, + p4: { target: '通勤防晒 · 油皮 / 敏感肌', bullets: ['SPF50+ PA++++', '物理防晒不刺激', '清透不假白'], imgs: 6 }, + p5: { target: '咖啡入门 · 早八党 · 加班族', bullets: ['冷热水即溶', '原产地豆精选', '24 颗精装'], imgs: 6 }, + p6: { target: '小户型 · 健康饮食 · 新手厨房', bullets: ['4L 大容量', '可视玻璃观察', '一键预设'], imgs: 6 }, + p7: { target: '健身房 · 通勤穿搭 · 18-32 岁女性', bullets: ['裸感面料', '高弹收腹', '亲肤透气'], imgs: 6 }, }; +// 渲染商品图片 grid · n 张占位 + 上传按钮 +function renderProdImgs(n) { + const grid = document.getElementById('pcf-imgs'); + const ct = document.getElementById('pcf-imgs-ct'); + if (!grid) return; + if (ct) ct.textContent = n; + let html = ''; + for (let i = 0; i < n; i++) { + html += `
1:1
`; + } + html += `
`; + grid.innerHTML = html; + grid.querySelectorAll('.thumb .rm').forEach(btn => { + btn.addEventListener('click', () => { + btn.closest('.thumb').remove(); + if (ct) ct.textContent = grid.querySelectorAll('.thumb').length; + }); + }); + const addBtn = document.getElementById('pcf-img-add'); + if (addBtn) addBtn.addEventListener('click', () => { + if (typeof Shell !== 'undefined' && Shell.toast) Shell.toast('上传图片', '// 演示版暂不支持真实上传'); + }); +} + let _editingProdId = null; function openEditProductDrawer(id) { @@ -1510,8 +2500,10 @@ function openEditProductDrawer(id) { document.getElementById('pc-drawer-title').textContent = '编辑商品 · ' + p.name; document.getElementById('pcf-name').value = p.name; document.getElementById('pcf-cat').value = p.cat; - const extra = PRODUCT_EXTRA[id] || { target: '', bullets: [] }; + const extra = PRODUCT_EXTRA[id] || { target: '', bullets: [], imgs: 0 }; document.getElementById('pcf-target').value = extra.target || ''; + // 渲染商品图片 (n 张占位) + renderProdImgs(typeof extra.imgs === 'number' ? extra.imgs : 6); // 渲染 bullets const ul = document.getElementById('pcf-bullets'); // 移除除 .add 之外的所有 li @@ -1579,16 +2571,18 @@ document.getElementById('pc-save-btn').addEventListener('click', () => { // 写回 PRODUCTS const p = PRODUCTS.find(x => x.id === _editingProdId); if (p) { p.name = newName; p.cat = newCat; } - // 写回 PRODUCT_EXTRA + // 写回 PRODUCT_EXTRA (含 imgs 数量) const bullets = [...document.querySelectorAll('#pcf-bullets li:not(.add) input')].map(i => i.value.trim()).filter(Boolean); - PRODUCT_EXTRA[_editingProdId] = { target: newTarget, bullets }; + const imgs = document.querySelectorAll('#pcf-imgs .thumb').length; + PRODUCT_EXTRA[_editingProdId] = { target: newTarget, bullets, imgs }; Shell.toast('已保存', newName); closeEditProductDrawer(); renderProdLib(); renderSelectedProds(); }); -document.getElementById('prod-add-btn').addEventListener('click', () => { +// 全部商品 入口 (左侧栏底部 · 打开商品库 modal) +function openProdLibModal() { _plDraft = state.selectedProd; _plCatFilter = ''; _plQuery = ''; @@ -1596,6 +2590,30 @@ document.getElementById('prod-add-btn').addEventListener('click', () => { document.querySelectorAll('.pl-side-item').forEach(x => x.classList.toggle('active', x.dataset.cat === '')); renderProdLib(); document.getElementById('pl-modal-bg').classList.add('show'); +} +document.getElementById('ps-all-btn').addEventListener('click', openProdLibModal); + +// 商品空间 · 搜索框 · 新建按钮 +document.getElementById('ps-search-input').addEventListener('input', e => { + _psQuery = e.target.value; + renderProdSpace(); +}); +document.getElementById('ps-new-btn').addEventListener('click', () => { + if (!window.NewProductDrawer) { Shell.toast('Drawer 未加载'); return; } + window.NewProductDrawer.open({ + onSave: function (p) { + const product = { + id: p.id, + name: p.name, + cat: p.cat, + meta: (p.target ? p.target.split(/[ ,、、]+/)[0] : '新建') + ' · ' + p.imgs + ' 张图', + }; + PRODUCTS.unshift(product); + renderProdSpace(); + selectProduct(product.id); + Shell.toast('已加入商品库', '+ ' + product.name); + } + }); }); document.getElementById('pl-close-btn').addEventListener('click', () => { document.getElementById('pl-modal-bg').classList.remove('show'); @@ -1605,9 +2623,8 @@ document.getElementById('pl-cancel-btn').addEventListener('click', () => { }); document.getElementById('pl-confirm-btn').addEventListener('click', () => { if (!_plDraft) { Shell.toast('请先选择商品', '只能选 1 个'); return; } - state.selectedProd = _plDraft; document.getElementById('pl-modal-bg').classList.remove('show'); - renderSelectedProds(); + selectProduct(_plDraft); }); document.getElementById('pl-new-btn').addEventListener('click', () => { if (!window.NewProductDrawer) { Shell.toast('Drawer 未加载'); return; } @@ -1624,14 +2641,14 @@ document.getElementById('pl-new-btn').addEventListener('click', () => { PRODUCTS.unshift(product); // 单选: 新建商品直接选中(覆盖原选) _plDraft = product.id; - state.selectedProd = product.id; // 强制 reset filter/query,保证新商品在首位可见 _plCatFilter = ''; _plQuery = ''; const searchInput = document.getElementById('pl-search-input'); if (searchInput) searchInput.value = ''; renderProdLib(); - renderSelectedProds(); + renderProdSpace(); + selectProduct(product.id); Shell.toast('已加入商品库', '+ ' + product.name); } }); @@ -1670,21 +2687,49 @@ function updateCost() { } function selectModel(id) { state.selectedModel = (state.selectedModel === id) ? null : id; - // 同步所有出现的 model-card (mini grid + lib grid) - document.querySelectorAll('.model-card').forEach(c => + renderModelMini(); + // 同步所有出现的 model-card (lib grid 里的) + document.querySelectorAll('.ml-grid .model-card').forEach(c => c.classList.toggle('selected', c.dataset.id === state.selectedModel) ); updateModelSummary(); } -document.querySelectorAll('#model-grid-mini .model-card').forEach(card => { - card.addEventListener('click', e => { - if (e.target.closest('.m-thumb')) { - openModelDetail(card.dataset.id); - return; - } - selectModel(card.dataset.id); + +/* mini grid · 动态渲染 · 选中的模特(如不在默认 4 张里)会顶替到首位 */ +const MINI_DEFAULT_IDS = ['m1','m2','m3','m4']; +function renderModelMini() { + const grid = document.getElementById('model-grid-mini'); + if (!grid) return; + const selId = state.selectedModel; + let ids; + if (!selId || MINI_DEFAULT_IDS.includes(selId)) { + ids = MINI_DEFAULT_IDS.slice(); + } else { + ids = [selId, ...MINI_DEFAULT_IDS.slice(0, 3)]; + } + grid.innerHTML = ids.map(id => { + const m = MODELS.find(x => x.id === id); + if (!m) return ''; + const isSelected = m.id === selId; + return ` +
+
${m.name}
+
${m.name}
+
${m.gender}·${m.age}·${m.style}
+
+ `; + }).join(''); + grid.querySelectorAll('.model-card').forEach(card => { + card.addEventListener('click', e => { + if (e.target.closest('.m-thumb')) { + openModelDetail(card.dataset.id); + return; + } + selectModel(card.dataset.id); + }); }); -}); +} +// 注:首次 renderModelMini() 调用挪到 MODELS 声明之后,避免 TDZ // ─── 立即生成设置 ─── document.querySelectorAll('.pill-row').forEach(row => { row.addEventListener('click', e => { @@ -1728,20 +2773,65 @@ const RERUN_SVG = '${label || state.ratio}
-
- - -
+
${label || state.ratio}
已采用 +
+ + +
+ +
+ + +
+
+
`; - setTimeout(() => div.classList.remove('regenerating'), 1200); + // 模拟 ~1.2s 后切到 ok 状态 + setTimeout(() => { + if (div.classList.contains('gen')) div.classList.remove('gen'); + }, 1200); div.querySelector('.r-rerun').addEventListener('click', e => { e.stopPropagation(); rerunOne(div); }); - div.querySelector('.r-adopt').addEventListener('click', e => { e.stopPropagation(); adoptOne(div); }); + div.querySelector('.r-dl').addEventListener('click', e => { + e.stopPropagation(); + Shell.toast('下载', '已开始下载 · MOCK'); + }); + // 更多 menu 开/合 + const moreBtn = div.querySelector('.r-more'); + const moreWrap = div.querySelector('.cell-more-wrap'); + moreBtn.addEventListener('click', e => { + e.stopPropagation(); + const willOpen = !moreWrap.classList.contains('open'); + document.querySelectorAll('.mp-result .cell-more-wrap.open').forEach(w => w.classList.remove('open')); + if (willOpen) moreWrap.classList.add('open'); + }); + div.querySelector('.r-adopt').addEventListener('click', e => { + e.stopPropagation(); + moreWrap.classList.remove('open'); + adoptOne(div); + }); + div.querySelector('.r-del').addEventListener('click', e => { + e.stopPropagation(); + moreWrap.classList.remove('open'); + const batch = div.closest('.mp-result-batch'); + div.remove(); + if (batch) { + // 如果该批次空了, 整批移除 + if (!batch.querySelectorAll('.mp-result:not(.placeholder-only)').length) batch.remove(); + else updateBatchSummary(); + } + Shell.toast('已删除'); + }); return div; } function appendBatch(n, kind) { @@ -1754,42 +2844,85 @@ function appendBatch(n, kind) { const tsStr = `${String(ts.getHours()).padStart(2,'0')}:${String(ts.getMinutes()).padStart(2,'0')}:${String(ts.getSeconds()).padStart(2,'0')}`; const labCls = kind === 'gen' ? 'gen' : 'rerun'; const labTxt = kind === 'gen' ? `批次 ${_batchSeq} · 初始生成` : (kind === 'rerun-all' ? `批次 ${_batchSeq} · 全部重跑` : `批次 ${_batchSeq} · 单张重跑`); + const _curModel = state.selectedModel ? MODELS.find(x => x.id === state.selectedModel) : null; + batch.dataset.ts = String(ts.getTime()); + batch.dataset.modelId = _curModel ? _curModel.id : ''; + batch.dataset.modelName = _curModel ? _curModel.name : ''; + batch.dataset.ratio = state.ratio || ''; + batch.dataset.search = [ + labTxt, _curModel ? _curModel.name : '', + _curModel ? _curModel.style : '', + state.ratio || '', n + '张' + ].join(' ').toLowerCase(); batch.innerHTML = `
${labTxt} · ${n} 张 · ${state.ratio} + ${_curModel ? `·${_curModel.name}` : ''} · ${tsStr}
- +
+ + +
`; const gridInner = batch.querySelector('.mp-result-grid'); for (let i = 0; i < n; i++) gridInner.appendChild(buildResultCard()); batch.querySelector('.rerun-batch').addEventListener('click', () => { appendBatch(n, 'rerun-all'); - Shell.toast('全部重跑', n + ' 张图重新生成中 · 新批次已追加'); + Shell.toast('再次生成', n + ' 张图重新生成中 · 新批次已追加'); }); batch.querySelector('.adopt-batch').addEventListener('click', () => { const cards = batch.querySelectorAll('.mp-result:not(.adopted)'); if (!cards.length) { Shell.toast('该批次已全部采用'); return; } - cards.forEach(c => { c.classList.remove('regenerating'); c.classList.add('adopted'); }); + cards.forEach(c => { c.classList.remove('gen'); c.classList.add('adopted'); }); updateBatchSummary(); Shell.toast('已全部采用', cards.length + ' 张图入对应商品的 AI 素材 · 扣 ¥' + (cards.length * UNIT_PRICE).toFixed(2)); }); + // 批次「更多」按钮 → 开/合 menu + const _bMoreBtn = batch.querySelector('.batch-more'); + const _bMoreWrap = batch.querySelector('.batch-more-wrap'); + if (_bMoreBtn && _bMoreWrap) { + _bMoreBtn.addEventListener('click', e => { + e.stopPropagation(); + const willOpen = !_bMoreWrap.classList.contains('open'); + document.querySelectorAll('.mp-pv-batch .batch-more-wrap.open').forEach(w => w.classList.remove('open')); + if (willOpen) _bMoreWrap.classList.add('open'); + }); + } + batch.querySelector('.batch-save-all').addEventListener('click', e => { + e.stopPropagation(); + if (_bMoreWrap) _bMoreWrap.classList.remove('open'); + batch.querySelector('.adopt-batch').click(); + }); + batch.querySelector('.batch-del').addEventListener('click', e => { + e.stopPropagation(); + if (_bMoreWrap) _bMoreWrap.classList.remove('open'); + batch.remove(); + updateBatchSummary(); + Shell.toast('已删除该批结果'); + }); grid.appendChild(batch); batch.scrollIntoView({ behavior: 'smooth', block: 'end' }); updateBatchSummary(); + if (typeof _refreshModelMenu === 'function') _refreshModelMenu(); + if (typeof applyPvFilters === 'function') applyPvFilters(); } function renderResultCards(n) { const grid = document.getElementById('pv-grid'); @@ -1805,11 +2938,20 @@ function rerunOne(card) { } function adoptOne(card) { if (!card || card.classList.contains('adopted')) return; - card.classList.remove('regenerating'); + card.classList.remove('gen'); card.classList.add('adopted'); - Shell.toast('已采用', '入对应商品的 AI 素材 · 扣 ¥' + UNIT_PRICE.toFixed(2)); + Shell.toast('已加入资产库', '入对应商品的 AI 素材 · 扣 ¥' + UNIT_PRICE.toFixed(2)); updateBatchSummary(); } +// 点击页面其它位置 → 关闭单图/批次 more menu +document.addEventListener('click', e => { + if (!e.target.closest('.mp-result .cell-more-wrap')) { + document.querySelectorAll('.mp-result .cell-more-wrap.open').forEach(w => w.classList.remove('open')); + } + if (!e.target.closest('.mp-pv-batch .batch-more-wrap')) { + document.querySelectorAll('.mp-pv-batch .batch-more-wrap.open').forEach(w => w.classList.remove('open')); + } +}); function updateBatchSummary() { // 逐批次更新「全部采用 · 已采用/总数」 document.querySelectorAll('#pv-grid .mp-result-batch').forEach(batch => { @@ -1824,13 +2966,13 @@ function updateBatchSummary() { document.getElementById('mp-go-btn').addEventListener('click', () => { if (!state.selectedModel || !state.selectedProd) return; - // 已有真实批次时,语义是追加新批次(用户换了设置再次生成) const grid = document.getElementById('pv-grid'); const hasReal = grid && grid.querySelector('.mp-result-batch:not(.placeholder-batch)'); + const prod = PRODUCTS.find(p => p.id === state.selectedProd); if (hasReal) { Shell.toast('已追加批次', state.count + ' 张图新增到下方 · 旧批次保留'); } else { - Shell.toast('已提交任务', '可在 [任务中心] 查看进度'); + Shell.toast('已提交任务', (prod ? prod.name + ' · ' : '') + state.count + ' 张图生成中'); } showPreviewContent(); renderResultCards(state.count); @@ -1839,6 +2981,152 @@ document.getElementById('mp-go-btn').addEventListener('click', () => { // 批量按钮已下沉到每个批次内部 (.rerun-batch / .adopt-batch) // 不再有全局 #pv-rerun-all / #pv-adopt-all +// ============================================================ +// 工具台头部 · 搜索 / 时间 / 模特 筛选 +// ============================================================ +const _pvFilter = { time: 'all', model: 'all', search: '' }; + +function _pvTimeMatch(ts, key) { + if (key === 'all') return true; + const now = Date.now(); + const diff = now - Number(ts); + 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(ts)); + return a.toDateString() === b.toDateString(); + } + return true; +} + +function applyPvFilters() { + const grid = document.getElementById('pv-grid'); + if (!grid) return; + const q = (_pvFilter.search || '').trim().toLowerCase(); + let visible = 0; + grid.querySelectorAll('.mp-result-batch:not(.placeholder-batch)').forEach(batch => { + let ok = true; + if (!_pvTimeMatch(batch.dataset.ts, _pvFilter.time)) ok = false; + if (ok && _pvFilter.model !== 'all' && batch.dataset.modelId !== _pvFilter.model) ok = false; + if (ok && q && !(batch.dataset.search || '').includes(q)) ok = false; + batch.dataset.hidden = ok ? '0' : '1'; + if (ok) visible += 1; + }); + // 占位批次:只有当无真实批次 & 无 active filter 时显示 + const hasReal = !!grid.querySelector('.mp-result-batch:not(.placeholder-batch)'); + const filterActive = _pvFilter.time !== 'all' || _pvFilter.model !== 'all' || q.length > 0; + grid.querySelectorAll('.placeholder-batch').forEach(ph => { + ph.dataset.hidden = (hasReal || filterActive) ? '1' : '0'; + }); +} + +function _refreshModelMenu() { + const menu = document.getElementById('mp-menu-model'); + if (!menu) return; + const grid = document.getElementById('pv-grid'); + const used = new Map(); + if (grid) { + grid.querySelectorAll('.mp-result-batch:not(.placeholder-batch)').forEach(b => { + const id = b.dataset.modelId; const nm = b.dataset.modelName; + if (id) used.set(id, nm || id); + }); + } + const items = ['']; + if (used.size === 0) { + items.push('
暂无批次,生成后可按模特筛选
'); + } else { + used.forEach((nm, id) => { + items.push(``); + }); + } + menu.innerHTML = items.join(''); +} + +function _setChipLabel(chipId, baseLabel, val, valText) { + const lbl = document.querySelector('#' + chipId + ' .lbl'); + const chip = document.getElementById(chipId); + if (!lbl || !chip) return; + if (val === 'all' || !val) { + lbl.textContent = baseLabel; + chip.classList.remove('active'); + } else { + lbl.textContent = baseLabel; + chip.classList.add('active'); + // 触发 ::after ':' 伪元素显示已选项 + chip.title = baseLabel + ':' + valText; + } +} + +function _closeAllMenus(except) { + document.querySelectorAll('.mp-main-h .tb-menu-wrap.open').forEach(w => { + if (w !== except) w.classList.remove('open'); + }); +} + +// chip 点击 → 开/合菜单 +document.querySelectorAll('.mp-main-h .tb-menu-wrap').forEach(wrap => { + const chip = wrap.querySelector('.tb-chip'); + chip.addEventListener('click', e => { + e.stopPropagation(); + const willOpen = !wrap.classList.contains('open'); + _closeAllMenus(wrap); + wrap.classList.toggle('open', willOpen); + if (willOpen && wrap.dataset.filter === 'model') _refreshModelMenu(); + }); +}); +// 菜单项 → 选中并应用 +document.querySelectorAll('#mp-menu-time, #mp-menu-model').forEach(menu => { + menu.addEventListener('click', e => { + const btn = e.target.closest('.tb-menu-item'); + if (!btn) return; + const val = btn.dataset.val; + const txt = btn.textContent.trim(); + const wrap = menu.closest('.tb-menu-wrap'); + const key = wrap.dataset.filter; + _pvFilter[key] = val; + menu.querySelectorAll('.tb-menu-item').forEach(it => it.classList.toggle('active', it === btn)); + wrap.classList.remove('open'); + const baseLabel = key === 'time' ? '时间' : '模特'; + _setChipLabel(key === 'time' ? 'mp-chip-time' : 'mp-chip-model', baseLabel, val, txt); + applyPvFilters(); + }); +}); +// 点击页面其它位置关闭菜单 +document.addEventListener('click', e => { + if (!e.target.closest('.mp-main-h .tb-menu-wrap')) _closeAllMenus(null); +}); + +// 搜索 toggle + 输入 +(function setupSearch() { + const wrap = document.getElementById('mp-search-wrap'); + const toggle = document.getElementById('mp-search-toggle'); + const input = document.getElementById('mp-search-input'); + if (!wrap || !toggle || !input) return; + toggle.addEventListener('click', e => { + e.stopPropagation(); + const willExpand = !wrap.classList.contains('expanded'); + wrap.classList.toggle('expanded', willExpand); + if (willExpand) setTimeout(() => input.focus(), 50); + else { + input.value = ''; + _pvFilter.search = ''; + applyPvFilters(); + } + }); + input.addEventListener('input', () => { + _pvFilter.search = input.value; + applyPvFilters(); + }); + input.addEventListener('keydown', e => { + if (e.key === 'Escape') { + input.value = ''; + _pvFilter.search = ''; + wrap.classList.remove('expanded'); + applyPvFilters(); + } + }); +})(); + // ─── 模特库 全屏 ─── const MODELS = [ { id: 'm1', name: 'Ava', gender: '女', age: '青年', style: '清新自然', source: 'preset', used: 12, region: '东亚', skin: '白皙', height: '中等', build: '标准', hairLen: '长发', hairColor: '黑', vibe: '温柔', feature: '邻家女孩气质,微笑亲和' }, @@ -1861,16 +3149,30 @@ function renderModelLib(filter) { if (filter.source && filter.source !== 'all') list = list.filter(m => m.source === filter.source); if (filter.gender) list = list.filter(m => m.gender === filter.gender); if (filter.age) list = list.filter(m => m.age === filter.age); - grid.innerHTML = list.map(m => ` + + // 「添加模特」入口卡 · 平台预设是只读素材库,不展示入口 + const uploadCard = (filter.source === 'preset') ? '' : ` +
+
+
+ +
+
+
添加模特
+
// AI 生成 / 本地上传
+
+ `; + + grid.innerHTML = uploadCard + list.map(m => `
-
${m.name} · ${m.style}
${m.name}
${m.gender}·${m.age}·${m.style}
`).join(''); - // 绑定 click (单选) - grid.querySelectorAll('.model-card').forEach(card => { + + // 绑定 click (单选) · 排除上传卡片 + grid.querySelectorAll('.model-card:not(.ml-upload-card)').forEach(card => { card.addEventListener('click', e => { if (e.target.closest('.m-thumb')) { openModelDetail(card.dataset.id); @@ -1879,8 +3181,171 @@ function renderModelLib(filter) { selectModel(card.dataset.id); }); }); + + // 上传卡点击 → 打开选择 modal + const upCard = grid.querySelector('#ml-upload-card'); + if (upCard) { + upCard.addEventListener('click', () => openUploadChoice()); + upCard.addEventListener('keydown', e => { + if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openUploadChoice(); } + }); + } } +/* ─── 添加模特 · 工作台画布(取代原 modal) ─── */ +const _uploadCanvas = document.getElementById('ml-canvas'); +function openUploadChoice() { + _uploadCanvas.classList.add('show'); + _uploadCanvas.setAttribute('aria-hidden', 'false'); +} +function closeUploadChoice() { + _uploadCanvas.classList.remove('show'); + _uploadCanvas.setAttribute('aria-hidden', 'true'); +} +document.getElementById('ml-canvas-x').addEventListener('click', closeUploadChoice); +document.getElementById('ml-canvas-back').addEventListener('click', closeUploadChoice); +document.addEventListener('keydown', e => { + if (e.key === 'Escape' && _uploadCanvas.classList.contains('show')) closeUploadChoice(); +}); + +/* ─── 工作台画布 · 左 AI 生成 区 ─── */ +const _mcInputText = document.getElementById('mc-input-text'); +const _mcSendBtn = document.getElementById('mc-send-btn'); +_mcInputText?.addEventListener('input', () => { + _mcSendBtn.disabled = _mcInputText.value.trim().length === 0; + // textarea 自适应高度 + _mcInputText.style.height = 'auto'; + _mcInputText.style.height = Math.min(_mcInputText.scrollHeight, 160) + 'px'; +}); +_mcSendBtn?.addEventListener('click', () => { + const txt = _mcInputText.value.trim(); + if (!txt) return; + Shell.toast('AI 生成已排队', '约 12s · 完成后写入「我的上传」'); + _mcInputText.value = ''; + _mcInputText.style.height = 'auto'; + _mcSendBtn.disabled = true; +}); +// 示例 chip → 填入 textarea +document.querySelectorAll('.mc-empty .examples .ex').forEach(b => { + b.addEventListener('click', () => { + _mcInputText.value = b.dataset.ex || b.textContent.trim(); + _mcInputText.dispatchEvent(new Event('input')); + _mcInputText.focus(); + }); +}); +// + 按钮上传参考图(仅作展示参考,不入库) +const _mcAiRefInput = document.getElementById('mc-ai-ref-input'); +const _mcRefs = document.getElementById('mc-input-refs'); +let _mcRefList = []; +document.getElementById('mc-add-btn')?.addEventListener('click', () => _mcAiRefInput.click()); +_mcAiRefInput?.addEventListener('change', e => { + const files = [...(e.target.files || [])].filter(f => /^image\//.test(f.type)); + files.forEach(f => _mcRefList.push({ name: f.name, url: URL.createObjectURL(f) })); + e.target.value = ''; + _renderMcRefs(); +}); +function _renderMcRefs() { + if (!_mcRefs) return; + _mcRefs.classList.toggle('show', _mcRefList.length > 0); + _mcRefs.innerHTML = _mcRefList.map((r, i) => ` +
+ ${r.name} + +
`).join(''); + _mcRefs.querySelectorAll('.x').forEach(x => x.addEventListener('click', () => { + _mcRefList.splice(+x.dataset.idx, 1); + _renderMcRefs(); + })); +} + +/* ─── 工作台画布 · 右 本地上传 区(累积多张) ─── */ +const _mcUpInput = document.getElementById('mc-up-input'); +const _mcUpDrop = document.getElementById('mc-up-drop'); +const _mcUpList = document.getElementById('mc-up-list'); +const _mcUpCount = document.getElementById('mc-up-count'); +const _mcUpStat = document.getElementById('mc-up-stat'); +const _mcUpCommit = document.getElementById('mc-up-commit'); +const _mcUpClear = document.getElementById('mc-up-clear'); +let _mcUpFiles = []; // [{ file, url, name, size }] + +function _renderMcUp() { + _mcUpList.innerHTML = _mcUpFiles.map((f, i) => ` +
+ ${f.name} + +
${f.name}
+
`).join(''); + _mcUpList.querySelectorAll('.x').forEach(x => x.addEventListener('click', () => { + _mcUpFiles.splice(+x.dataset.idx, 1); + _renderMcUp(); + })); + const n = _mcUpFiles.length; + const mb = _mcUpFiles.reduce((s, f) => s + f.size, 0) / (1024 * 1024); + _mcUpCount.textContent = n; + _mcUpStat.innerHTML = `${n} 张 · ${mb.toFixed(1)} MB`; + _mcUpCommit.disabled = n === 0; + _mcUpClear.hidden = n === 0; +} + +function _mcAddFiles(rawFiles) { + const files = [...(rawFiles || [])].filter(f => /^image\//.test(f.type)); + if (!files.length) return; + files.forEach(f => _mcUpFiles.push({ + file: f, url: URL.createObjectURL(f), name: f.name, size: f.size, + })); + _renderMcUp(); +} + +_mcUpDrop?.addEventListener('click', () => _mcUpInput.click()); +_mcUpDrop?.addEventListener('keydown', e => { + if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); _mcUpInput.click(); } +}); +_mcUpInput?.addEventListener('change', e => { + _mcAddFiles(e.target.files); + e.target.value = ''; +}); +['dragenter', 'dragover'].forEach(ev => _mcUpDrop?.addEventListener(ev, e => { + e.preventDefault(); _mcUpDrop.classList.add('dragover'); +})); +['dragleave', 'drop'].forEach(ev => _mcUpDrop?.addEventListener(ev, e => { + e.preventDefault(); _mcUpDrop.classList.remove('dragover'); +})); +_mcUpDrop?.addEventListener('drop', e => { + _mcAddFiles(e.dataTransfer?.files); +}); + +_mcUpClear?.addEventListener('click', () => { _mcUpFiles = []; _renderMcUp(); }); + +_mcUpCommit?.addEventListener('click', () => { + if (_mcUpFiles.length === 0) return; + const n = _mcUpFiles.length; + _mcUpFiles.forEach((f, i) => { + const baseName = (f.name || 'YouNew').replace(/\.[^.]+$/, '').slice(0, 8); + const ts = Date.now().toString(36) + i; + MODELS.unshift({ + id: 'm-up-' + ts, + name: baseName || 'YouNew', + gender: '女', age: '青年', style: '我的模特', + source: 'own', used: 0, + region: '—', skin: '—', height: '—', build: '—', + hairLen: '—', hairColor: '—', vibe: '—', + feature: '用户上传素材 · 等待生成三视图', + }); + }); + _mcUpFiles = []; + _renderMcUp(); + // 切回「我的上传」+ 重渲染列表 + 关闭画布 + _libFilter.source = 'own'; + document.querySelectorAll('.ml-side .ml-side-item').forEach(x => + x.classList.toggle('active', x.dataset.source === 'own')); + renderModelLib(_libFilter); + closeUploadChoice(); + Shell.toast('已加入模特库', `+ ${n} 张 · 来源 我的上传`); +}); + +/* 兼容旧 modal:#ml-up-file 仍存在,但当前未使用(保留以防外部调用) */ +const _uploadFileInput = document.getElementById('ml-up-file'); + let _libFilter = { source: 'all', gender: '', age: '' }; document.getElementById('open-model-lib').addEventListener('click', () => { renderModelLib(_libFilter); @@ -1960,15 +3425,27 @@ document.getElementById('md-close-btn').addEventListener('click', () => { document.getElementById('md-modal-bg').classList.remove('show'); }); document.getElementById('md-select').addEventListener('click', () => { + let pickedName = ''; if (_detailModelId) { state.selectedModel = _detailModelId; - // 同步所有 model-card (mini + lib) - document.querySelectorAll('.model-card').forEach(c => + // 重建 mini 网格 · 让选中的模特(无论是否预设)显示在首位 + renderModelMini(); + // 同步模特库 modal 网格里的 .selected 类 + document.querySelectorAll('.ml-grid .model-card').forEach(c => c.classList.toggle('selected', c.dataset.id === state.selectedModel) ); updateModelSummary(); + const m = MODELS.find(x => x.id === _detailModelId); + if (m) pickedName = m.name; } + // 关闭模特详情 + 模特库两个 modal,回到工作台主视图 document.getElementById('md-modal-bg').classList.remove('show'); + document.getElementById('ml-modal-bg').classList.remove('show'); + // 工作台主区域 scroll 到「选择模特」step,可视化反馈 + document.getElementById('model-grid-mini')?.scrollIntoView({ behavior: 'smooth', block: 'center' }); + if (pickedName && window.Shell?.toast) { + Shell.toast('已选用模特「' + pickedName + '」', '可继续选择服装与场景'); + } }); // pv-swap 也打开模特库 @@ -1989,14 +3466,8 @@ document.getElementById('pv-swap').addEventListener('click', () => { state.selectedProd = p.id; })(); -// 初始化 -renderSelectedProds(); -updateModelSummary(); -updateCost(); -showPreviewEmpty(); // 默认新任务态 → 右侧显示空态 - /* ============================================================ - 任务历史 (localStorage) · 仅在「立即生成」时创建任务 + 生成批次 (localStorage) · 按当前商品过滤 · 立即生成时追加 ============================================================ */ (function () { 'use strict'; @@ -2004,58 +3475,6 @@ showPreviewEmpty(); // 默认新任务态 → 右侧显示空态 const KEY = 'fs-image-tasks-' + TASK_TYPE; let tasks = []; - let currentId = null; - let _draftSnap = null; // 临时新任务的状态缓存 (切到真实任务前保存, 切回时恢复) - - function saveDraftSnap() { - _draftSnap = { - selectedProd: state.selectedProd, - selectedModel: state.selectedModel, - count: state.count, - ratio: state.ratio, - }; - } - function resetPreviewToPlaceholder() { - const grid = document.getElementById('pv-grid'); - if (grid) { - grid.innerHTML = ''; - const wrap = document.createElement('div'); - wrap.className = 'mp-result-batch placeholder-batch'; - const inner = document.createElement('div'); - inner.className = 'mp-result-grid'; - for (let i = 0; i < 4; i++) { - const div = document.createElement('div'); - div.className = 'mp-result placeholder-only'; - div.innerHTML = `
待生成 · 1:1
`; - inner.appendChild(div); - } - wrap.appendChild(inner); - grid.appendChild(wrap); - } - _batchSeq = 0; - } - function applyDraftToForm() { - // 从 _draftSnap 恢复 (没缓存就用默认空态) - const snap = _draftSnap || { selectedProd: null, selectedModel: null, count: 4, ratio: '1:1' }; - state.selectedProd = snap.selectedProd; - state.selectedModel = snap.selectedModel; - state.count = snap.count; - state.ratio = snap.ratio; - document.querySelectorAll('.pill-row[data-key="count"] .opt').forEach(b => b.classList.toggle('active', +b.dataset.val === state.count)); - document.querySelectorAll('.pill-row[data-key="ratio"] .opt').forEach(b => b.classList.toggle('active', b.dataset.val === state.ratio)); - document.querySelectorAll('.model-card').forEach(c => c.classList.toggle('selected', c.dataset.id === state.selectedModel)); - renderSelectedProds(); - updateModelSummary(); - updateCost(); - resetPreviewToPlaceholder(); - showPreviewEmpty(); // 临时任务态: 右侧空态 - } - function backToDraft() { - if (currentId === null) return; // 已经在 draft 态, 无需切换 - applyDraftToForm(); - currentId = null; - renderTasksList(); - } function load() { try { return JSON.parse(localStorage.getItem(KEY) || '[]'); } catch (e) { return []; } @@ -2063,10 +3482,6 @@ showPreviewEmpty(); // 默认新任务态 → 右侧显示空态 function save(arr) { try { localStorage.setItem(KEY, JSON.stringify(arr)); } catch (e) {} } - function escapeHtml(s) { - return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); - } - function buildSnapshot() { return { selectedProd: state.selectedProd, @@ -2075,147 +3490,62 @@ showPreviewEmpty(); // 默认新任务态 → 右侧显示空态 ratio: state.ratio, }; } - function autoName(snap) { - const prod = snap.selectedProd ? PRODUCTS.find(p => p.id === snap.selectedProd) : null; - const model = snap.selectedModel ? MODELS.find(m => m.id === snap.selectedModel) : null; - const left = prod ? (prod.name.length > 10 ? prod.name.slice(0, 10) + '…' : prod.name) : '未选商品'; - const right = model ? model.name : '未选模特'; - return `${left} × ${right}`; - } function timeNow() { const d = new Date(); return ('0'+(d.getMonth()+1)).slice(-2) + '.' + ('0'+d.getDate()).slice(-2) + ' ' + ('0'+d.getHours()).slice(-2) + ':' + ('0'+d.getMinutes()).slice(-2); } - function renderTasksList() { - const root = document.getElementById('tasks-list'); - document.getElementById('tasks-count').textContent = tasks.length; + // 暴露给上层 (给 cur-prod header 用) + window._countBatchesForProd = function (prodId) { + return tasks.filter(t => t.snap && t.snap.selectedProd === prodId).length; + }; - // 临时新任务卡 — 仅在 draft 态 (currentId=null) 或有暂存的 _draftSnap 时显示 - // 立即生成后 currentId 指向真实任务,_draftSnap 也未设置 → 不自动追加新草稿 - const isDraft = (currentId === null); - const hasDraft = isDraft || _draftSnap !== null; - const draftHtml = hasDraft ? ` -
-
新任务
-
- 编辑中 -
-
- ` : ''; + // 切换商品 → 清空 pv-grid (历史批次只在当前 session 持有) + // cur-prod header 的"本商品 N 批"由 _countBatchesForProd 提供累计数 + window.renderBatchesForCurrentProd = function () { + const grid = document.getElementById('pv-grid'); + if (!grid) return; + grid.innerHTML = ''; + _batchSeq = 0; + showPreviewEmpty(); + }; - const STATUS_LABEL = { gen: '生成中', ok: '已完成', err: '失败' }; - const sorted = [...tasks].sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0)); - const savedHtml = sorted.map(t => ` -
-
${escapeHtml(t.name)}
-
- ${STATUS_LABEL[t.status] || t.status} - ${escapeHtml(t.time || '')} -
- -
- `).join(''); - root.innerHTML = draftHtml + savedHtml; - // draft 卡点击 → 切回临时态 - const draftCard = root.querySelector('.mp-task-card.draft'); - if (draftCard) draftCard.addEventListener('click', backToDraft); - // 真实任务卡点击 → loadTaskIntoForm - root.querySelectorAll('.mp-task-card:not(.draft)').forEach(card => { - card.addEventListener('click', e => { - if (e.target.closest('button.x')) return; - loadTaskIntoForm(card.dataset.id); - }); - }); - root.querySelectorAll('button.x[data-rm]').forEach(btn => { - btn.addEventListener('click', e => { - e.stopPropagation(); - const id = btn.dataset.rm; - const idx = tasks.findIndex(t => t.id === id); - if (idx < 0) return; - const name = tasks[idx].name; - tasks.splice(idx, 1); - save(tasks); - if (currentId === id) currentId = null; - renderTasksList(); - Shell.toast('已删除任务', name); - }); - }); - } - - /* ---------- 把历史任务还原到表单 ---------- */ - function loadTaskIntoForm(id) { - const t = tasks.find(x => x.id === id); - if (!t) return; - if (currentId === null) saveDraftSnap(); // 切走 draft 前保存当前编辑状态 - state.selectedProd = t.snap.selectedProd || null; - state.selectedModel = t.snap.selectedModel || null; - state.count = t.snap.count; - state.ratio = t.snap.ratio; - document.querySelectorAll('.pill-row[data-key="count"] .opt').forEach(b => b.classList.toggle('active', +b.dataset.val === state.count)); - document.querySelectorAll('.pill-row[data-key="ratio"] .opt').forEach(b => b.classList.toggle('active', b.dataset.val === state.ratio)); - document.querySelectorAll('.model-card').forEach(c => c.classList.toggle('selected', c.dataset.id === state.selectedModel)); - renderSelectedProds(); - updateModelSummary(); - updateCost(); - showPreviewContent(); // 真实任务: 显示预览内容 - renderResultCards(state.count);// 重新渲染结果卡 (hover overlay + 批量 bar) - currentId = id; - renderTasksList(); - } - - /* ---------- 新建空白任务 (重置表单, 不写库) ---------- */ - function newBlankTask() { - _draftSnap = null; // 清空之前的 draft 缓存 - state.selectedProd = null; - state.selectedModel = null; - state.count = 4; - state.ratio = '1:1'; - document.querySelectorAll('.pill-row[data-key="count"] .opt').forEach(b => b.classList.toggle('active', +b.dataset.val === 4)); - document.querySelectorAll('.pill-row[data-key="ratio"] .opt').forEach(b => b.classList.toggle('active', b.dataset.val === '1:1')); - document.querySelectorAll('.model-card').forEach(c => c.classList.remove('selected')); - renderSelectedProds(); - updateModelSummary(); - updateCost(); - resetPreviewToPlaceholder(); - showPreviewEmpty(); // 新任务态: 右侧空态 - currentId = null; - renderTasksList(); - } - - /* ---------- 「立即生成」 → 才创建任务 ---------- */ + // 立即生成 → push 新批次 + persist + 刷新视图 document.getElementById('mp-go-btn').addEventListener('click', () => { if (!state.selectedModel || !state.selectedProd) return; const snap = buildSnapshot(); - const name = autoName(snap); - const time = timeNow(); - if (currentId) { - const t = tasks.find(x => x.id === currentId); - if (t) { t.snap = snap; t.name = name; t.status = 'gen'; t.time = time; } - } else { - currentId = 'task-' + Date.now(); - tasks.push({ id: currentId, type: TASK_TYPE, name, snap, status: 'gen', time, createdAt: Date.now() }); - } + const _prod = PRODUCTS.find(p => p.id === state.selectedProd); + const _model = MODELS.find(m => m.id === state.selectedModel); + const _name = ((_prod && _prod.name) || '商品') + ' × ' + ((_model && _model.name) || '模特'); + const task = { + id: 'task-' + Date.now(), + type: TASK_TYPE, + name: _name, + snap, + status: 'ok', + time: timeNow(), + createdAt: Date.now(), + }; + tasks.push(task); save(tasks); - renderTasksList(); + // 不重渲整个 grid (保留 hover 重跑/采用的实时态),只更新 cur-prod 计数 + updateCurProdHeader(); }); - /* ---------- 「+ 新建任务」按钮 ---------- */ - document.getElementById('tasks-new-btn').addEventListener('click', newBlankTask); - /* ---------- 初始化 ---------- */ tasks = load(); - renderTasksList(); - - // URL ?taskId=xxx → 从任务中心跳过来时自动载入 - (function applyUrlTaskId() { - const q = new URLSearchParams(location.search); - const tid = q.get('taskId'); - if (tid && tasks.some(t => t.id === tid)) loadTaskIntoForm(tid); - })(); })(); + +// 初始化 +renderProdSpace(); +renderSelectedProds(); +renderModelMini(); // MODELS 已声明,可安全调用 +updateModelSummary(); +updateCost(); +showPreviewEmpty(); // 默认 → 右侧显示空态 +// 默认选中: URL ?product= 优先, 否则选 PRODUCTS 首位 (= 最近编辑) +const defaultProdId = state.selectedProd || (PRODUCTS[0] && PRODUCTS[0].id); +if (defaultProdId) selectProduct(defaultProdId); diff --git a/电商AI平台/pipeline.html b/电商AI平台/pipeline.html index 485752e..eb6c019 100644 --- a/电商AI平台/pipeline.html +++ b/电商AI平台/pipeline.html @@ -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,") 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 @@
镜头脚本 · 空 · 待生成 + +
@@ -604,12 +864,20 @@
- -
- - - +
+ + +
+ + + +
+
@@ -647,14 +915,35 @@
-
-
透真补水面膜 · 主图
-
-
透真补水面膜缺三视图
-
- - -
+
+
+ + + 缺三视图 + + + + MISSING TRI-VIEW + + 该商品还未生成 正 / 侧 / 背 三视图。直接生成图片或视频,模型缺少多角度参考,角色一致性、姿态稳定性可能下降。 + 建议:点右下 AI 生成三视图 先补齐三视图,再发起后续生成。 + + + 透真补水面膜 · 主图 +
+
+
透真补水面膜
+
美妆个护
+
2026-05-15 创建
+
+
+
@@ -1191,6 +1480,43 @@
+ +
+ +
+ + - `; } else { toolbar.innerHTML = ` @@ -1746,10 +2240,6 @@ const Stage2 = (function () {
- `; } 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 => ` -
-
${it.name}
-
${it.name}
-
${it.sub}
-
- 预设 - -
-
- `).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) => ` -
- 当前 - ${ver.label} -
- `).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 ` +
+ 已采用 + ${ver.label} +
+ `; + }).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 = `${prodName()} · 三视图(正/侧/背) · ${ver.label}`; - statusEl.textContent = `${ver.label} · 已应用,不满意可重跑`; + img.classList.add('is-zoomable'); + img.title = '点击放大查看'; + statusEl.textContent = isAdopted + ? `${ver.label} · 已采用,不满意可重跑` + : `${ver.label} · 预览中(未采用)`; foot.innerHTML = ` + ~¥0.30 / 次 `; 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 = '已三视图'; - } - // 同步到商品详情 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 = `
生成中
`; + img.classList.remove('is-zoomable'); + img.removeAttribute('title'); statusEl.textContent = '生成中 · 约 12s'; foot.innerHTML = '// POST /assets/tri-view'; 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 = ` + + `; + 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 = + `${prodName} · 三视图(正/侧/背) · ${ver.label}`; + 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'); +} diff --git a/电商AI平台/platform-cover.html b/电商AI平台/platform-cover.html index e17873a..1db1d92 100644 --- a/电商AI平台/platform-cover.html +++ b/电商AI平台/platform-cover.html @@ -8,112 +8,336 @@ @@ -269,13 +313,9 @@
// 成员 · 角色 · 额度 · 共享资产库
-
@@ -298,7 +338,7 @@ 充值 - +
@@ -312,18 +352,18 @@
[ 月限额 ]
-
¥3,000
+
¥3,000
// 自然月重置
[ 当月已用 ]
-
¥162.60
-
// 占月限 5.4%
+
¥162.60
+
// 占月限 5.4%
[ 当月剩余 ]
-
¥2,837.40
-
// 还可生成约 280 个项目
+
¥2,837.40
+
// 还可生成约 280 个项目
@@ -333,7 +373,7 @@

团队动态

// 最近 12 条 - 全部 → + 全部 →
@@ -442,25 +482,141 @@
- + + + + + + + + + +