feat(v2): 添加 V2.1 设计稿目录 · 团队/设置页 · pipeline 多项 mock 优化
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 6s

This commit is contained in:
UI 设计 2026-05-21 16:18:28 +08:00
parent 9bcf943e82
commit e293aa43be
25 changed files with 28377 additions and 0 deletions

952
v2/DESIGN_SPEC_V2.md Normal file
View File

@ -0,0 +1,952 @@
# 流·Studio 设计规范 · V2.1
> **版本:** V2.1(2026-05-15)
> **风格代号:** Restraint(克制)· Firecrawl-aligned
> **适用范围:** 流·Studio 全产品(工作台 / 项目 / 商品库 / 流水线 / 资产库 / 编辑器 / 账户)
> **V2.1 更新:** 色彩系统全面对齐 Firecrawl 实测——**冷灰底色**(放弃米白)· **主橙 #FA5D19**(从 #E55B26 调亮)· **20 档 black-alpha**(从 11 档扩展)· **5 色 accent 多彩点**(新增)· accent-black 替代 ink。保留 V2 所有结构性决策(8 px 圆角、inside-border、装订线、mono 装饰)。
> **与 V2 关系:** V2.1 兼容 V2 的所有 token 命名(`--ink` `--bg` `--card` `--green` `--red` 等保留为 legacy 别名),代码层无需重写。
---
## 0. 变更速查
### V2 → V2.1(本次 · 色彩系统对齐 Firecrawl)
| # | 维度 | V2 | V2.1 |
| - | ---- | -- | ---- |
| C1 | 底色 | `#FAF9F5` 暖米白 | **`#f9f9f9` 冷灰**(`--background-base`) |
| C2 | 主橙 | `#E55B26` 砖红 | **`#FA5D19`** 更红更饱和 |
| C3 | 文字基色 | `--ink: #15140F` | **`--accent-black: #262626`**(更柔和)· `--ink` 作 legacy 别名 |
| C4 | Alpha 阶梯 | 11 档 `--ink-alpha-*` | **20 档 `--black-alpha-*`**(1/2/3/4/5/6/7/8/10/12/16/20/24/32/40/48/56/64/72/88) |
| C5 | Alpha base | 全部用 `rgba(0,0,0,...)` | **32% 起换 `rgba(38,38,38,...)`**(避免叠出"灰中带蓝") |
| C6 | 状态色 | `--green #3F6B3F` 深森林绿 / `--red #B33A2A` 暗砖红 | **`--accent-forest #42c366`** / **`--accent-crimson #eb3424`**(信号灯感) |
| C7 | Accent 多彩 | 仅橙+绿+红 | **新增 5 色** amethyst/bluetron/crimson/forest/honey(仅用于语义信号) |
| C8 | 边框 | `#EFEBE0` 暖色 | **`#ededed`** 冷灰(无色相) |
| C9 | Selection | 未定义 | **`background: var(--heat-20)` + `color: var(--heat)`**(Firecrawl 签名) |
### V1 → V2(上次 · 结构性变更,保留)
| # | 维度 | V1 | V2 | 决策类型 |
| - | ---- | -- | -- | -------- |
| Δ1 | 圆角 | 大容器 0 / 按钮 9 / pill 999 | **大容器 8 / 按钮 8 / pill 999 / dot 999** | **变更** |
| Δ2 | mono `[ STATUS ]` | 大量使用 | **保留**(品牌签名) | 保留 |
| Δ3 | 字体 | Inter | **Inter Tight + 字重 500 替代 450**(免费) | 变更(轻) |
| Δ4 | 边框 | 真 `border:1px` | **`::before` inside-border**(hover 不抖动) | 变更 |
| Δ5 | 主容器左右垂直边 | 无 | **加 `border-x` + 四角准星**(图纸装订线) | 新增 |
| Δ6 | 主 CTA 阴影 | 全场无 | **4 层橙色发光** | 变更(小幅) |
---
## 1. 设计哲学
**一句话:** 一台精密设备的工作面板。
**三条铁律:**
1. **克制大于装饰** — 留白 > 容器 > 内容,大量空气感。
2. **单色锚点** — 全场只有一种 accent(橙色),且只用于 CTA / 关键状态 / 强调单词。
3. **结构清晰可见** — 用 **1 px 边框 + 8 px 圆角 + 四角准星 + 容器纵向装订线 + mono `[ ]` 标签**暴露"图纸感",而非阴影/渐变隐藏结构。
**避免的"AI 味":**
- 渐变铺面 / 玻璃拟态 / 彩色阴影
- 多色 emoji 图标
- 圆角无差别(全部 8px / 16px 的 SaaS 模板感)
- 卡片浮在背景上的"贴纸感"
- 装饰盖过内容(场记板 / 霓虹 / 丝绒幕布)
**新增禁令:**
- ❌ **0 px 硬切角的卡片** —— V2 起所有结构性容器都用 8 px 圆角
- ❌ **变深 hue 的 hover 色** —— 橙色 hover 用 alpha 阶梯,不用更深的橙
---
## 2. 色彩系统(V2.1 · 对齐 Firecrawl)
### 2.1 表面 / 背景(冷灰 · 无色相)
| Token | Hex | 用途 |
| ---------------------- | --------- | --------------------------- |
| `--background-base` | `#f9f9f9` | 页面底色 · 冷灰 |
| `--background-lighter` | `#fbfbfb` | 容器底色 / hover 浅底 |
| `--surface` | `#ffffff` | 卡片 / 容器表面 |
| `--surface-raised` | `#ffffff` | 浮层 / Modal 表面 |
> **Legacy 别名(V2 → V2.1):** `--bg = var(--background-base)` / `--bg-soft = var(--background-lighter)` / `--card = var(--surface)`。组件 CSS 不用改。
### 2.2 边框(冷灰 · 3 档差距极小)
| Token | Hex | 用途 |
| ---------------- | --------- | --------------------- |
| `--border-faint` | `#ededed` | **默认 1 px 边框 ★ 80% 场景** |
| `--border-muted` | `#e8e8e8` | 略深 |
| `--border-loud` | `#e6e6e6` | 最深(强分隔) |
> **设计意图:** 3 档之间只差 12 个色阶,**肉眼几乎看不出**。**用语义(faint/muted/loud)选择,不用视觉对比选择。** 这是 Firecrawl 的细节哲学。
### 2.3 Heat · 主橙(单 hue + 8 档 alpha)
| Token | Value | 用途 |
| ----------- | ----------------------- | ----------------------------- |
| `--heat` | `#fa5d19` | **主橙 100% · CTA / 链接 ★** |
| `--heat-90` | `rgba(250,93,25,.90)` | hover(替代 V1 `#D04E1F`) |
| `--heat-40` | `rgba(250,93,25,.40)` | focus ring / 边框次级 |
| `--heat-20` | `rgba(250,93,25,.20)` | pill 边框 / **selection 底色 ★** |
| `--heat-16` | `rgba(250,93,25,.16)` | hover 软底 |
| `--heat-12` | `rgba(250,93,25,.12)` | tint 底(active nav / icon-box) |
| `--heat-8` | `rgba(250,93,25,.08)` | |
| `--heat-4` | `rgba(250,93,25,.04)` | 极弱底 |
> **从 V2 `#E55B26` 调亮到 V2.1 `#FA5D19`** —— 更红更饱和,与 Firecrawl 100% 实测一致。**hover 永远不换 hue,只换 alpha。**
### 2.4 Accent · 5 色信号(新增)
> **作用:** 仅用于**语义信号**——代码高亮、图标色、状态色源。**禁止做大面积背景或装饰**。全场仍只有 1 个 accent(橙)。
| Token | Hex | 用途 |
| ------------------ | --------- | ----------------------------- |
| `--accent-black` | `#262626` | **主前景**(替代 V2 `--ink #15140F`,更柔和) |
| `--accent-white` | `#ffffff` | 反色文字(在橙底 / 黑底上) |
| `--accent-amethyst`| `#9061ff` | 紫 · 代码 property |
| `--accent-bluetron`| `#2a6dfb` | 蓝 · info |
| `--accent-crimson` | `#eb3424` | 红 · **error / 失败 ★** |
| `--accent-forest` | `#42c366` | 绿 · **success / 成功 ★** |
| `--accent-honey` | `#ecb730` | 黄 · warning |
> **Legacy 别名:** `--ink = var(--accent-black)` / `--green = var(--accent-forest)` / `--red = var(--accent-crimson)`
### 2.5 Black-Alpha 阶梯(20 档 · 核心工具尺)
> **替代 V2 的 11 档 ink-alpha**。这是日常用得最多的 token。024% 用 `rgba(0,0,0,...)`;**32% 起换 `rgba(38,38,38,...)`**(=`--accent-black` 作底),避免叠出"灰中带蓝"——Firecrawl 实测细节。
| Token | Light 值 | Dark 值 | 典型用途 |
| ------------------ | ---------------------- | ---------------------- | ----------------------- |
| `--black-alpha-1` | `rgba(0,0,0,.01)` | `rgba(255,255,255,.01)`| 极弱底 |
| `--black-alpha-2` | `rgba(0,0,0,.02)` | `rgba(255,255,255,.02)`| |
| `--black-alpha-3` | `rgba(0,0,0,.03)` | `rgba(255,255,255,.03)`| |
| `--black-alpha-4` | `rgba(0,0,0,.04)` | `rgba(255,255,255,.04)`| **hover bg ★** |
| `--black-alpha-5` | `rgba(0,0,0,.05)` | `rgba(255,255,255,.05)`| tab 间分隔条 |
| `--black-alpha-6` | `rgba(0,0,0,.06)` | `rgba(255,255,255,.06)`| |
| `--black-alpha-7` | `rgba(0,0,0,.07)` | `rgba(255,255,255,.07)`| **active bg ★** |
| `--black-alpha-8` | `rgba(0,0,0,.08)` | `rgba(255,255,255,.08)`| |
| `--black-alpha-10` | `rgba(0,0,0,.10)` | `rgba(255,255,255,.10)`| |
| `--black-alpha-12` | `rgba(0,0,0,.12)` | `rgba(255,255,255,.12)`| **inside-border ★** |
| `--black-alpha-16` | `rgba(0,0,0,.16)` | `rgba(255,255,255,.16)`| |
| `--black-alpha-20` | `rgba(0,0,0,.20)` | `rgba(255,255,255,.20)`| |
| `--black-alpha-24` | `rgba(0,0,0,.24)` | `rgba(255,255,255,.24)`| input hover 边框 |
| `--black-alpha-32` | `rgba(38,38,38,.32)` | `rgba(255,255,255,.32)`| ← base 切换 → ↓ |
| `--black-alpha-40` | `rgba(38,38,38,.40)` | `rgba(255,255,255,.40)`| |
| `--black-alpha-48` | `rgba(38,38,38,.48)` | `rgba(255,255,255,.48)`| **占位字色 ★** |
| `--black-alpha-56` | `rgba(38,38,38,.56)` | `rgba(255,255,255,.56)`| **次级文字 / 未选中 Tab ★** |
| `--black-alpha-64` | `rgba(38,38,38,.64)` | `rgba(255,255,255,.64)`| 描述文字 |
| `--black-alpha-72` | `rgba(38,38,38,.72)` | `rgba(255,255,255,.72)`| 强次级 |
| `--black-alpha-88` | `rgba(38,38,38,.88)` | `rgba(255,255,255,.88)`| 近主前景 |
> **V2 → V2.1 兼容映射(legacy 别名,组件 CSS 不必改):**
> - `--ink-alpha-4``--black-alpha-4`
> - `--ink-alpha-7``--black-alpha-7`
> - `--ink-alpha-12``--black-alpha-12`
> - `--ink-alpha-24``--black-alpha-24`
> - `--ink-alpha-32``--black-alpha-32`
> - `--ink-alpha-48``--black-alpha-48`
> - `--ink-alpha-56``--black-alpha-56`
> - `--ink-alpha-64``--black-alpha-64`
> - `--ink-alpha-72``--black-alpha-72`
> - `--ink-alpha-88``--black-alpha-88`
### 2.6 状态色配套底/边
| 含义 | 主色 | 配套底色(8% alpha) | 配套边框(20% alpha) |
| ---- | -------------------------- | --------------------------- | --------------------------- |
| 成功 | `--green` (`#42c366`) | `rgba(66,195,102,.08)` | `rgba(66,195,102,.20)` |
| 失败 | `--red` (`#eb3424`) | `rgba(235,52,36,.08)` | `rgba(235,52,36,.20)` |
| 信息 | `--heat` (`#fa5d19`) | `--heat-12` | `--heat-20` |
| 警告 | `--accent-honey`(`#ecb730`)| `rgba(236,183,48,.08)` | `rgba(236,183,48,.20)` |
### 2.7 Selection · 文字选中色(Firecrawl 签名)
```css
::selection {
background: var(--heat-20);
color: var(--heat);
}
```
选中任何文字时,底色 20% 橙、文字 100% 橙。这是 Firecrawl 易被忽略但**整站感知到位**的细节。
---
## 3. 字体系统(V2.1 · 中英混排策略)
### 3.1 字体族 · Inter + 阿里巴巴普惠体
| 用途 | 字体声明 |
| ------------ | ------------------------------------------------------------------------------------------------- |
| **正文 / UI**| `'Inter', 'Alibaba PuHuiTi', 'PingFang SC', 'Microsoft YaHei', system-ui, sans-serif` |
| **纯英文场景** | `'Inter', system-ui, sans-serif`(`--font-inter`)· 用于"Ctrl K"等强制纯英文徽标 |
| **Mono 装饰** | `'JetBrains Mono', 'Geist Mono', ui-monospace, monospace` |
**核心策略 · 浏览器字符级 fallthrough:**
```
Inter ────────→ 英文 / 数字 / 符号(命中)
↓ (CJK 不命中,继续找)
Alibaba PuHuiTi ──→ 中文(命中)
↓ (字体未加载到)
PingFang SC / Microsoft YaHei ──→ 系统中文兜底
```
浏览器对每个字符**逐个查找** font-family 链,Inter 不含 CJK 字形 → 中文字符自动跳到下一个候选。这是中英混排的标准做法,**不需要 JS、不会字重错位**。
**V2 → V2.1 → V2.1 变更轨迹:**
- V2: `Inter Tight` → 中文走系统 fallback → 字重错位
- V2.1 (前一版): 单一 `Alibaba PuHuiTi` → 英文用普惠体英文字形 → 英文略圆,缺乏 Inter 的"科技感"
- **V2.1 (本版)** : **Inter(英)+ Alibaba PuHuiTi(中)双字体协作** → 各自处理最擅长的语种
**为什么这个组合最优:**
- Inter:Vercel / Linear / Stripe 御用,**工程产品默认审美**。专门给屏幕 UI 优化,数字字形漂亮(同宽)。中文不擅长。
- Alibaba PuHuiTi:阿里出品,免费商用,**为中英混排专门设计的笔画粗细配比**(45/55/65/85 多档),中文笔画与 Inter 视觉重量贴近。专门给中文优化。
- 两者结合:**英文有 Inter 的锐利,中文有普惠体的清晰**,字重之间衔接自然。
**Inter 载入:**
```html
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
```
**Alibaba PuHuiTi 载入:**
```css
@font-face {
font-family: 'Alibaba PuHuiTi';
font-weight: 400; /* 55 Regular */
src: local('Alibaba PuHuiTi 3.0'),
local('AlibabaPuHuiTi-3-55-Regular'),
url('https://chinese-fonts-cdn.deno.dev/packages/alibaba_puhuiti/dist/AlibabaPuHuiTi-3-55-Regular/AlibabaPuHuiTi-3-55-Regular.woff2') format('woff2');
}
/* 同写法处理 500 Medium / 600 SemiBold / 700 Bold */
```
> **优先级:** 本地安装(设计师机器一般装了)→ CDN 加载 → 系统中文兜底。**永远不会出现假字或方框。**
**特殊场景 · 强制纯英文用 `--font-inter`:**
某些场景必须确保用 Inter(比如 "Ctrl K" 这种快捷键徽标想要 Inter Bold 的紧凑感),直接用专属变量:
```css
.search-wrap .k {
font-family: var(--font-inter); /* 跳过 fallback 链,锁定 Inter */
font-weight: 700;
font-size: 11.5px;
letter-spacing: 0.02em;
}
```
Mono 字体保留 `JetBrains Mono`——用作装饰元素 `[ 200 OK ]` `// 05.14` `/v2`,**不参与中文**。
### 3.2 字号 / 字重 / 行高(V2.1 整体放大半档,行高提升)
| 角色 | 字号 | 字重 | 字距 | 行高 | 用途 |
| ---------------- | ------- | ---- | ---------- | ---- | ---------------- |
| H1 / Hero | 36 px | 500 | -0.024em | 1.2 | 页面主标题(从 V2 的 32 上调) |
| 区块 H2 | 28 px | 500 | -0.02em | 1.25 | section-head 标题 |
| KPI 数值 | 32 px | 500 | -0.02em | 1.1 | 统计大数字 |
| 子区 H3 | 16 px | 500 | -0.01em | 1.4 | subsection 标题(从 15 上调) |
| 卡片标题 | 14 px | 500 | normal | 1.4 | 项目名 / 商品名(从 13.5 上调) |
| 正文 body | 14 px | 400 | normal | **1.65** | 默认正文(行高从 1.5 上调) |
| 区块描述 | 14 px | 400 | normal | 1.75 | section-head 描述 |
| 子区 lead | 13 px | 400 | normal | 1.8 | 子区下方说明 |
| Label(按钮/Tab)| 13 px | 500 | normal | 1.4 | 按钮文字 / Tab |
| Pill 文字 | 11.5 px | 500 | normal | 1.3 | 状态徽标 |
| Mono 标签 | 1111.5 px | 400/500 | 0.04em | 1.5 | `[ STATUS ]` |
| Mono 散点 | 8.5 px | 400 | 0.04em | 1 | 背景 ASCII 装饰 |
**字重档位仍仅 3 档:400 / 500 / 600**。普惠体 500(Medium 65)的中文笔画比 Inter 500 重一些,**整体不显单薄**,因此 600 几乎不需要。
**关键属性:**
- 数值类必须加 `font-variant-numeric: tabular-nums`
- 标题用 negative letter-spacing(-0.01 ~ -0.024em)
- 正文行高 **1.651.8**(V2 是 1.51.7),中文字间留呼吸
---
## 4. 圆角规则 · V2 统一 8 px
> **核心原则(V2 改写):统一 8 px / 状态徽标完全圆 / 极少数微元素降到 46 px**
| 元素类型 | 圆角值 | 例子 |
| ------------------------------ | ---------- | ------------------------------- |
| 所有结构性容器(大卡片 / 区块) | **8 px** | `.stats` `.list-card` `.shortcut` `.tip` `.modal` |
| 所有按钮 / 输入框 | **8 px** | `.btn` `.pill-btn` `.icon-btn` `.search` |
| nav 项 | **8 px** | `nav a`(V1 是 7,V2 统一) |
| 缩略图 / 画面占位 | **8 px** | `.thumb` `.ic` |
| 头像 / 小色块 | **6 px** | `.av` `.team .p`(可选 8) |
| Mono 标签 / badge / kbd | **4 px** | `.kbd` `.badge` |
| 进度条段位 | **2 px** | `.prog span` |
| Pill 状态徽标 / dot | **999 px** | `.pill` `.dot`(完全圆) |
**为什么改:** Firecrawl 实测全站统一 8 px,工程感来自"准星 + 装订线 + mono 装饰",**不是硬切角**。0 圆角容器在小尺寸下会显得"卡顿、廉价",8 px 是同时兼顾"图纸感"和"成品感"的最佳值。
---
## 5. 边框 / 阴影 / 描边
### 5.1 边框策略 · V2 改为 inside-border
> **V2 改进:** 默认边框用 `::before` 伪元素绘制,而非真 `border`。原因:hover 时让 `::before` 透明度 → 0 不会触发布局抖动。
**通用工具类:**
```css
.inside-border {
position: relative;
}
.inside-border::before {
content: '';
position: absolute;
inset: 0;
border: 1px solid var(--border-faint);
border-radius: inherit;
pointer-events: none;
transition: opacity .2s ease, border-color .2s ease;
}
.inside-border:hover::before { opacity: 0; }
```
**层级:**
- 默认:`var(--border-faint)`
- 略深:`var(--border-muted)`(主分隔线)
- 最深:`var(--border-loud)`(强分隔,少用)
- **禁止 2px / 3px 实线**
- **虚线**仅用于 `.tip` 提示框:`1px dashed var(--border-faint)`
### 5.2 阴影 · V2 引入主 CTA 专属橙色发光
**默认无阴影**(V1 规则保留)。
**新增例外 · 主 CTA 4 层橙色阴影**(替代 V1 的"全场无"):
```css
.btn-primary {
box-shadow:
inset 0 -4px 8px rgba(250, 93, 25, 0.20), /* 内阴影:底部暗一点 = 立体 */
0 1px 1px rgba(250, 93, 25, 0.12),
0 2px 4px rgba(250, 93, 25, 0.10),
0 0.5px 0.5px rgba(250, 93, 25, 0.16);
}
.btn-primary:hover {
box-shadow:
inset 0 -4px 8px rgba(250, 93, 25, 0.20),
0 1px 1px rgba(250, 93, 25, 0.16),
0 4px 8px rgba(250, 93, 25, 0.20), /* 更高更亮 */
0 0.5px 0.5px rgba(250, 93, 25, 0.16);
}
```
**Toast 阴影**(V1 保留):`0 4px 20px rgba(21,20,15,0.06)`,白色调,不属于橙色发光体系。
### 5.3 容器四角"+"准星(签名元素 · V2 升级为 SVG)
V1 用字符 `+`(font-family JetBrains Mono);V2 升级为 SVG 路径,带圆弧内凹,质感更工程化:
```html
<svg width="22" height="21" viewBox="0 0 22 21" fill="none" class="corner">
<path d="M10.5 4C10.5 7.31371 7.81371 10 4.5 10H0.5V11H4.5C7.81371 11 10.5 13.6863 10.5 17V21H11.5V17C11.5 13.6863 14.1863 11 17.5 11H21.5V10H17.5C14.1863 10 11.5 7.31371 11.5 4V0H10.5V4Z" fill="var(--border-muted)"/>
</svg>
```
**位置:** 容器四角,中心精确落在边框交点上(用 `-translate-x-1/2 -translate-y-1/2`)。
**字符版本兼容:** 简单卡片(如 modal 内嵌)仍可用 `content: '+'` 字符版,但全屏主容器必须用 SVG。
---
## 6. 主内容容器 · 新增装订线规则(V2 新增章节)
> **核心签名:** 主工作区始终被两条 1 px 垂直边线包夹,配合四角准星,形成图纸装订线效果。
```html
<div class="workbench-container">
<svg class="corner top-left">...</svg>
<svg class="corner top-right">...</svg>
<svg class="corner bottom-left">...</svg>
<svg class="corner bottom-right">...</svg>
<!-- content -->
</div>
```
```css
.workbench-container {
max-width: 1480px;
margin: 0 auto;
border-left: 1px solid var(--border-faint);
border-right: 1px solid var(--border-faint);
position: relative;
/* 上下边线不要,只左右 */
}
.workbench-container .corner {
position: absolute;
width: 22px; height: 21px;
}
.workbench-container .corner.top-left { top: -10.5px; left: -11px; }
.workbench-container .corner.top-right { top: -10.5px; right: -11px; }
.workbench-container .corner.bottom-left { bottom: -10.5px; left: -11px; }
.workbench-container .corner.bottom-right { bottom: -10.5px; right: -11px; }
```
适用范围:工作台 / 项目列表页 / 商品库 / 流水线主区。**编辑器全屏画布、Modal 内不必加。**
---
## 7. 间距 / 栅格(V2.1 全局放大,Firecrawl-level 呼吸)
**基础栅格:** 4 px
**常用间距阶梯:** 4 / 8 / 12 / 16 / 20 / 24 / 28 / 32 / 40 / 48 / 64 / 80 / 104
> **V2 → V2.1 变更:** 全局垂直间距系统性放大 30%。原因:Alibaba PuHuiTi 中文笔画比 Inter 厚一档,**字体密度提升 → 必须用更大的留白补偿**,否则视觉拥挤。这就是为什么 Firecrawl 的间距看起来"奢侈"——它的字体也是出版级偏紧凑的 Suisse Intl,留白补偿到位。
**主区块布局:**
- 侧边栏宽度:`248 px`(无变)
- **主内容 padding:`72 px 80 px 120 px`**(从 V2 的 `48 56 56` 上调)
- 内容最大宽度:`1280 px`(从 1480 收窄,留更多自然边距)
- 区块间垂直:**`104 px`**(`section margin-bottom` · 从 80 上调)
- 子区间垂直:**`64 px`**(`subsection margin-bottom` · 从 48 上调)
- 子区标题底距:**`22 px`**(`h3 → 内容` · 从 16 上调)
- 卡片网格间距:`24 px`(主区) / `14 px`(子区)
- 卡片内 padding:
- 大卡片 / stats: **`28 px 30 px`**(从 22 24 上调)
- 列表行: **`20 px 24 px`**(从 14 18 上调)
- 快捷入口: **`18 px 20 px`**(从 14 上调)
- Modal 头/体: **`24 px 28 px`**(从 20 24 上调)
- Hero / section-head: **`52 px 56 px`** / `padding-bottom: 28 px`
**最重要的一条:** 别再吝啬空气。**当不确定 padding 是 16 还是 24 时,选 24。** 当不确定 margin 是 48 还是 64 时,选 64。
**间距/字号配对原则:**
- 标题旁边的描述/副标:`mb 1014 px`(紧凑组合)
- 标题下方的正文/列表:`mb 2228 px`(分组组合)
- section 顶部 mono-tag → h2:`mb 14 px`
- h2 → section description:`mb 12 px`
- section-head 整体下方边线:`pb 28 px / mb 44 px`
---
## 8. 背景:制图纸网格(V1 保留)
### 8.1 三层叠加
```
图层 1(最上):主交叉点 "+" 准星 SVG — 240×240 重复
图层 2(中间):子交叉点小圆点 — 60×60 重复
图层 3(最下):虚线网格 — 240×240 重复(stroke-dasharray: 1.5 4)
```
**配色:**
- "+" 准星:`#B8B3A4`(stroke 1 px)
- 小点:`#CFCABB`(r=0.9)
- 虚线:`#E2DED2`(stroke 1 px)
### 8.2 视觉聚焦遮罩
```css
mask-image: radial-gradient(ellipse 95% 80% at 50% 35%, #000 25%, transparent 95%);
```
### 8.3 装饰散点(Mono ASCII)
主区域 4 个固定位置撒 ASCII 散点。字号 8.5 px / 颜色 `--ink-alpha-12` / 透明度 0.8 / `pointer-events: none`
### 8.4 边角 Mono 标签(品牌签名 · 保留)
主区域 4 个角各放一个 Mono 标签:
```
左上 [ 200 OK ] 右上 [ /v2 ]
左下 [ .MP4 · 9:16 ] 右下 [ STUDIO ]
```
字号 10.5 px / 颜色 `--ink-alpha-48` / 字距 0.06em。
> **作用:** 让页面看起来像「在某个开发环境 / 调试视图里」,而不是普通官网。Firecrawl **没有**这个元素,这是流·Studio 的独特品牌资产。
---
## 9. Icon 系统(V2 强化章节,直接对应用户优化诉求 ①)
### 9.1 通用规则
- **格式:** 一律 SVG inline,**禁止** `<img>` 引图标
- **库选择:** 推荐 Lucide(line icon,1.52 px stroke)或 Phosphor Regular
- **stroke width:** 统一 **1.5 px**(替代 V1 提到的 1.8)
- **stroke linecap / linejoin:** `round`
- **填充:** **不填充**(纯 line icon)
- **颜色:** 通过 `stroke="currentColor"`,继承父元素 `color`
- **emoji 禁用:** 任何场景都不允许彩色 emoji
### 9.2 尺寸阶梯
| 场景 | 尺寸 | 用途 |
| ------------------- | ------- | ----------------------------- |
| Icon-S | 14 px | 内嵌 inline 文字旁 |
| Icon-M(默认) | 16 px | 按钮内 / Tab / list 行 |
| Icon-L | 20 px | 顶栏 / 快捷入口 / dropdown 触发器 |
| Icon-XL | 24 px | Modal 头部 / Toast / 空状态 |
| Icon-Hero | 36 px | 空状态插画 / 大占位 |
### 9.3 颜色规则
| 场景 | 颜色 |
| ------------ | --------------------- |
| 默认 | `--ink-alpha-56` |
| Hover | `--ink` |
| Active / 选中| `--heat` |
| Disabled | `--ink-alpha-12` |
| 在主 CTA 内 | `#FFFFFF` |
### 9.4 Icon-Box(快捷入口左侧的方块图标容器)
```css
.icon-box {
width: 32px;
height: 32px;
border-radius: 8px; /* V1 是 0,V2 改 8 */
background: var(--heat-12);
display: grid;
place-items: center;
}
.icon-box svg { width: 16px; stroke: var(--heat); }
```
---
## 10. 组件规范 · 含完整状态(V2 大幅扩充,对应用户诉求 ②③④)
### 10.1 按钮(3 种类型 × 5 种状态 × 3 种尺寸)
**类型:**
| 类型 | 背景 | 文字 | 边框/描边 |
| ----------------- | ---------------- | ------------------ | ---------------------------------- |
| `.btn` 默认 | `--card` 白 | `--ink` | inside-border `--ink-alpha-12` |
| `.btn-primary` 主 | `--heat` | `#FFFFFF` | 无,靠橙色阴影分层 |
| `.btn-ghost` 无框 | transparent | `--ink-alpha-56` | 无 |
**尺寸:**
| 尺寸 | 高度 | padding | 字号 | icon 尺寸 |
| ------ | ----- | ------------ | ------- | --------- |
| `-sm` | 28 px | `0 10 px` | 12 px | 14 px |
| 默认 | 32 px | `0 14 px` | 13 px | 16 px |
| `-lg` | 40 px | `0 18 px` | 14 px | 16 px |
**5 种状态(每种类型都要实现):**
| 状态 | `.btn` 默认 | `.btn-primary` 主 |
| -------- | ------------------------------------------ | ---------------------------------------------- |
| Default | 白底 / inside-border `--ink-alpha-12` | 橙底 + 4 层橙阴影 |
| Hover | 底色 `--ink-alpha-4` + 边框 opacity → 0 | 阴影第 3 层加亮(`0 4px 8px rgba(229,91,38,0.20)`) |
| Active(按下) | 底色 `--ink-alpha-7` + `scale(0.99)` | `scale(0.995)` + 阴影 inset 加深 |
| Focused | 外层 2 px `--heat-40` ring,offset 2 px | 同左 |
| Disabled | 底 `--bg-soft` + 文字 `--ink-alpha-12` + `cursor: not-allowed` + 无 hover | 底 `--heat-40` + 文字 `#FFFFFF` + 阴影消失 + `cursor: not-allowed` |
**过渡:** 全场统一 `transition: background-color 200ms, opacity 200ms, transform 100ms, box-shadow 100ms ease`
### 10.2 Pill(状态徽标)· 严格分级(对应用户诉求 ②)
> **V2 核心改进:Pill 分 3 级,同级别尺寸完全一致**
| 级别 | 高度 | padding | 字号 | 圆角 | dot 尺寸 | 用途 |
| ------------ | ----- | ------------ | ------- | ----- | -------- | --------------------- |
| **L1 大胶囊** | 28 px | `0 12 px` | 13 px | 999 px| 8 px | 项目状态 / 列表行 |
| **L2 中胶囊** | 22 px | `0 10 px` | 11.5 px | 999 px| 6 px | **默认 / 通用** |
| **L3 小胶囊** | 18 px | `0 8 px` | 10.5 px | 999 px| 5 px | KPI 卡角标 / Mono 标签 |
**色调(3 种,通用所有级别):**
| 状态 | 文字色 | 底色 | 边框(20% alpha)|
| ---- | --------------- | ------------- | ---------------- |
| info | `--heat` | `--heat-12` | `--heat-20` |
| ok | `--green` | `--green-bg`(`#3F6B3F14`) | `--green-bd`(`#3F6B3F33`) |
| err | `--red` | `--red-bg`(`#B33A2A14`) | `--red-bd`(`#B33A2A33`) |
每个 pill 前置圆点(`.dot`,直径同上表,颜色继承文字色)。
**HTML 写法:**
```html
<span class="pill pill-l2 pill-info">
<span class="dot"></span>
生成中
</span>
```
### 10.3 输入框 / 搜索(V2.1 · 含 Firecrawl 式快捷键提示)
**尺寸:** 高 **36 px**(从 V2 的 32 上调,中文留白更舒展)/ padding `0 14 px` / 字号 14 px / 圆角 8 px
**状态:**
| 状态 | 边框 | 底色 |
| -------- | --------------------------------- | --------------------- |
| Default | inside-border `--black-alpha-12` | `--card` |
| Hover | inside-border `--black-alpha-24` | `--card` |
| Focused | inside-border `--heat-40` + inset 1 px | `--card` |
| Error | inside-border `--red` | `--red-bg` |
| Disabled | inside-border `--black-alpha-12` | **`--black-alpha-5`**(从 `--bg-soft` 改 · 冷灰底太接近白,看不出禁用) |
占位字色 `--black-alpha-48`,disabled 占位 `--black-alpha-24`
**带图标 / 快捷键搜索框(参考 Firecrawl 实测):**
```
[🔍] [ 搜索任意内容... ] Ctrl K
│ │
│ 16px line icon, color: --black-alpha-56 │ mono · 11.5px · --black-alpha-48
│ 左 14 px,**z-index: 2**(关键) │ 右 14 px,**z-index: 2**
│ │ 平铺文本(无边框盒),不用 kbd 样式
```
**关键坑(已修):**
1. **搜索 icon 看不见** —— `<input>` 的白色 bg 会盖住同级的 SVG icon。**SVG 必须加 `z-index: 2`** 才能抬到 input 之上。同理 Ctrl K 提示也要 z-index。
2. **快捷键不用 `⌘`** —— JetBrains Mono webfont 不带 U+2318 `⌘` 字形,会显示成方框。**Windows 用户体系用 "Ctrl K" 纯文本**;Mac 端要显示 ⌘ 时用 SVG command 图标(见 §9.4)。
3. **快捷键提示不要用 kbd 边框盒** —— Firecrawl 是平铺灰色 mono 文本,无任何 border / bg / radius。**显得克制,符合"克制大于装饰"。**
### 10.4 KPI 统计行(`.stats`)
- 1 行 4 格,共用一个 inside-border 容器,圆角 8 px,**无 gap**
- 列与列之间用 `border-right: 1px solid var(--border-faint)` 分隔(最后一列去掉)
- 容器四角加 SVG "+" 准星
- 每格结构:label + L3 pill → 大数字(30 px) → delta / progress
### 10.5 列表行(`.list-row`)
```
[缩略图 54×70] [标题 + meta] [进度条 5 段] [L2 pill] [按钮 -sm]
```
- grid 5 列:`54px 1fr auto auto auto`
- 行高 padding `14 px 18 px`
- 行间 1 px 分隔线(`--border-faint`),最后行去掉
- **整行 hover**`--ink-alpha-4` 底色
- **整行 active**(键盘选中)→ `--ink-alpha-7` 底色
- **整行 disabled** → opacity 0.5 + `cursor: not-allowed`
### 10.6 进度条段位(流水线 5 阶段专用)· **V2.1 语义色重写**
5 个 `18×5 px` 小段,3 px 间距,**每段颜色映射真实业务状态**(替代 V2 的"全灰已完成"——那样像项目失败):
| 状态 | 颜色 | 视觉处理 |
| -------- | --------------------- | ------------------------------ |
| 未开始 | `--black-alpha-8` | 极弱灰底 · 静态 |
| **已完成** | `--accent-forest`(`#42c366`)| **绿色 · 静态**(替代 V2 灰色) |
| **进行中** | `--heat`(`#fa5d19`) | **橙色 + 1.4s 脉动**(opacity 1↔0.55 + scaleY 1↔0.7)· 区别于失败 |
| 失败 | `--accent-crimson`(`#eb3424`)| 红色 · 静态 |
圆角:每段 2 px。
**关键设计:**
- **绿/橙/红/灰** 四色一眼可辨:已完成的绿色让"5/5 全绿"=完美交付,**不会再有"全灰像失败了"的误读**
- **橙色脉动动画** 是区分"进行中"与"失败"的关键——红色永远静态,只有橙色会呼吸,潜意识上"动 = 在运行,静 = 出错"
```css
.prog span.now {
background: var(--heat);
animation: prog-pulse 1.4s ease-in-out infinite;
}
@keyframes prog-pulse {
0%,100% { opacity: 1; transform: scaleY(1); }
50% { opacity: .55; transform: scaleY(.7); }
}
```
### 10.7 快捷入口卡(`.shortcut`)
- 白底 / inside-border / **8 px 圆角**(V1 是 0) / 14 px padding
- 左侧 32×32 橙色 tint 图标块(`--heat-12` 底 / 8 px 圆角 / 居中 16 px line icon)
- 右侧:标题(13 px 500)+ Mono 描述(11.5 px,`--ink-alpha-48`)
- **Default → Hover → Active 完整状态:**
- Hover:底色 `--ink-alpha-4` / 标题 underline-from-orange 1 px(可选)
- Active(点击瞬间):scale(0.99)
- Focused(键盘):2 px `--heat-40` ring
### 10.8 提示框(`.tip`)
- 白底 / **1 px 虚线**边框(`dashed`)/ **8 px 圆角**
- 加粗标题独立一行 + 正文
- 内联代码用 `.mono` 类:橙色文字 + `--heat-12` 底 + 4 px 圆角
### 10.9 Toast
- 右下角 24 px 偏移 / 白底 / inside-border / **8 px 圆角**
- 唯一允许的"白阴影":`0 4px 20px rgba(21,20,15,0.06)`
- 进入动画:`translateX(420px → 0)`,缓动 `cubic-bezier(0.34, 1.56, 0.64, 1)`,300 ms
- 自动消失:2400 ms
- 内容结构:左侧 24×24 橙色 tint 图标(`--heat-12` 底)+ 右侧标题 + Mono 副文本(`[ 200 OK ]`)
### 10.10 弹窗(Modal)
- 居中,460480 px 宽,白底,**8 px 圆角**(V1 是 0)
- 四角加 SVG "+" 准星
- 遮罩 `rgba(21,20,15,0.42)`,带 `backdrop-filter: blur(8px)`
- 进入动画:`scale(0.96 → 1)`,250 ms 弹性缓动
- 三段结构:
- **Header:** 36 px 橙色 tint 图标 + 标题 + Mono 副标
- **Body:** 13 px / `--ink-alpha-56` / 行高 1.7
- **Footer:** 右对齐两个按钮(取消 + 主 CTA)
- ESC 关闭 / 点击遮罩关闭
### 10.11 Tab(双层)
**主 Tab:**
- 高 36 px / padding `0 14 px` / 字号 13 px / 字重 500
- 未选中:`--ink-alpha-56` 文字
- 选中:`--ink` 文字 + 底部 2 px `--heat` 横线(整个 tab 宽度)
- Hover(未选中态):`--ink-alpha-7` 底色
**副 Tab(过滤型):**
- 高 28 px / padding `0 10 px` / 字号 12 px
- 未选中:`--ink-alpha-56` + 灰度 icon
- 选中:`--ink` + 多色 icon(grayscale: 0)
- Tab 之间用 1 px 高 12 px 的 `--ink-alpha-7` 竖条分隔(可选)
### 10.12 Dropdown / Select
- 触发器:同输入框样式(高 32 / 圆角 8 / inside-border)
- 右侧 16 px chevron-down icon,色 `--ink-alpha-48`,展开时旋转 180°
- 弹层:白底 / 8 px 圆角 / `0 4px 20px rgba(21,20,15,0.06)` 阴影 / **唯一允许阴影场景之二**
- 选项:高 32 / padding `0 12 / hover `--ink-alpha-4` / 选中 `--heat-12` + 右侧 14 px checkmark `--heat`
- 分组分隔:1 px `--border-faint`
### 10.13 Checkbox / Radio / Switch(V2.1 · 全部用 SVG icon)
**通用原则:** 所有 indicator 都用真 SVG(via background-image data URI 或 inline `<svg>`),**禁止用 border-width / transform: rotate(45deg) 凑对勾**——那种 CSS hack 在缩放/字体渲染下会变形。
**Checkbox:**
- **容器:** 16×16 / 4 px 圆角 / inside-border `--black-alpha-24`(hover 时 → `--black-alpha-48`)
- **Checked:** `--heat` 底 · 居中 12×12 白色 SVG checkmark(`<path d="M20 6 9 17l-5-5"/>` · stroke 3 · linecap round)
- **Indeterminate:** `--heat` 底 · 居中 12×12 白色 SVG 横线(`<path d="M5 12h14"/>` · stroke 3)
- **Disabled:**`--black-alpha-5` + 边 `--black-alpha-12` + icon 不渲染或半透明
- **实现方式:** `background-image: url("data:image/svg+xml,...")` · `background-size: 12px 12px` · `background-position: center` · 这样不需要 ::after 凑出 icon,SVG 是真实的
**Radio:**
- 16×16 圆 / inside-border `--black-alpha-24`
- Checked:内嵌 8×8 `--heat` 实心圆(纯几何形状,不算 icon,用 ::after 即可)
- Disabled:底 `--black-alpha-5`
**Switch:**
- 容器 28×16 / 999 px 圆角
- Off:底 `--black-alpha-12` / 圆球 `#FFFFFF` 12×12 + 1 px subtle shadow
- On:底 `--heat` / 圆球右移到 `left: 14px`
- 过渡:`background 200 ms`,圆球位移 `left 200 ms`
> **为什么不用字符 `✓` `✗` 这种 Unicode:** 不同系统、不同字体对这些字符的字形支持差异巨大(`⌘` 在 JetBrains Mono Web 字体里就是缺字形 → 显示成方框)。**SVG 是唯一可靠的解。**
---
## 11. 微观细节(品牌签名 · V1 保留)
### 11.1 标签全部包方括号
`[ 200 OK ]` `[ .MP4 · 9:16 ]` `[ /v2 ]` `[ /sidebar collapse ]`
### 11.2 时间戳像代码注释
`// 05.14 · 周五`
### 11.3 数值后缀
`¥327` 主体大字 + `.40` 小字次级(`<small>`)
### 11.4 强调单词上色
正文里 `<b style="color: var(--ink)">3 个项目</b>` —— 让具体数值/名词比周围文字更深一档(不是变橙)。**橙色只留给 CTA。**
### 11.5 ASCII 字符做装饰
`↑ 本月 +3` `↓ -1.2%`
### 11.6 链接式"更多"按钮
`[ ALL · 12 ] →` —— Mono 标签 + 箭头,色 `--ink-alpha-48`,hover 转橙。
### 11.7 缩略图不放图,放比例
`9:16` Mono 字符占位。
---
## 12. Don't List(绝对禁止)
- ❌ **0 px 卡片**(V1 → V2 反转)
- ❌ 渐变背景(只有 hero 区可考虑,但首选纯色)
- ❌ 玻璃拟态(`backdrop-filter` 只用于 modal 遮罩)
- ❌ 彩色 emoji 图标(用 SVG line icon,1.5 px stroke)
- ❌ 多个 accent 色(全场只有橙色)
- ❌ 大圆角容器(>12 px 直接判错)
- ❌ 灰色阴影 / 文字阴影(只允许橙色主 CTA 阴影 + Toast/Dropdown 白阴影)
- ❌ 鲜艳的状态色(避免荧光绿、电光蓝、霓虹粉)
- ❌ 居中对齐大段正文(全部左对齐)
- ❌ 把装饰当主角(场记板、丝绒、霓虹灯)
- ❌ 无意义的微动效(hover 旋转、缩放、彩虹流光)
- ❌ **hover 时换深 hue 的橙**(用 alpha)
- ❌ **真 `border` + hover 边框消失**(用 inside-border ::before)
- ❌ **同一行混用直角和圆角**(用户原话:"不要有些是直角,胶囊又是圆角")
---
## 13. Sass / CSS Token 速查表(V2.1)
```css
:root {
/* ============================================================
Color system V2.1 · Firecrawl-aligned
============================================================ */
/* === 表面 / 背景(冷灰)=== */
--background-base: #f9f9f9;
--background-lighter: #fbfbfb;
--surface: #ffffff;
--surface-raised: #ffffff;
/* === 边框(冷灰 · 3 档)=== */
--border-faint: #ededed; /* 默认 1 px */
--border-muted: #e8e8e8;
--border-loud: #e6e6e6;
/* === Accent 多彩(5 色信号)=== */
--accent-black: #262626;
--accent-white: #ffffff;
--accent-amethyst: #9061ff;
--accent-bluetron: #2a6dfb;
--accent-crimson: #eb3424;
--accent-forest: #42c366;
--accent-honey: #ecb730;
/* === Heat · 单 hue + 8 档 alpha === */
--heat: #fa5d19;
--heat-90: rgba(250, 93, 25, .90);
--heat-40: rgba(250, 93, 25, .40);
--heat-20: rgba(250, 93, 25, .20);
--heat-16: rgba(250, 93, 25, .16);
--heat-12: rgba(250, 93, 25, .12);
--heat-8: rgba(250, 93, 25, .08);
--heat-4: rgba(250, 93, 25, .04);
/* === Black-Alpha 阶梯(20 档 · 024 用 #000,32+ 用 #262626)=== */
--black-alpha-1: rgba(0, 0, 0, .01);
--black-alpha-2: rgba(0, 0, 0, .02);
--black-alpha-3: rgba(0, 0, 0, .03);
--black-alpha-4: rgba(0, 0, 0, .04);
--black-alpha-5: rgba(0, 0, 0, .05);
--black-alpha-6: rgba(0, 0, 0, .06);
--black-alpha-7: rgba(0, 0, 0, .07);
--black-alpha-8: rgba(0, 0, 0, .08);
--black-alpha-10: rgba(0, 0, 0, .10);
--black-alpha-12: rgba(0, 0, 0, .12);
--black-alpha-16: rgba(0, 0, 0, .16);
--black-alpha-20: rgba(0, 0, 0, .20);
--black-alpha-24: rgba(0, 0, 0, .24);
--black-alpha-32: rgba(38, 38, 38, .32);
--black-alpha-40: rgba(38, 38, 38, .40);
--black-alpha-48: rgba(38, 38, 38, .48);
--black-alpha-56: rgba(38, 38, 38, .56);
--black-alpha-64: rgba(38, 38, 38, .64);
--black-alpha-72: rgba(38, 38, 38, .72);
--black-alpha-88: rgba(38, 38, 38, .88);
/* === Legacy aliases(V2 命名 → V2.1 token,组件 CSS 无需重写)=== */
--bg: var(--background-base);
--bg-soft: var(--background-lighter);
--card: var(--surface);
--ink: var(--accent-black);
--green: var(--accent-forest);
--red: var(--accent-crimson);
--green-bg: rgba(66, 195, 102, .08);
--green-bd: rgba(66, 195, 102, .20);
--red-bg: rgba(235, 52, 36, .08);
--red-bd: rgba(235, 52, 36, .20);
--ink-alpha-4: var(--black-alpha-4);
--ink-alpha-7: var(--black-alpha-7);
--ink-alpha-12: var(--black-alpha-12);
--ink-alpha-24: var(--black-alpha-24);
--ink-alpha-32: var(--black-alpha-32);
--ink-alpha-48: var(--black-alpha-48);
--ink-alpha-56: var(--black-alpha-56);
--ink-alpha-64: var(--black-alpha-64);
--ink-alpha-72: var(--black-alpha-72);
--ink-alpha-88: var(--black-alpha-88);
/* === 圆角 === */
--r-sm: 4px;
--r-md: 8px; /* 默认主圆角 */
--r-pill: 999px;
/* === 字体 === */
--font-sans: 'Inter Tight', 'PingFang SC', 'Microsoft YaHei', sans-serif;
--font-mono: 'JetBrains Mono', 'Geist Mono', monospace;
/* === 容器宽度 === */
--container-max: 1480px;
--sidebar-w: 248px;
/* === 过渡 === */
--t-fast: 100ms ease;
--t-base: 200ms ease;
--t-slow: 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
/* === Selection · Firecrawl 签名细节 === */
::selection {
background: var(--heat-20);
color: var(--heat);
}
/* === Dark mode · 翻转底色 + black-alpha 改用 white-alpha === */
.dark {
--background-base: #0a0a0a;
--background-lighter: #141414;
--surface: #171717;
--surface-raised: #1f1f1f;
--border-faint: #2a2a2a;
--border-muted: #333333;
--border-loud: #404040;
--accent-black: #f5f5f5;
--black-alpha-1: rgba(255,255,255,.01);
--black-alpha-2: rgba(255,255,255,.02);
--black-alpha-3: rgba(255,255,255,.03);
--black-alpha-4: rgba(255,255,255,.04);
--black-alpha-5: rgba(255,255,255,.05);
--black-alpha-6: rgba(255,255,255,.06);
--black-alpha-7: rgba(255,255,255,.07);
--black-alpha-8: rgba(255,255,255,.08);
--black-alpha-10: rgba(255,255,255,.10);
--black-alpha-12: rgba(255,255,255,.12);
--black-alpha-16: rgba(255,255,255,.16);
--black-alpha-20: rgba(255,255,255,.20);
--black-alpha-24: rgba(255,255,255,.24);
--black-alpha-32: rgba(255,255,255,.32);
--black-alpha-40: rgba(255,255,255,.40);
--black-alpha-48: rgba(255,255,255,.48);
--black-alpha-56: rgba(255,255,255,.56);
--black-alpha-64: rgba(255,255,255,.64);
--black-alpha-72: rgba(255,255,255,.72);
--black-alpha-88: rgba(255,255,255,.88);
}
```
---
## 14. V1 → V2 迁移检查清单(给后续改代码用)
- [ ] 全局替换 `border-radius: 0``border-radius: 8px`(卡片 / stats / shortcut / modal / toast / thumb)
- [ ] 替换 V1 ink-2/3/4 token 为 ink-alpha-56/48/12
- [ ] 替换 `--orange-tint` `--orange-soft``--heat-12` `--heat-20`
- [ ] 主 CTA hover 移除 `#D04E1F`,改用 4 层橙阴影变化
- [ ] 所有 `.btn` `.input``inside-border`
- [ ] 主工作区容器加 `border-left + border-right` + 4 个 SVG 准星
- [ ] 字体 Inter → Inter Tight
- [ ] 所有 icon 转 SVG line icon,stroke 1.5
- [ ] Pill 按 L1/L2/L3 三级规范化高度/字号/圆点尺寸
- [ ] 每个交互组件补齐 hover / active / focused / disabled 状态
---
## 15. 参考与来源
- **视觉灵感(实测):** [Firecrawl Playground](https://www.firecrawl.dev/playground?endpoint=parse) · 详见 [firecrawl_playground_spec.md](_design_src/firecrawl_playground_spec.md)
- **结构灵感:** Linear / Stripe Dashboard
- **图纸感来源:** 印刷套版准星 + 老 Unix 终端
- **V1 文档:** [DESIGN_SPEC.md](DESIGN_SPEC.md)(保留作为历史)

170
v2/account.html Normal file
View File

@ -0,0 +1,170 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>账户 · 流·Studio</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css">
<style>
.acc-grid { display: grid; grid-template-columns: 1.7fr 1fr; gap: 24px; align-items: start; }
.balance-banner {
background: var(--accent-black);
color: var(--accent-white);
padding: 28px 32px;
margin-bottom: 24px;
position: relative;
border: 1px solid var(--accent-black);
border-radius: var(--r-md);
}
.balance-banner::before, .balance-banner::after { content: ''; position: absolute; width: 14px; height: 14px; background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 21' fill='%23e8e8e8'%3E%3Cpath d='M10.5 4C10.5 7.31371 7.81371 10 4.5 10H0.5V11H4.5C7.81371 11 10.5 13.6863 10.5 17V21H11.5V17C11.5 13.6863 14.1863 11 17.5 11H21.5V10H17.5C14.1863 10 11.5 7.31371 11.5 4V0H10.5V4Z'/%3E%3C/svg%3E") no-repeat center; background-size: contain; pointer-events: none; }
.balance-banner::before { top: -7px; left: -7px; }
.balance-banner::after { bottom: -7px; right: -7px; }
.balance-banner .corner-tr, .balance-banner .corner-bl { position: absolute; color: var(--black-alpha-48); font-family: var(--font-mono); font-size: 13px; }
.balance-banner .corner-tr { top: -8px; right: -8px; }
.balance-banner .corner-bl { bottom: -8px; left: -8px; }
.balance-banner .lbl { font-family: var(--font-mono); font-size: 12px; color: rgba(255,255,255,.55); letter-spacing: .04em; }
.balance-banner .v { font-size: 42px; font-weight: 700; letter-spacing: -.018em; margin-top: 8px; font-variant-numeric: tabular-nums; }
.balance-banner .meta { font-size: 12.5px; color: rgba(255,255,255,.5); margin-top: 8px; font-family: var(--font-mono); letter-spacing: .02em; }
.balance-banner .actions { display: flex; gap: 8px; margin-top: 18px; }
.balance-banner .btn { background: var(--accent-white); color: var(--accent-black); border-color: var(--accent-white); }
.balance-banner .btn:hover { background: var(--background-base); }
.balance-banner .btn-ghost { background: transparent; color: var(--accent-white); border: 1px solid rgba(255,255,255,.25); }
.balance-banner .btn-ghost:hover { background: rgba(255,255,255,.1); color: var(--accent-white); }
.recharge-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin-top: 12px; }
.recharge-card { border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 18px; text-align: center; cursor: pointer; background: var(--surface); position: relative; }
.recharge-card:hover { background: var(--background-lighter); }
.recharge-card.selected { border-color: var(--heat); background: var(--heat-12); }
.recharge-card.selected::before, .recharge-card.selected::after { content: ''; position: absolute; width: 14px; height: 14px; background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 21' fill='%23FA5D19'%3E%3Cpath d='M10.5 4C10.5 7.31371 7.81371 10 4.5 10H0.5V11H4.5C7.81371 11 10.5 13.6863 10.5 17V21H11.5V17C11.5 13.6863 14.1863 11 17.5 11H21.5V10H17.5C14.1863 10 11.5 7.31371 11.5 4V0H10.5V4Z'/%3E%3C/svg%3E") no-repeat center; background-size: contain; pointer-events: none; }
.recharge-card.selected::before { top: -7px; left: -7px; }
.recharge-card.selected::after { bottom: -7px; right: -7px; }
.recharge-card .amt { font-size: 22px; font-weight: 700; font-variant-numeric: tabular-nums; }
.recharge-card .gift { font-size: 11px; color: var(--black-alpha-48); margin-top: 4px; font-family: var(--font-mono); }
.recharge-card .gift.bonus { color: var(--accent-forest); font-weight: 600; }
.recharge-card .ribbon { position: absolute; top: -8px; right: 8px; font-family: var(--font-mono); font-size: 9.5px; padding: 1px 6px; background: var(--heat); color: var(--accent-white); letter-spacing: .04em; font-weight: 600; border-radius: var(--r-sm); }
.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: 14px; }
.pane h3 + .desc { font-size: 12px; color: var(--black-alpha-48); margin-top: -10px; margin-bottom: 14px; font-family: var(--font-mono); letter-spacing: .02em; }
.bills .neg { color: var(--accent-black); font-variant-numeric: tabular-nums; font-weight: 500; }
.bills .pos { color: var(--accent-forest); font-variant-numeric: tabular-nums; font-weight: 500; }
.bills .ref { color: var(--black-alpha-48); font-size: 11px; font-family: var(--font-mono); }
.usage-line { display: flex; justify-content: space-between; padding: 6px 0; font-size: 13px; }
.usage-line .v { font-variant-numeric: tabular-nums; color: var(--accent-black); font-weight: 600; }
.usage-bar { height: 4px; background: var(--background-lighter); border-radius: 2px; margin: 6px 0 12px; overflow: hidden; }
.usage-bar > span { display: block; height: 100%; }
.rule-list { font-size: 12.5px; color: var(--black-alpha-56); line-height: 1.7; }
.rule-list strong { color: var(--accent-black); font-weight: 600; }
.rule-list .mono { font-family: var(--font-mono); color: var(--heat); background: var(--heat-12); padding: 1px 5px; font-size: 11.5px; }
</style>
</head>
<body>
<div id="page">
<div class="page-head">
<div>
<h1>账户</h1>
<div class="sub"><span class="mono">// 余额 · 充值 · 消费明细</span></div>
</div>
</div>
<div class="acc-grid">
<div>
<div class="balance-banner">
<span class="corner-tr" aria-hidden></span><span class="corner-bl" aria-hidden></span>
<div class="lbl">[ CURRENT BALANCE ]</div>
<div class="v">¥327.40</div>
<div class="meta">// 本月已消费 ¥162.60 · 可使用约 32 个项目</div>
<div class="actions">
<button class="btn btn-lg" onclick="Shell.toast('充值', '/billing/topup')">充值</button>
<button class="btn btn-ghost btn-lg" onclick="Shell.toast('提取明细', '/billing/export')">提取消费明细</button>
</div>
</div>
<div class="pane">
<h3>快速充值</h3>
<div class="desc">// 充值后立刻到账,可开发票</div>
<div class="recharge-row">
<div class="recharge-card" onclick="Shell.toast('选择 ¥100')"><div class="amt">¥100</div><div class="gift">无赠送</div></div>
<div class="recharge-card selected"><span class="ribbon">推荐</span><div class="amt">¥500</div><div class="gift bonus">+ ¥30 赠送</div></div>
<div class="recharge-card" onclick="Shell.toast('选择 ¥1000')"><div class="amt">¥1000</div><div class="gift bonus">+ ¥80 赠送</div></div>
<div class="recharge-card" onclick="Shell.toast('选择 ¥3000')"><div class="amt">¥3000</div><div class="gift bonus">+ ¥300 赠送</div></div>
</div>
<div style="display:flex; gap:10px; margin-top:14px;">
<input class="input" placeholder="自定义金额(最低 ¥50)" style="flex:1;">
<button class="btn btn-primary" onclick="Shell.toast('微信支付', '¥500 · WechatPay')">微信支付 ¥500</button>
<button class="btn" onclick="Shell.toast('支付宝', '¥500 · Alipay')">支付宝</button>
</div>
</div>
<div style="display:flex; align-items:baseline; margin-bottom:12px;">
<h2 style="font-size:15px; font-weight:600;">消费明细</h2>
<span class="spacer"></span>
<div class="hstack">
<button class="chip" style="height:28px; font-size:12px;">近 30 天</button>
<button class="chip" style="height:28px; font-size:12px;">导出</button>
</div>
</div>
<table class="t bills">
<thead>
<tr><th>时间</th><th>项目 / 类型</th><th>详情</th><th style="text-align:right;">金额</th></tr>
</thead>
<tbody>
<tr><td class="muted-2 mono" style="font-size:11.5px;">05.09 14:08</td><td>补水面膜 · v3</td><td class="muted">故事板 image-2 · 1 次</td><td style="text-align:right;" class="neg">-¥0.45</td></tr>
<tr><td class="muted-2 mono" style="font-size:11.5px;">05.09 14:02</td><td>补水面膜 · v3</td><td class="muted">脚本 LLM · 2.4k tokens</td><td style="text-align:right;" class="neg">-¥0.04</td></tr>
<tr><td class="muted-2 mono" style="font-size:11.5px;">05.09 13:38</td><td>补水面膜 · v3</td><td class="muted">基础资产 · 5 张图</td><td style="text-align:right;" class="neg">-¥1.05</td></tr>
<tr><td class="muted-2 mono" style="font-size:11.5px;">05.08 18:21</td><td>透真防晒 · 通勤对比</td><td class="muted">视频片段 · 6 镜</td><td style="text-align:right;" class="neg">-¥1.20</td></tr>
<tr><td class="muted-2 mono" style="font-size:11.5px;">05.08 11:02</td><td>充值</td><td class="muted">微信支付 · <span class="ref">TX2024050811021Z</span></td><td style="text-align:right;" class="pos">+¥500.00</td></tr>
<tr><td class="muted-2 mono" style="font-size:11.5px;">05.07 20:14</td><td>蓝牙耳机 · 开箱</td><td class="muted">视频片段 · 5 镜(1 镜重跑不扣)</td><td style="text-align:right;" class="neg">-¥0.94</td></tr>
<tr><td class="muted-2 mono" style="font-size:11.5px;">05.07 15:48</td><td>咖啡冻干 · 早八</td><td class="muted">故事板生成失败 · <span style="color:var(--black-alpha-48);">不扣费</span></td><td style="text-align:right;" class="muted-2">¥0.00</td></tr>
<tr><td class="muted-2 mono" style="font-size:11.5px;">05.06 10:30</td><td>瑜伽裤 · 通勤穿搭</td><td class="muted">项目导出 · 1 次</td><td style="text-align:right;" class="neg">-¥3.20</td></tr>
</tbody>
</table>
</div>
<div>
<div class="pane">
<h3>本月消费分布</h3>
<div class="usage-line"><span>视频片段(Seedance)</span><span class="v">¥98.40</span></div>
<div class="usage-bar"><span style="width:60%; background:var(--heat);"></span></div>
<div class="usage-line"><span>故事板(image-2)</span><span class="v">¥36.00</span></div>
<div class="usage-bar"><span style="width:22%; background:var(--accent-forest);"></span></div>
<div class="usage-line"><span>基础资产</span><span class="v">¥21.00</span></div>
<div class="usage-bar"><span style="width:13%; background:var(--black-alpha-56);"></span></div>
<div class="usage-line"><span>脚本 LLM</span><span class="v">¥7.20</span></div>
<div class="usage-bar"><span style="width:5%; background:var(--black-alpha-48);"></span></div>
<div class="divider"></div>
<div class="usage-line" style="font-weight:600;"><span>合计</span><span class="v">¥162.60</span></div>
</div>
<div class="pane">
<h3>扣费规则</h3>
<div class="rule-list">
<strong>① 失败不扣</strong>:模型超时、内容审核拦截、生成异常一律不扣费。<br>
<strong>② 用户重跑不扣首次</strong>:第一次重跑保留原扣费,第二次起按次结算。<br>
<strong>③ 仅在你点击 <span class="mono">[ 确认通过 ]</span> 时入账</strong><br>
<strong>④ 导出不再扣费</strong>,所有 token 已在过程中结算。
</div>
</div>
<div class="pane">
<h3>开发票</h3>
<div style="font-size:12.5px; color:var(--black-alpha-56); margin-bottom:10px;">本月可开发票额度:<strong style="color:var(--accent-black);">¥162.60</strong></div>
<button class="btn" style="width:100%;" onclick="Shell.toast('申请发票', '/billing/invoice')">申请发票</button>
</div>
</div>
</div>
</div>
<script src="assets/shell.js"></script>
<script>Shell.render({ active: 'account', crumbs: [{ label: '工作台', href: 'index.html' }, { label: '账户' }] });</script>
</body>
</html>

900
v2/asset-factory.html Normal file
View File

@ -0,0 +1,900 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>图片生成 · 流·Studio</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css">
<style>
#page-content { padding: 24px 28px 60px; }
/* ─── 三 Hero 卡片网格(模特上身图 / 平台套图 / 图片优化 · 等比)─── */
.factory-hero {
display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16px; margin-bottom: 56px;
}
@media (max-width: 1400px) { .factory-hero { grid-template-columns: 1fr 1fr; } }
@media (max-width: 1000px) { .factory-hero { grid-template-columns: 1fr; } }
.factory-card {
position: relative;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
padding: 28px 30px;
overflow: hidden;
}
/* 卡片内 · 文上图下 单列(3 卡并排时保持视觉一致)*/
.factory-body {
display: flex; flex-direction: column;
gap: 18px;
height: 100%;
}
.factory-text { display: flex; flex-direction: column; min-width: 0; }
.factory-tag {
align-self: flex-start;
font-family: var(--font-mono); font-size: 10.5px;
color: var(--black-alpha-48); letter-spacing: .06em;
padding: 2px 8px;
border: 1px solid var(--border-faint);
background: var(--background-lighter);
border-radius: var(--r-sm);
margin-bottom: 14px;
}
.factory-title {
font-size: 22px; font-weight: 600;
letter-spacing: -.018em; line-height: 1.25;
color: var(--accent-black);
}
.factory-desc {
margin-top: 8px;
font-size: 13.5px; color: var(--black-alpha-64); line-height: 1.55;
}
/* feature 列表 */
.factory-features {
list-style: none; padding: 0;
margin: 22px 0 0;
display: flex; flex-direction: column; gap: 11px;
}
.factory-features li {
display: flex; align-items: center; gap: 10px;
font-size: 13px; color: var(--black-alpha-72); font-weight: 500;
}
.factory-features .ff-ic {
width: 26px; height: 26px;
display: inline-flex; align-items: center; justify-content: center;
background: var(--heat-12);
color: var(--heat);
border-radius: var(--r-md);
flex-shrink: 0;
}
.factory-features .ff-ic svg { width: 14px; height: 14px; }
/* 平台 chip 行 */
.platform-row {
display: flex; flex-wrap: wrap; gap: 8px;
margin-top: 20px;
}
.platform-chip {
display: inline-flex; align-items: center; gap: 7px;
height: 30px; padding: 0 12px 0 8px;
border: 1px solid var(--border-faint);
background: var(--surface);
border-radius: var(--r-pill);
transition: background var(--t-base);
cursor: pointer;
}
.platform-chip:hover { background: var(--black-alpha-4); }
.platform-chip .code {
display: inline-flex; align-items: center; justify-content: center;
width: 20px; height: 20px;
background: var(--accent-black); color: var(--accent-white);
font-family: var(--font-mono); font-size: 8.5px; font-weight: 600;
border-radius: var(--r-pill); letter-spacing: .04em;
}
.platform-chip .nm {
font-size: 12px; color: var(--accent-black); font-weight: 500;
}
/* CTA 行:主按钮 + 价格 mono */
.factory-cta {
margin-top: auto; padding-top: 24px;
display: flex; align-items: center; gap: 14px;
}
.factory-cta .cost {
font-family: var(--font-mono); font-size: 10.5px;
color: var(--black-alpha-48); letter-spacing: .04em;
}
/* 视觉占位 + feature 列表已隐藏 — CTA 卡片只保留标题 + 描述 + 按钮 */
.factory-visual { display: none; }
.factory-features { display: none; }
.model-visual { grid-template-columns: repeat(4, 1fr); }
.model-visual .main { aspect-ratio: 3 / 4; grid-column: span 1; }
.model-visual .stack { display: contents; }
.model-visual .stack .placeholder { aspect-ratio: 3 / 4; }
.kit-visual { grid-template-columns: repeat(4, 1fr); }
.kit-visual .placeholder { aspect-ratio: 1 / 1; }
.tri-visual { grid-template-columns: repeat(3, 1fr); }
.tri-visual .placeholder { aspect-ratio: 1 / 1; }
/* ─── 任务中心 · section header ─── */
.section-h { display: flex; align-items: center; gap: 12px; margin-top: 24px; margin-bottom: 14px; }
.section-h h2 { font-size: 18px; font-weight: 600; letter-spacing: -.01em; }
.section-h .sub-mono { font-family: var(--font-mono); font-size: 11.5px; color: var(--black-alpha-48); letter-spacing: .02em; }
/* ─── 视图切换 (复用 projects.html · 图标 + 文字) ─── */
.view-toggle { display: inline-flex; border: 1px solid var(--border-faint); border-radius: var(--r-md); overflow: hidden; }
.view-toggle button { padding: 0 14px; background: var(--surface); color: var(--black-alpha-56); font-size: 13px; border: 0; border-right: 1px solid var(--border-faint); border-radius: 0; height: 36px; cursor: pointer; font-family: inherit; display: flex; align-items: center; gap: 6px; transition: background var(--t-base), color var(--t-base); }
.view-toggle button:last-child { border-right: 0; }
.view-toggle button:hover { background: var(--background-lighter); color: var(--accent-black); }
.view-toggle button.active { background: var(--heat-12); color: var(--heat); font-weight: 600; }
.view-toggle button svg { width: 13px; height: 13px; }
/* ─── 网格视图 · 卡片(原 history-grid) ─── */
.history-grid {
display: grid; grid-template-columns: repeat(4, 1fr);
gap: 16px;
}
@media (max-width: 1280px) { .history-grid { grid-template-columns: repeat(3, 1fr); } }
@media (max-width: 960px) { .history-grid { grid-template-columns: repeat(2, 1fr); } }
.history-grid[hidden] { display: none; }
.history-card {
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
padding: 12px;
display: grid; grid-template-columns: 78px 1fr; gap: 14px;
align-items: center;
transition: background var(--t-base);
cursor: pointer;
position: relative;
}
.history-card:hover { background: var(--black-alpha-4); }
.history-card:hover .card-del-btn { opacity: 1; }
.history-card .placeholder { width: 78px; height: 78px; }
/* ─── 列表视图 · 表格 ─── */
#task-list-view[hidden] { display: none; }
.task-name-cell { display: flex; align-items: center; gap: 12px; }
.task-thumb { width: 40px; height: 40px; flex-shrink: 0; border-radius: var(--r-sm); }
.task-name { font-weight: 600; color: var(--accent-black); font-size: 13.5px; }
.task-sub { font-size: 11.5px; color: var(--black-alpha-48); margin-top: 3px; font-family: var(--font-mono); letter-spacing: .02em; }
table.t .task-list-prog { display: flex; align-items: center; gap: 8px; min-width: 120px; }
table.t .task-list-prog .bar { flex: 1; height: 4px; background: var(--black-alpha-7); border-radius: 2px; overflow: hidden; }
table.t .task-list-prog .bar span { display: block; height: 100%; background: var(--heat); border-radius: 2px; animation: hp-pulse 1.4s ease-in-out infinite; }
table.t .task-list-prog .pct { font-family: var(--font-mono); font-size: 10.5px; color: var(--heat); letter-spacing: .02em; white-space: nowrap; }
/* ─── 行末 ⋯ 删除气泡 (复用 projects.html) ─── */
.row-action { display: flex; gap: 4px; justify-content: flex-end; }
table.t tbody tr .row-more { opacity: 0; transition: opacity .15s; }
table.t tbody tr:hover .row-more { opacity: 1; }
.row-more {
position: relative; display: inline-flex;
cursor: pointer; align-items: center;
color: var(--black-alpha-56);
padding: 4px;
}
.row-more:hover { color: var(--accent-black); }
.row-more-tip {
position: absolute; top: calc(100% + 6px); right: 0;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
box-shadow: 0 4px 16px rgba(0,0,0,.08);
padding: 4px; min-width: 110px;
opacity: 0; pointer-events: none;
transform: translateY(-2px);
transition: opacity .15s, transform .15s;
z-index: 12;
}
.row-more-tip::before {
content: ''; position: absolute;
top: -8px; left: 0; right: 0; height: 8px;
}
.row-more:hover .row-more-tip,
.row-more-tip:hover { opacity: 1; pointer-events: auto; transform: translateY(0); }
.row-more-tip .mi {
display: flex; align-items: center; gap: 6px;
width: 100%; padding: 6px 10px;
background: transparent; border: 0;
border-radius: var(--r-sm); cursor: pointer;
font-size: 12.5px; color: var(--accent-black);
font-family: inherit; text-align: left;
transition: background var(--t-base), color var(--t-base);
}
.row-more-tip .mi:hover {
background: var(--crimson-bg, #fdebea);
color: var(--accent-crimson, #c43d3d);
}
.row-more-tip .mi svg { width: 13px; height: 13px; }
/* ─── 删除确认 modal · 复用 ─── */
.modal-bg.show { display: flex; }
.mono-acc { font-family: var(--font-mono); color: var(--heat); font-weight: 600; }
.history-body { min-width: 0; display: flex; flex-direction: column; gap: 4px; }
.history-name {
font-size: 13.5px; font-weight: 600; color: var(--accent-black);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.history-type {
font-size: 11.5px; color: var(--black-alpha-48);
font-family: var(--font-mono); letter-spacing: .02em;
}
.history-foot {
display: flex; align-items: center; justify-content: space-between;
margin-top: 4px; gap: 6px; min-width: 0;
}
.history-foot .mono {
font-family: var(--font-mono); font-size: 10.5px;
color: var(--black-alpha-48); letter-spacing: .02em;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.history-foot .pill { padding: 2px 8px; font-size: 10.5px; }
.history-foot .pill .dot { width: 5px; height: 5px; }
/* 进度条(生成中状态) */
.history-prog {
margin-top: 6px;
display: flex; align-items: center; gap: 8px;
}
.history-prog .bar {
flex: 1; height: 4px;
background: var(--black-alpha-7);
border-radius: 2px; overflow: hidden;
}
.history-prog .bar span {
display: block; height: 100%;
background: var(--heat); border-radius: 2px;
animation: hp-pulse 1.4s ease-in-out infinite;
}
@keyframes hp-pulse {
0%, 100% { opacity: 1; transform: scaleY(1); }
50% { opacity: .55; transform: scaleY(.7); }
}
.history-prog .pct {
font-family: var(--font-mono); font-size: 10px;
color: var(--heat); letter-spacing: .02em; white-space: nowrap;
}
</style>
</head>
<body>
<div id="page">
<div class="page-head">
<div>
<h1>图片生成</h1>
<div class="sub">
<span class="mono">// 一键生成</span>
<span>·</span>
<span>电商视觉素材,提升内容制作效率</span>
</div>
</div>
</div>
<!-- 双 Hero 卡片 -->
<div class="factory-hero">
<!-- 卡片 A · 模特上身图 -->
<div class="factory-card with-corners">
<span class="corner-tr" aria-hidden></span>
<span class="corner-bl" aria-hidden></span>
<div class="factory-body">
<div class="factory-text">
<span class="factory-tag">[ MODEL · TRY-ON ]</span>
<div class="factory-title">模特上身图</div>
<div class="factory-desc">选择模特,AI 生成商品模特上身效果图</div>
<ul class="factory-features">
<li>
<span class="ff-ic">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="9" r="3"/><circle cx="17" cy="9" r="2.5"/><path d="M3 19c0-3 2.7-5 6-5s6 2 6 5M14 19c.5-2.4 2.4-4 5-4 .8 0 1.5.2 2 .5"/></svg>
</span>
支持多模特选择
</li>
<li>
<span class="ff-ic">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="8" height="8"/><rect x="13" y="3" width="8" height="8"/><rect x="3" y="13" width="8" height="8"/><rect x="13" y="13" width="8" height="8"/></svg>
</span>
一次生成 4 张
</li>
<li>
<span class="ff-ic">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="m3 7 9-4 9 4-9 4-9-4z"/><path d="m3 12 9 4 9-4M3 17l9 4 9-4"/></svg>
</span>
支持多商品并行
</li>
</ul>
<div class="factory-cta">
<a class="btn btn-primary btn-lg" href="model-photo.html">
开始生成
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
</a>
<span class="cost">[ ≈ ¥0.30 / 张 ]</span>
</div>
</div>
<div class="factory-visual model-visual">
<div class="placeholder main"><span class="ph-frame">Ava · 9:16</span></div>
<div class="stack">
<div class="placeholder"><span class="ph-frame">变体 01</span></div>
<div class="placeholder"><span class="ph-frame">变体 02</span></div>
<div class="placeholder"><span class="ph-frame">变体 03</span></div>
</div>
</div>
</div>
</div>
<!-- 卡片 B · 平台套图 -->
<div class="factory-card with-corners">
<span class="corner-tr" aria-hidden></span>
<span class="corner-bl" aria-hidden></span>
<div class="factory-body">
<div class="factory-text">
<span class="factory-tag">[ PLATFORM · KIT ]</span>
<div class="factory-title">平台套图</div>
<div class="factory-desc">选择平台模板,AI 生成电商平台套图</div>
<ul class="factory-features">
<li>
<span class="ff-ic">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21V9M9 21V5M15 21v-8M21 21V11"/></svg>
</span>
覆盖主流电商平台
</li>
<li>
<span class="ff-ic">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="8" height="8"/><rect x="13" y="3" width="8" height="8"/><rect x="3" y="13" width="8" height="8"/><rect x="13" y="13" width="8" height="8"/></svg>
</span>
一键生成 4 张套图
</li>
<li>
<span class="ff-ic">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l1.7 4.6L18 9l-4.3 1.4L12 15l-1.7-4.6L6 9l4.3-1.4L12 3z"/></svg>
</span>
智能排版设计
</li>
</ul>
<div class="platform-row">
<div class="platform-chip"><span class="code">DY</span><span class="nm">抖音</span></div>
<div class="platform-chip"><span class="code">TB</span><span class="nm">淘宝</span></div>
<div class="platform-chip"><span class="code">XHS</span><span class="nm">小红书</span></div>
<div class="platform-chip"><span class="code">PDD</span><span class="nm">拼多多</span></div>
<div class="platform-chip"><span class="code">AMZ</span><span class="nm">亚马逊</span></div>
</div>
<div class="factory-cta">
<a class="btn btn-primary btn-lg" href="platform-cover.html">
开始生成
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
</a>
<span class="cost">[ ≈ ¥0.50 / 张 ]</span>
</div>
</div>
<div class="factory-visual kit-visual">
<div class="placeholder"><span class="ph-frame">套图 / TB</span></div>
<div class="placeholder"><span class="ph-frame">套图 / DY</span></div>
<div class="placeholder"><span class="ph-frame">套图 / XHS</span></div>
<div class="placeholder"><span class="ph-frame">套图 / PDD</span></div>
</div>
</div>
</div>
<!-- 卡片 C · 图片优化(生成人物 / 商品三视图)-->
<div class="factory-card with-corners">
<span class="corner-tr" aria-hidden></span>
<span class="corner-bl" aria-hidden></span>
<div class="factory-body">
<div class="factory-text">
<span class="factory-tag">[ TRI-VIEW · OPTIMIZE ]</span>
<div class="factory-title">图片优化</div>
<div class="factory-desc">为人物 / 商品生成三视图,保证多镜头一致性</div>
<ul class="factory-features">
<li>
<span class="ff-ic">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="9" r="3"/><path d="M3 19c0-3 2.7-5 6-5s6 2 6 5"/></svg>
</span>
人物 · 商品 全支持
</li>
<li>
<span class="ff-ic">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="6" height="18"/><rect x="9" y="3" width="6" height="18"/><rect x="15" y="3" width="6" height="18"/></svg>
</span>
正面 / 侧面 / 背面 一次输出
</li>
<li>
<span class="ff-ic">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/></svg>
</span>
多镜头一致性保证
</li>
</ul>
<div class="factory-cta">
<a class="btn btn-primary btn-lg" href="model-photo.html?mode=tri">
开始生成
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
</a>
<span class="cost">[ ≈ ¥0.40 / 组 ]</span>
</div>
</div>
<div class="factory-visual tri-visual">
<div class="placeholder"><span class="ph-frame">正面</span></div>
<div class="placeholder"><span class="ph-frame">侧面</span></div>
<div class="placeholder"><span class="ph-frame">背面</span></div>
</div>
</div>
</div>
</div>
<!-- ============= 任务中心 · 参考 projects.html 布局 ============= -->
<div class="section-h">
<h2>任务中心</h2>
<span class="sub-mono">// <span id="tc-sub-total">0</span> 个 · <span id="tc-sub-gen">0</span> 生成中 · <span id="tc-sub-ok">0</span> 已完成 · <span id="tc-sub-err">0</span> 失败</span>
</div>
<!-- 状态 tabs (复用 .tabs) -->
<div class="tabs" id="tc-tabs">
<div class="tab active" data-filter="all">全部 <span class="count">0</span></div>
<div class="tab" data-filter="gen">生成中 <span class="count">0</span></div>
<div class="tab" data-filter="ok">已完成 <span class="count">0</span></div>
<div class="tab" data-filter="err">失败 <span class="count">0</span></div>
</div>
<!-- toolbar: search + 类型 chip + clear + view-toggle -->
<div class="toolbar">
<div class="search-inline">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
<input class="input" id="tc-search" placeholder="搜索任务名">
</div>
<div class="chip-wrap" data-key="type">
<button class="chip" type="button"><span class="chip-label">任务类型</span> <svg class="caret" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg></button>
<div class="chip-menu"></div>
</div>
<button class="clear-filters" id="tc-clear" type="button" hidden>
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 4l8 8M12 4l-8 8"/></svg>
清空筛选
</button>
<span class="spacer"></span>
<div class="view-toggle" id="tc-view-toggle">
<button type="button" data-view="grid">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2" y="2" width="5" height="5"/><rect x="9" y="2" width="5" height="5"/><rect x="2" y="9" width="5" height="5"/><rect x="9" y="9" width="5" height="5"/></svg>
网格
</button>
<button type="button" class="active" data-view="list">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 4h12M2 8h12M2 12h12"/></svg>
列表
</button>
</div>
</div>
<div class="result-meta" id="tc-result-meta">// 显示 <span class="count">0</span> / 0 个任务</div>
<!-- ============= LIST VIEW (默认) ============= -->
<div id="task-list-view">
<table class="t">
<thead>
<tr>
<th style="width:32%">任务</th>
<th>类型</th>
<th style="width:140px">进度</th>
<th>状态</th>
<th style="width:120px">创建于</th>
<th style="width:48px"></th>
</tr>
</thead>
<tbody id="task-list-tbody"><!-- JS 从卡片同步生成 --></tbody>
</table>
</div>
<!-- ============= GRID VIEW ============= -->
<div class="history-grid" id="task-grid" hidden>
<div class="task-card history-card" data-status="ok" data-type="model" data-name="补水面膜 × Ava">
<button class="card-del-btn" type="button" title="删除任务" onclick="event.stopPropagation();" data-action="delete-task"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
<div class="placeholder"><span class="ph-frame">Ava · 1:1</span></div>
<div class="history-body">
<div class="history-name">补水面膜 × Ava</div>
<div class="history-type">模特上身图 · 4 张</div>
<div class="history-foot">
<span class="mono">// 05.19 · 14:30</span>
<span class="pill ok"><span class="dot"></span>已完成</span>
</div>
</div>
</div>
<div class="task-card history-card" data-status="ok" data-type="platform" data-name="精华液 × 平台套图">
<button class="card-del-btn" type="button" title="删除任务" onclick="event.stopPropagation();"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
<div class="placeholder"><span class="ph-frame">TB / XHS · 1:1</span></div>
<div class="history-body">
<div class="history-name">精华液 × 平台套图</div>
<div class="history-type">平台套图 · 4 张</div>
<div class="history-foot">
<span class="mono">// 05.19 · 14:25</span>
<span class="pill ok"><span class="dot"></span>已完成</span>
</div>
</div>
</div>
<div class="task-card history-card" data-status="gen" data-type="model" data-name="防晒霜 × Luna">
<button class="card-del-btn" type="button" title="删除任务" onclick="event.stopPropagation();"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
<div class="placeholder"><span class="ph-frame">Luna · 1:1</span></div>
<div class="history-body">
<div class="history-name">防晒霜 × Luna</div>
<div class="history-type">模特上身图 · 4 张</div>
<div class="history-foot">
<span class="mono">// 05.19 · 14:20</span>
<span class="pill info"><span class="dot"></span>生成中</span>
</div>
<div class="history-prog">
<div class="bar"><span style="width:65%"></span></div>
<span class="pct">65%</span>
</div>
</div>
</div>
<div class="task-card history-card" data-status="err" data-type="platform" data-name="面霜 × 平台套图">
<button class="card-del-btn" type="button" title="删除任务" onclick="event.stopPropagation();"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
<div class="placeholder"><span class="ph-frame">DY / PDD · 1:1</span></div>
<div class="history-body">
<div class="history-name">面霜 × 平台套图</div>
<div class="history-type">平台套图 · 4 张</div>
<div class="history-foot">
<span class="mono">// 05.19 · 14:15</span>
<span class="pill err"><span class="dot"></span>失败</span>
</div>
</div>
</div>
<div class="task-card history-card" data-status="ok" data-type="model" data-name="口红 × Mia">
<button class="card-del-btn" type="button" title="删除任务" onclick="event.stopPropagation();"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
<div class="placeholder"><span class="ph-frame">Mia · 1:1</span></div>
<div class="history-body">
<div class="history-name">口红 × Mia</div>
<div class="history-type">模特上身图 · 4 张</div>
<div class="history-foot">
<span class="mono">// 05.18 · 21:08</span>
<span class="pill ok"><span class="dot"></span>已完成</span>
</div>
</div>
</div>
<div class="task-card history-card" data-status="ok" data-type="platform" data-name="眼霜 × 平台套图">
<button class="card-del-btn" type="button" title="删除任务" onclick="event.stopPropagation();"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
<div class="placeholder"><span class="ph-frame">TB / DY · 1:1</span></div>
<div class="history-body">
<div class="history-name">眼霜 × 平台套图</div>
<div class="history-type">平台套图 · 8 张</div>
<div class="history-foot">
<span class="mono">// 05.18 · 18:42</span>
<span class="pill ok"><span class="dot"></span>已完成</span>
</div>
</div>
</div>
<div class="task-card history-card" data-status="ok" data-type="model" data-name="瑜伽裤 × Zoe">
<button class="card-del-btn" type="button" title="删除任务" onclick="event.stopPropagation();"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
<div class="placeholder"><span class="ph-frame">Zoe · 1:1</span></div>
<div class="history-body">
<div class="history-name">瑜伽裤 × Zoe</div>
<div class="history-type">模特上身图 · 12 张</div>
<div class="history-foot">
<span class="mono">// 05.18 · 11:20</span>
<span class="pill ok"><span class="dot"></span>已完成</span>
</div>
</div>
</div>
<div class="task-card history-card" data-status="ok" data-type="platform" data-name="咖啡粉 × 平台套图">
<button class="card-del-btn" type="button" title="删除任务" onclick="event.stopPropagation();"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
<div class="placeholder"><span class="ph-frame">XHS / AMZ · 1:1</span></div>
<div class="history-body">
<div class="history-name">咖啡粉 × 平台套图</div>
<div class="history-type">平台套图 · 4 张</div>
<div class="history-foot">
<span class="mono">// 05.17 · 16:50</span>
<span class="pill ok"><span class="dot"></span>已完成</span>
</div>
</div>
</div>
</div>
</div>
<!-- ===== 删除确认 modal (复用 projects.html 风格) ===== -->
<div class="modal-bg" id="tc-del-bg">
<div class="modal" role="dialog">
<span class="corner-tr" aria-hidden></span>
<span class="corner-bl" aria-hidden></span>
<div class="modal-h">
<div class="ic-m" style="background:var(--crimson-bg,#fdebea);color:var(--accent-crimson,#c43d3d)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg>
</div>
<div class="ti">确认删除任务<span>// CONFIRM DELETE</span></div>
</div>
<div class="modal-b" id="tc-del-body">即将删除任务记录。</div>
<div class="modal-f">
<button class="btn" type="button" id="tc-del-cancel">取消</button>
<button class="btn" type="button" id="tc-del-ok" style="background:var(--accent-crimson);color:var(--accent-white);border-color:var(--accent-crimson)">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/></svg>
确认删除
</button>
</div>
</div>
</div>
<script src="assets/shell.js"></script>
<script src="assets/new-product-drawer.js"></script>
<script>
Shell.render({
active: 'asset-factory',
crumbs: [{ label: '工作台', href: 'index.html' }, { label: '图片生成' }]
});
/* ============================================================
任务中心 · projects.html 风格 (tabs + toolbar + 双视图)
============================================================ */
(function () {
'use strict';
const TYPE_LABEL = { model: '模特上身图', platform: '平台套图' };
const STATUS_LABEL = { ok: '已完成', gen: '生成中', err: '失败' };
const STATUS_PILL = { ok: 'ok', gen: 'info', err: 'err' };
const taskGrid = document.getElementById('task-grid');
const listTbody = document.getElementById('task-list-tbody');
const gridView = document.getElementById('task-grid');
const listView = document.getElementById('task-list-view');
const cards = [...taskGrid.querySelectorAll('.task-card')];
const state = { filter: 'all', type: 'all', search: '', view: 'list' };
function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c])); }
// 任务行点击 → 跳转到对应工作台,携带商品名(任务名一般是「商品 × 模特/平台」格式)
function goToWorkbench(type, name) {
const productName = (name || '').split(/\s[×x]\s/)[0].trim();
const q = '?t=' + Date.now() + (productName ? '&product=' + encodeURIComponent(productName) : '');
const url = (type === 'model') ? 'model-photo.html' + q
: (type === 'platform') ? 'platform-cover.html' + q
: 'projects-new.html' + q;
location.href = url;
}
/* ---------- 1. 从卡片生成 list 表行 (单数据源) ---------- */
function rowFor(card) {
const name = card.dataset.name;
const type = card.dataset.type;
const status = card.dataset.status;
const subText = (card.querySelector('.history-type')?.textContent || '').trim();
const timeText = (card.querySelector('.history-foot .mono')?.textContent || '').replace(/^\/\/\s*/, '');
const pct = card.querySelector('.history-prog .pct')?.textContent || '';
const pillClass = STATUS_PILL[status] || 'info';
const pillLabel = STATUS_LABEL[status] || status;
const tr = document.createElement('tr');
tr.dataset.taskRow = '1';
tr.dataset.name = name;
tr.dataset.type = type;
tr.dataset.status = status;
tr.addEventListener('click', () => goToWorkbench(type, name));
tr.innerHTML = `
<td>
<div class="task-name-cell">
<div class="placeholder task-thumb"><span class="ph-frame">${esc(name.split(' ')[0] || '')}</span></div>
<div>
<div class="task-name">${esc(name)}</div>
<div class="task-sub">${esc(subText)}</div>
</div>
</div>
</td>
<td><span class="muted">${esc(TYPE_LABEL[type] || type)}</span></td>
<td>${status === 'gen'
? `<div class="task-list-prog"><div class="bar"><span style="width:${pct || '60%'}"></span></div><span class="pct">${esc(pct || '60%')}</span></div>`
: (status === 'ok' ? '<span class="muted-2 mono" style="font-size:11px;">已完成</span>' : '<span class="muted-2 mono" style="font-size:11px;"></span>')}</td>
<td><span class="pill ${pillClass}"><span class="dot"></span>${esc(pillLabel)}</span></td>
<td class="muted-2">${esc(timeText)}</td>
<td>
<div class="row-action">
<span class="row-more"><svg width="14" height="14" viewBox="0 0 16 16"><circle cx="3" cy="8" r="1.2" fill="currentColor"/><circle cx="8" cy="8" r="1.2" fill="currentColor"/><circle cx="13" cy="8" r="1.2" fill="currentColor"/></svg>
<div class="row-more-tip"><button class="mi mi-del-task" type="button"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg>删除任务</button></div>
</span>
</div>
</td>
`;
// 阻止 row-more 区域冒泡触发整行点击
tr.querySelector('.row-more').addEventListener('click', e => e.stopPropagation());
return tr;
}
cards.forEach(card => {
const tr = rowFor(card);
listTbody.appendChild(tr);
card._listRow = tr;
tr._card = card;
});
/* ---------- 2. 构建类型 chip 菜单 ---------- */
const checkSvg = '<svg class="mi-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg>';
const typeMenu = document.querySelector('.chip-wrap[data-key="type"] .chip-menu');
const typeOptions = [...new Set(cards.map(c => c.dataset.type))];
typeMenu.innerHTML = `<div class="mi selected" data-value="all">${checkSvg}<span>全部任务类型</span></div><div class="mi-sep"></div>`
+ typeOptions.map(v => `<div class="mi" data-value="${esc(v)}">${checkSvg}<span>${esc(TYPE_LABEL[v] || v)}</span></div>`).join('');
function syncTypeChip() {
const wrap = document.querySelector('.chip-wrap[data-key="type"]');
const label = wrap.querySelector('.chip-label');
const chip = wrap.querySelector('.chip');
if (state.type === 'all') {
label.textContent = '任务类型';
chip.classList.remove('active');
} else {
label.textContent = TYPE_LABEL[state.type] || state.type;
chip.classList.add('active');
}
wrap.querySelectorAll('.mi').forEach(mi => mi.classList.toggle('selected', mi.dataset.value === state.type));
}
/* ---------- 3. applyFilter ---------- */
function applyFilter() {
const q = state.search.toLowerCase();
let visible = 0;
cards.forEach(card => {
const okStatus = state.filter === 'all' || card.dataset.status === state.filter;
const okType = state.type === 'all' || card.dataset.type === state.type;
const okSearch = !q || (card.dataset.name || '').toLowerCase().includes(q);
const show = okStatus && okType && okSearch;
card.style.display = show ? '' : 'none';
if (card._listRow) card._listRow.style.display = show ? '' : 'none';
if (show) visible++;
});
// 计数
const counts = { all: cards.length, gen: 0, ok: 0, err: 0 };
cards.forEach(c => { if (counts[c.dataset.status] !== undefined) counts[c.dataset.status]++; });
document.querySelectorAll('#tc-tabs .tab').forEach(t => {
const f = t.dataset.filter;
t.querySelector('.count').textContent = f === 'all' ? counts.all : counts[f];
});
document.getElementById('tc-sub-total').textContent = counts.all;
document.getElementById('tc-sub-gen').textContent = counts.gen;
document.getElementById('tc-sub-ok').textContent = counts.ok;
document.getElementById('tc-sub-err').textContent = counts.err;
document.getElementById('tc-result-meta').innerHTML = `// 显示 <span class="count">${visible}</span> / ${cards.length} 个任务`;
document.getElementById('tc-clear').hidden = !(state.search || state.type !== 'all');
}
/* ---------- 4. 事件绑定 ---------- */
// status tabs
document.querySelectorAll('#tc-tabs .tab').forEach(t => {
t.addEventListener('click', () => {
document.querySelectorAll('#tc-tabs .tab').forEach(x => x.classList.remove('active'));
t.classList.add('active');
state.filter = t.dataset.filter;
applyFilter();
});
});
// search
document.getElementById('tc-search').addEventListener('input', e => {
state.search = e.target.value.trim();
applyFilter();
});
// type chip
const typeWrap = document.querySelector('.chip-wrap[data-key="type"]');
typeWrap.querySelector('.chip').addEventListener('click', e => {
e.stopPropagation();
const isOpen = typeWrap.classList.contains('open');
document.querySelectorAll('.chip-wrap.open').forEach(w => w.classList.remove('open'));
if (!isOpen) typeWrap.classList.add('open');
});
typeMenu.addEventListener('click', e => {
const mi = e.target.closest('.mi');
if (!mi) return;
e.stopPropagation();
state.type = mi.dataset.value;
typeWrap.classList.remove('open');
syncTypeChip();
applyFilter();
});
document.addEventListener('click', () => {
document.querySelectorAll('.chip-wrap.open').forEach(w => w.classList.remove('open'));
});
// clear filters
document.getElementById('tc-clear').addEventListener('click', () => {
state.search = '';
state.type = 'all';
document.getElementById('tc-search').value = '';
syncTypeChip();
applyFilter();
Shell.toast('已清空筛选');
});
// view toggle
document.querySelectorAll('#tc-view-toggle button').forEach(b => {
b.addEventListener('click', () => {
document.querySelectorAll('#tc-view-toggle button').forEach(x => x.classList.remove('active'));
b.classList.add('active');
state.view = b.dataset.view;
if (state.view === 'list') { listView.hidden = false; gridView.hidden = true; }
else { listView.hidden = true; gridView.hidden = false; }
});
});
/* ---------- 5. 删除 modal + 配对删除 (复用 projects.html 模式) ---------- */
const delBg = document.getElementById('tc-del-bg');
const delBody = document.getElementById('tc-del-body');
const delCancel = document.getElementById('tc-del-cancel');
const delOk = document.getElementById('tc-del-ok');
let _delTarget = null;
function openDelConfirm(target) {
_delTarget = target;
const name = target.dataset.name || '该任务';
delBody.innerHTML = '即将删除任务 <span class="mono-acc">' + esc(name) + '</span>。任务记录将清除,已入库的素材不受影响。';
delBg.classList.add('show');
}
function closeDelConfirm() { delBg.classList.remove('show'); _delTarget = null; }
delCancel.addEventListener('click', closeDelConfirm);
delBg.addEventListener('click', e => { if (e.target === delBg) closeDelConfirm(); });
delOk.addEventListener('click', () => {
if (!_delTarget) return;
const card = _delTarget._card || _delTarget; // 可能传 row (with ._card) 或 card (with ._listRow)
const row = card._listRow;
const name = card.dataset.name;
card.remove();
if (row) row.remove();
// 同步 cards 数组
const idx = cards.indexOf(card);
if (idx >= 0) cards.splice(idx, 1);
closeDelConfirm();
Shell.toast('已删除', name);
applyFilter();
});
// 绑定网格卡 删除按钮
document.querySelectorAll('.task-card .card-del-btn').forEach(btn => {
btn.addEventListener('click', e => {
e.stopPropagation();
const card = btn.closest('.task-card');
if (card) openDelConfirm(card);
});
});
// 绑定网格卡片点击 → 跳工作台
cards.forEach(card => {
card.style.cursor = 'pointer';
card.addEventListener('click', e => {
if (e.target.closest('.card-del-btn')) return;
goToWorkbench(card.dataset.type, card.dataset.name);
});
});
// 绑定列表行 删除按钮 (事件委托, 因为是动态生成)
listTbody.addEventListener('click', e => {
const btn = e.target.closest('.mi-del-task');
if (!btn) return;
e.stopPropagation();
const tr = btn.closest('tr');
if (tr) openDelConfirm(tr);
});
/* ---------- 6. 初始化 ---------- */
applyFilter();
})();
</script>
</body>
</html>

View File

@ -0,0 +1,578 @@
/* ============================================================
新建商品 · 共享 Drawer 模块
----------------------------------------------------------
在任意页面只需 <script src="assets/new-product-drawer.js"> 引入,
然后调用 NewProductDrawer.open({ onSave: fn }) 即可在当前页之上
弹出右侧 Drawer点击遮罩 / X / 取消 / ESC 关闭后,用户停在原页面
提供:
window.NewProductDrawer.open(opts?)
window.NewProductDrawer.close()
opts 字段:
onSave(product) 保存时回调,product = { id, name, cat, target,
points: string[], images: { id, dataUrl, name }[] }
============================================================ */
(function () {
'use strict';
if (window.NewProductDrawer) return; // idempotent
const DRAWER_ID = 'npd-drawer';
const DRAWER_BG_ID = 'npd-drawer-bg';
/* ---------- 注入样式(独立 namespace 以免与 products.html 冲突) ---------- */
const CSS = `
/* drawer base (相同尺寸/动画) */
#${DRAWER_BG_ID} {
position: fixed; inset: 0;
background: rgba(21, 20, 15, .32);
display: none; z-index: 1100;
}
#${DRAWER_BG_ID}.show { display: block; }
#${DRAWER_ID} {
position: fixed; right: 0; top: 0; bottom: 0;
width: 820px; max-width: 100vw;
background: var(--surface);
border-left: 1px solid var(--border-faint);
z-index: 1101;
transform: translateX(100%);
transition: transform .25s cubic-bezier(.32, .72, 0, 1);
display: flex; flex-direction: column;
box-shadow: -4px 0 24px rgba(21, 20, 15, .04);
}
#${DRAWER_ID}.show { transform: translateX(0); }
#${DRAWER_ID} .drawer-h {
padding: 20px 24px;
border-bottom: 1px solid var(--border-faint);
display: flex; align-items: center;
}
#${DRAWER_ID} .drawer-h h3 { font-size: 16px; font-weight: 600; color: var(--accent-black); }
#${DRAWER_ID} .drawer-h .x {
margin-left: auto; width: 32px; height: 32px;
border-radius: var(--r-md);
display: grid; place-items: center;
color: var(--black-alpha-56); cursor: pointer;
background: transparent; border: 0;
transition: background var(--t-base);
}
#${DRAWER_ID} .drawer-h .x:hover { background: var(--black-alpha-4); color: var(--accent-black); }
#${DRAWER_ID} .drawer-b { padding: 24px 28px; overflow-y: auto; flex: 1; overscroll-behavior: contain; }
#${DRAWER_ID} .drawer-f {
padding: 14px 24px;
border-top: 1px solid var(--border-faint);
display: flex; gap: 10px; align-items: center;
background: var(--surface);
}
#${DRAWER_ID} .drawer-f .btn-guide {
margin-right: auto;
display: inline-flex; align-items: center; gap: 6px;
font-size: 13px; color: var(--black-alpha-56);
background: transparent; border: 0; cursor: pointer;
padding: 8px 10px; border-radius: var(--r-md);
font-family: inherit;
transition: background var(--t-base), color var(--t-base);
}
#${DRAWER_ID} .drawer-f .btn-guide:hover { color: var(--accent-black); background: var(--black-alpha-4); }
#${DRAWER_ID} .drawer-f .btn-guide svg { width: 14px; height: 14px; }
/* form-card */
#${DRAWER_ID} .form-h {
font-size: 15px; font-weight: 600; color: var(--accent-black);
margin-bottom: 18px; padding-bottom: 12px;
border-bottom: 1px solid var(--border-faint);
}
#${DRAWER_ID} .field { margin-bottom: 16px; }
#${DRAWER_ID} .field:last-child { margin-bottom: 0; }
#${DRAWER_ID} .field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-bottom: 16px; }
#${DRAWER_ID} .field-label {
display: block; font-size: 13px; font-weight: 500;
color: var(--accent-black); margin-bottom: 6px;
}
#${DRAWER_ID} .field-label .req { color: var(--heat); margin-left: 2px; }
#${DRAWER_ID} .field-label .opt {
color: var(--black-alpha-48); font-weight: 400; font-size: 12px; margin-left: 6px;
}
#${DRAWER_ID} .input,
#${DRAWER_ID} .select {
width: 100%; height: 38px;
background: var(--background-lighter);
border: 1px solid var(--black-alpha-12);
border-radius: var(--r-md);
padding: 0 14px;
font-size: 13.5px; color: var(--accent-black);
outline: none; font-family: inherit;
transition: border-color var(--t-base);
}
#${DRAWER_ID} .input:focus,
#${DRAWER_ID} .select:focus {
border-color: var(--heat-40);
box-shadow: inset 0 0 0 1px var(--heat-40);
}
/* upload */
#${DRAWER_ID} .pf-upload-row {
display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
gap: 16px; align-items: stretch;
}
#${DRAWER_ID} .pf-upload-zone {
border: 1.5px dashed var(--black-alpha-24);
border-radius: var(--r-md);
padding: 28px 20px;
background: var(--background-lighter);
cursor: pointer; text-align: center;
transition: border-color var(--t-base), background var(--t-base);
display: flex; flex-direction: column; align-items: center; justify-content: center;
min-height: 180px;
}
#${DRAWER_ID} .pf-upload-zone:hover { border-color: var(--heat); background: var(--heat-8); }
#${DRAWER_ID} .pf-upload-zone .uz-ic {
width: 44px; height: 44px;
margin: 0 auto 10px;
background: var(--surface);
border: 1px solid var(--heat-20);
border-radius: var(--r-md);
color: var(--heat);
display: grid; place-items: center;
}
#${DRAWER_ID} .pf-upload-zone .uz-ic svg { width: 20px; height: 20px; }
#${DRAWER_ID} .pf-upload-zone .uz-t { font-size: 14px; color: var(--accent-black); font-weight: 500; }
#${DRAWER_ID} .pf-upload-zone .uz-t strong { color: var(--heat); font-weight: 600; }
#${DRAWER_ID} .pf-upload-zone .uz-d {
margin-top: 8px;
font-family: var(--font-mono); font-size: 11.5px;
color: var(--black-alpha-48); letter-spacing: .02em;
}
#${DRAWER_ID} .pf-example {
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
padding: 16px;
display: flex; flex-direction: column; gap: 10px;
}
#${DRAWER_ID} .pf-example .ex-h { font-size: 13px; font-weight: 600; color: var(--accent-black); }
#${DRAWER_ID} .pf-example .ex-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
#${DRAWER_ID} .pf-example .ex-grid .ex-thumb {
aspect-ratio: 1;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
overflow: hidden; position: relative;
display: grid; place-items: center;
color: var(--black-alpha-32);
}
#${DRAWER_ID} .pf-example .ex-grid .ex-thumb svg { width: 22px; height: 22px; }
#${DRAWER_ID} .pf-example .ex-grid .ex-thumb::after {
content: ''; position: absolute; inset: 0;
background: repeating-linear-gradient(135deg, transparent 0 6px, rgba(0,0,0,.03) 6px 7px);
pointer-events: none;
}
#${DRAWER_ID} .pf-example .ex-d { font-size: 12px; color: var(--black-alpha-56); line-height: 1.5; }
#${DRAWER_ID} .pf-grid {
display: grid; grid-template-columns: repeat(5, 1fr);
gap: 8px; margin-top: 12px;
}
#${DRAWER_ID} .pf-grid:empty { display: none; }
#${DRAWER_ID} .pf-thumb {
aspect-ratio: 1;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
position: relative; overflow: hidden; cursor: pointer;
}
#${DRAWER_ID} .pf-thumb img { width: 100%; height: 100%; object-fit: cover; }
#${DRAWER_ID} .pf-thumb .pf-x {
position: absolute; top: 4px; right: 4px;
width: 22px; height: 22px;
background: rgba(0,0,0,.7); color: var(--accent-white);
border: 0; border-radius: 50%; cursor: pointer;
display: grid; place-items: center;
opacity: 0; transition: opacity var(--t-base);
}
#${DRAWER_ID} .pf-thumb:hover .pf-x { opacity: 1; }
#${DRAWER_ID} .pf-thumb .pf-x svg { width: 11px; height: 11px; }
/* bullet-list */
#${DRAWER_ID} .bullet-list { list-style: none; padding: 0; margin: 0; }
#${DRAWER_ID} .bullet-list .bl-item,
#${DRAWER_ID} .bullet-list .bl-add {
display: flex; align-items: center; gap: 10px;
padding: 8px 12px;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
margin-bottom: 6px;
font-size: 13.5px;
}
#${DRAWER_ID} .bullet-list .bl-add { background: transparent; border-style: dashed; }
#${DRAWER_ID} .bullet-list .num {
width: 22px; height: 22px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
font-family: var(--font-mono);
font-size: 11px; color: var(--heat); font-weight: 700;
display: grid; place-items: center; flex-shrink: 0;
}
#${DRAWER_ID} .bullet-list .bl-text { flex: 1; color: var(--accent-black); }
#${DRAWER_ID} .bullet-list .bl-input {
flex: 1; background: transparent; border: 0; outline: none;
font-size: 13.5px; color: var(--accent-black); font-family: inherit;
}
#${DRAWER_ID} .bullet-list .bl-x {
width: 22px; height: 22px;
color: var(--black-alpha-48);
cursor: pointer; display: grid; place-items: center;
border-radius: var(--r-sm);
transition: color var(--t-base), background var(--t-base);
}
#${DRAWER_ID} .bullet-list .bl-x:hover { color: var(--accent-crimson); background: var(--crimson-bg); }
#${DRAWER_ID} .bullet-list .bl-x svg { width: 11px; height: 11px; }
@media (max-width: 900px) {
#${DRAWER_ID} .pf-upload-row { grid-template-columns: 1fr; }
}
`;
const HTML = `
<div id="${DRAWER_BG_ID}"></div>
<aside id="${DRAWER_ID}" role="dialog" aria-label="新建商品" aria-hidden="true">
<div class="drawer-h">
<h3>新建商品</h3>
<button class="x" type="button" data-act="close" aria-label="关闭">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 6l12 12M6 18L18 6"/></svg>
</button>
</div>
<div class="drawer-b">
<div class="form-card">
<div class="form-h">基础信息</div>
<div class="field">
<label class="field-label">商品名称<span class="req">*</span></label>
<input class="input" data-f="name" placeholder="请输入商品名称(必填)" maxlength="100">
</div>
<div class="field-row">
<div>
<label class="field-label">品类<span class="req">*</span></label>
<select class="select" data-f="cat">
<option>美妆个护</option>
<option>服饰内衣</option>
<option>食品饮料</option>
<option>家居家电</option>
<option>数码 3C</option>
<option>个护清洁</option>
<option>运动户外</option>
<option>母婴亲子</option>
</select>
</div>
<div>
<label class="field-label">目标人群<span class="opt">(选填)</span></label>
<input class="input" data-f="target" placeholder="例: 22-32 岁女性、敏感肌、办公室通勤">
</div>
</div>
<div class="field">
<label class="field-label">商品主图<span class="req">*</span></label>
<input type="file" data-f="file" accept="image/*" multiple hidden>
<div class="pf-upload-row">
<div class="pf-upload-zone" data-act="upload-zone">
<div class="uz-ic">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M17 8l-5-5-5 5M12 3v12"/></svg>
</div>
<div class="uz-t">点击上传或<strong>拖拽图片</strong></div>
<div class="uz-d">// 支持 JPG、PNG 格式,建议尺寸 800×800 以上,大小不超过 10MB</div>
</div>
<div class="pf-example">
<div class="ex-h">示例图</div>
<div class="ex-grid">
<div class="ex-thumb"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M7 4h10l1 4v12H6V8l1-4z"/><path d="M9 4v3M15 4v3M9 11h6M9 14h6"/></svg></div>
<div class="ex-thumb"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="5" width="12" height="15" rx="2"/><path d="M9 9h6M9 12h6M9 15h4"/></svg></div>
<div class="ex-thumb"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3h8l1 5v12H7V8l1-5z"/><circle cx="12" cy="13" r="2.5"/></svg></div>
</div>
<div class="ex-d">优质的商品图有助于生成更好的素材效果</div>
</div>
</div>
<div class="pf-grid" data-f="grid"></div>
</div>
<div class="field" style="margin-bottom: 0;">
<label class="field-label">核心卖点<span class="req">*</span></label>
<ul class="bullet-list" data-f="bullets">
<li class="bl-add"><span class="num">+</span><input class="bl-input" placeholder=" · "></li>
</ul>
</div>
</div>
</div>
<div class="drawer-f">
<button class="btn-guide" type="button" data-act="guide">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M9.5 9a2.5 2.5 0 015 0c0 1.5-2.5 2-2.5 4M12 17h.01"/></svg>
使用指南
</button>
<button class="btn" type="button" data-act="cancel">取消</button>
<button class="btn btn-primary" type="button" data-act="save">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l5 5L20 6"/></svg>
创建商品
</button>
</div>
</aside>
`;
/* ---------- DOM refs (populated by ensureInjected) ---------- */
let injected = false;
let bg, drawer, $f, $grid, $bullets, $blInput;
let currentOpts = {};
const PF_MAX = 5;
let pfFiles = []; // { id, dataUrl, name }
const blXSvg = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M4 4l8 8M12 4l-8 8"/></svg>';
function esc(s) { return String(s == null ? '' : s).replace(/[<>&"]/g, c => ({ '<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;' })[c]); }
function uid() { return Date.now().toString(36) + Math.random().toString(36).slice(2, 5); }
function toast(msg, sub) {
if (typeof Shell !== 'undefined' && Shell && Shell.toast) Shell.toast(msg, sub);
}
function ensureInjected() {
if (injected) return;
// style
const styleEl = document.createElement('style');
styleEl.textContent = CSS;
document.head.appendChild(styleEl);
// html
const wrap = document.createElement('div');
wrap.innerHTML = HTML;
while (wrap.firstChild) document.body.appendChild(wrap.firstChild);
bg = document.getElementById(DRAWER_BG_ID);
drawer = document.getElementById(DRAWER_ID);
$f = {
name: drawer.querySelector('[data-f="name"]'),
cat: drawer.querySelector('[data-f="cat"]'),
target: drawer.querySelector('[data-f="target"]'),
file: drawer.querySelector('[data-f="file"]'),
};
$grid = drawer.querySelector('[data-f="grid"]');
$bullets = drawer.querySelector('[data-f="bullets"]');
$blInput = $bullets.querySelector('.bl-add .bl-input');
bindEvents();
injected = true;
}
function bindEvents() {
// 关闭交互
bg.addEventListener('click', close);
drawer.addEventListener('click', e => {
const a = e.target.closest('[data-act]');
if (!a) return;
const act = a.dataset.act;
if (act === 'close') return close();
if (act === 'cancel') return close();
if (act === 'guide') return toast('使用指南', '点击查看完整填写指南');
if (act === 'save') return save();
if (act === 'upload-zone') return openFilePicker();
});
document.addEventListener('keydown', e => {
if (e.key === 'Escape' && drawer.classList.contains('show')) close();
});
// 上传
$f.file.addEventListener('change', e => { addFiles(e.target.files); e.target.value = ''; });
const zone = drawer.querySelector('[data-act="upload-zone"]');
zone.addEventListener('dragover', e => { e.preventDefault(); zone.style.borderColor = 'var(--heat)'; });
zone.addEventListener('dragleave', () => { zone.style.borderColor = ''; });
zone.addEventListener('drop', e => {
e.preventDefault(); zone.style.borderColor = '';
if (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length) addFiles(e.dataTransfer.files);
});
// 卖点 bullet-list
$blInput.addEventListener('keydown', e => {
if (e.key === 'Enter') { e.preventDefault(); blAdd($blInput.value); $blInput.value = ''; }
});
}
function openFilePicker() { if (pfFiles.length < PF_MAX) $f.file.click(); }
function pfRender() {
$grid.innerHTML = pfFiles.map(u => `
<div class="pf-thumb" data-id="${u.id}">
<img src="${u.dataUrl}" alt="${esc(u.name)}">
<button class="pf-x" type="button" title="删除">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 4l8 8M12 4l-8 8"/></svg>
</button>
</div>
`).join('');
$grid.querySelectorAll('.pf-thumb .pf-x').forEach(b => {
b.onclick = e => {
e.stopPropagation();
const id = b.closest('.pf-thumb').dataset.id;
const i = pfFiles.findIndex(f => f.id === id);
if (i >= 0) { pfFiles.splice(i, 1); pfRender(); }
};
});
}
function addFiles(fileList) {
const room = PF_MAX - pfFiles.length;
if (room <= 0) { toast('已达上限', PF_MAX + ' / ' + PF_MAX + ' 张'); return; }
const incoming = [...fileList].filter(f => f.type.startsWith('image/')).slice(0, room);
let done = 0;
incoming.forEach(f => {
const r = new FileReader();
r.onload = e => {
pfFiles.push({ id: uid(), dataUrl: e.target.result, name: f.name });
if (++done === incoming.length) {
pfRender();
toast('已上传', '+ ' + done + ' 张 · 共 ' + pfFiles.length + ' / ' + PF_MAX);
}
};
r.readAsDataURL(f);
});
}
function blRenumber() {
[...$bullets.querySelectorAll('.bl-item')].forEach((li, i) => {
li.querySelector('.num').textContent = i + 1;
});
}
function blAdd(text) {
const t = (text || '').trim();
if (!t) return;
const li = document.createElement('li');
li.className = 'bl-item';
li.innerHTML = '<span class="num">0</span><span class="bl-text">' + esc(t) + '</span><span class="bl-x" title="删除">' + blXSvg + '</span>';
$bullets.querySelector('.bl-add').before(li);
li.querySelector('.bl-x').addEventListener('click', () => {
li.style.transition = 'opacity .15s, transform .15s';
li.style.opacity = 0;
li.style.transform = 'translateX(-8px)';
setTimeout(() => { li.remove(); blRenumber(); }, 150);
});
blRenumber();
}
function getBullets() {
return [...$bullets.querySelectorAll('.bl-item .bl-text')].map(t => t.textContent.trim()).filter(Boolean);
}
/* ---------- API ---------- */
function resetForm() {
$f.name.value = '';
$f.cat.value = $f.cat.options[0].value;
$f.target.value = '';
pfFiles = [];
pfRender();
[...$bullets.querySelectorAll('.bl-item')].forEach(li => li.remove());
$blInput.value = '';
}
function lockBody() {
// 优先用 Shell 的引用计数实现(避免多 overlay 互相解锁)
if (typeof Shell !== 'undefined' && Shell && typeof Shell.lockScroll === 'function') {
Shell.lockScroll();
return;
}
// 兜底: Shell 未加载时本地锁
const docEl = document.documentElement;
const sbw = window.innerWidth - docEl.clientWidth;
drawer._lockSnap = {
bodyOverflow: document.body.style.overflow,
bodyPaddingRight: document.body.style.paddingRight,
htmlOverflow: docEl.style.overflow,
};
document.body.style.overflow = 'hidden';
docEl.style.overflow = 'hidden';
if (sbw > 0) document.body.style.paddingRight = sbw + 'px';
}
function unlockBody() {
if (typeof Shell !== 'undefined' && Shell && typeof Shell.unlockScroll === 'function') {
Shell.unlockScroll();
return;
}
const s = drawer._lockSnap;
if (s) {
document.body.style.overflow = s.bodyOverflow;
document.body.style.paddingRight = s.bodyPaddingRight;
document.documentElement.style.overflow = s.htmlOverflow;
drawer._lockSnap = null;
}
}
function open(opts) {
ensureInjected();
if (drawer.classList.contains('show')) return; // 已开则不重复锁
currentOpts = opts || {};
resetForm();
bg.classList.add('show');
drawer.classList.add('show');
drawer.setAttribute('aria-hidden', 'false');
lockBody();
setTimeout(() => $f.name.focus(), 280);
}
function close() {
if (!injected) return;
if (!drawer.classList.contains('show')) return; // 已关则不重复解锁
bg.classList.remove('show');
drawer.classList.remove('show');
drawer.setAttribute('aria-hidden', 'true');
unlockBody();
if (typeof currentOpts.onClose === 'function') currentOpts.onClose();
}
function save() {
const name = ($f.name.value || '').trim();
const cat = $f.cat.value;
const target = ($f.target.value || '').trim();
const points = getBullets();
const images = pfFiles.slice();
if (!name) {
toast('请填写商品名称');
$f.name.focus();
return;
}
if (images.length === 0) {
toast('请上传商品主图', '至少 1 张');
return;
}
if (points.length === 0) {
toast('请添加核心卖点', '至少 1 条');
$blInput.focus();
return;
}
const product = {
id: 'np-' + uid(),
name, cat, target,
points,
images,
imgs: images.length,
};
toast('商品已创建', '+ ' + name);
if (typeof currentOpts.onSave === 'function') currentOpts.onSave(product);
close();
}
window.NewProductDrawer = { open, close };
/* ---------- sessionStorage 自动打开钩子 ---------- */
// 任何页面只要在跳转前 sessionStorage.setItem('npd-auto-open','1') 即可,
// 落地页加载完模块后,会自动 open() 一次并清掉 flag。
// 用于:product-create.html 重定向后让落地页弹出 drawer,而不是用户重新点击。
function checkAutoOpen() {
try {
if (sessionStorage.getItem('npd-auto-open') === '1') {
sessionStorage.removeItem('npd-auto-open');
// 延后一拍,确保宿主页面自己的 init 已经跑完
setTimeout(() => open(), 50);
}
} catch (e) { /* sessionStorage 不可用就静默放弃 */ }
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', checkAutoOpen);
} else {
checkAutoOpen();
}
})();

1442
v2/assets/restraint.css Normal file

File diff suppressed because it is too large Load Diff

602
v2/assets/shell.js Normal file
View File

@ -0,0 +1,602 @@
/* ============================================================
·Studio · Shell renderer V2.1
渲染 sidebar / topbar / 网格背景装饰 / Toast / Modal helpers
每个页面调用 Shell.render({ active, crumbs, balance, topActions })
V2.1 变化:
- sidebar 搜索 K "Ctrl K" Inter Bold 平铺( kbd 边框)
============================================================ */
const NAV = [
{
id: 'dashboard', label: '工作台', href: 'index.html',
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 12 12 3l9 9"/><path d="M5 10v10h14V10"/></svg>'
},
{
id: 'products', label: '商品库', href: 'products.html', badge: '7',
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="m3 7 9-4 9 4-9 4-9-4z"/><path d="m3 12 9 4 9-4M3 17l9 4 9-4"/></svg>'
},
{
id: 'projects', label: '视频项目', href: 'projects.html', badge: '8',
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="4" width="18" height="16"/><path d="M7 4v16M16 4v16M3 9h18M3 15h18"/></svg>'
},
{
id: 'asset-factory', label: '图片生成', href: 'asset-factory.html',
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l1.7 4.6L18 9l-4.3 1.4L12 15l-1.7-4.6L6 9l4.3-1.4L12 3z"/><path d="M19 16l.85 2.3L22 19l-2.15.7L19 22l-.85-2.3L16 19l2.15-.7L19 16z"/></svg>'
},
{
id: 'library', label: '资产库', href: 'library.html',
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="4" width="6" height="16"/><rect x="11" y="4" width="4" height="16"/><rect x="17" y="6" width="4" height="14"/></svg>'
},
{
id: 'team', label: '团队', href: 'team.html', badge: '5',
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="9" cy="8" r="3"/><circle cx="17" cy="9" r="2.5"/><path d="M3 19c0-3 2.7-5 6-5s6 2 6 5M14 19c.5-2.4 2.4-4 5-4 .8 0 1.5.2 2 .5"/></svg>'
},
{
id: 'account', label: '账户', href: 'account.html',
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="6" width="18" height="13" rx="2"/><path d="M3 10h18M16 14h2"/></svg>'
},
{
id: 'settings', label: '设置', href: 'settings.html',
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8 2 2 0 0 1-2.8 2.8 1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5 2 2 0 0 1-4 0 1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3 2 2 0 0 1-2.8-2.8 1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1 2 2 0 0 1 0-4 1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8 2 2 0 0 1 2.8-2.8 1.7 1.7 0 0 0 1.8.3 1.7 1.7 0 0 0 1-1.5 2 2 0 0 1 4 0 1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3 2 2 0 0 1 2.8 2.8 1.7 1.7 0 0 0-.3 1.8 1.7 1.7 0 0 0 1.5 1 2 2 0 0 1 0 4 1.7 1.7 0 0 0-1.5 1.1Z"/></svg>'
}
];
window.Shell = {
render({ active = '', crumbs = [], balance = '¥327.40', topActions = '' } = {}) {
const navHtml = NAV.map(n => `
<a href="${n.href}" class="${active === n.id ? 'active' : ''}">
${n.icon}
<span>${n.label}</span>
${n.badge ? `<span class="pill-mini">${n.badge}</span>` : ''}
</a>
`).join('');
const sidebar = `
<aside class="sidebar">
<div class="brand">
<div class="flame"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2c1 3 4 5 4 9a4 4 0 0 1-4 4 4 4 0 0 1-4-4 5 5 0 0 1 1.5-3.5C10.5 6 11.5 4 12 2zm-1 13c0 2 1 3 1 5 1-1 3-2 3-5 0-1.5-1-2-2-3-1 1-2 2-2 3z"/></svg></div>
<div><div class="name">·Studio</div></div>
</div>
<div class="search-box" onclick="document.getElementById('global-search').focus()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
<input id="global-search" placeholder="搜索"/>
<span class="kbd">Ctrl K</span>
</div>
<div class="nav-section">主要</div>
<nav>${navHtml}</nav>
<div class="aside-foot">
<div class="user" onclick="Shell.toast('账户菜单', 'li@shop.com')">
<div class="av"></div>
<div class="em">小李的店</div>
</div>
</div>
</aside>
`;
const crumbHtml = crumbs.length ? `
<div class="crumbs">
${crumbs.map((c, i) => {
const last = i === crumbs.length - 1;
const sep = i > 0 ? '<span class="sep">/</span>' : '';
if (last) return `${sep}<span class="here">${c.label}</span>`;
return `${sep}<a href="${c.href || '#'}">${c.label}</a>`;
}).join('')}
</div>
` : '';
const topbar = `
<header class="topbar">
${crumbHtml}
<div class="right">
<span class="balance-chip" onclick="location.href='account.html'">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="9"/><path d="M12 7v10M9 10h4a1.5 1.5 0 0 1 0 3h-3a1.5 1.5 0 0 0 0 3h4"/></svg>
余额 <strong>${balance}</strong>
</span>
<button class="queue-chip" onclick="Shell.toast('任务队列', '3 个进行中')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
任务队列
<span class="count">3</span>
</button>
<button class="icon-btn" onclick="Shell.toast('通知中心', '12 条未读')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9M10 21a2 2 0 0 0 4 0"/></svg>
<span class="count-noti">12</span>
</button>
<div class="topbar-avatar" onclick="Shell.toast('账户菜单', '李 · li@shop.com')" title="账户">
<span></span>
</div>
${topActions}
</div>
</header>
`;
const decorations = `
<div class="grid-bg"></div>
<span class="sq-mark" style="top:238px;left:478px"></span>
<span class="sq-mark" style="top:478px;left:1198px"></span>
<span class="sq-mark" style="bottom:300px;left:238px"></span>
<span class="sq-mark" style="top:718px;right:240px"></span>
`;
const toastHtml = `
<div class="toast" id="__toast">
<div class="ic-t"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><polyline points="20 6 9 17 4 12"/></svg></div>
<div class="txt" id="__toast-txt">操作成功<span class="mono">[ 200 OK ]</span></div>
</div>
`;
// ─── 全局 Lightbox · 任意页面可用 Shell._openLightbox(src, name) ──
const lightboxHtml = `
<div class="np-lightbox" id="np-lightbox" onclick="Shell._closeLightbox()">
<button class="lb-x" type="button" onclick="event.stopPropagation();Shell._closeLightbox()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M18 6L6 18M6 6l12 12"/></svg>
</button>
<img id="np-lightbox-img" alt="">
<span class="lb-name" id="np-lightbox-name"></span>
</div>
`;
// [DEPRECATED · 弹窗已废弃,创建商品直接进 product-create.html]
const _deprecatedModalHtml = `
<div class="modal-bg" id="new-product-bg" onclick="if(event.target===this)Shell.closeModal('new-product-bg')">
<div class="modal new-product-modal">
<span class="corner-tr"></span>
<span class="corner-bl"></span>
<div class="np-header">
<div class="np-title-ic">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="m3 7 9-4 9 4-9 4-9-4z"/><path d="m3 12 9 4 9-4M3 17l9 4 9-4"/></svg>
</div>
<h2>新建商品</h2>
<span class="np-mode-pill">[ UPLOAD MODE ]</span>
<button class="np-x" type="button" onclick="Shell.closeModal('new-product-bg')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M18 6L6 18M6 6l12 12"/></svg>
</button>
</div>
<div class="np-body">
<div class="np-body-grid">
<!-- 左栏: 图片 -->
<div class="np-left">
<!-- AI CTA: 独立全宽区域,放在 商品图册 上方 -->
<a class="np-ai-cta primary" id="np-ai-cta" title="不上传也能用 AI 生成图">
<span class="ai-icon">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l1.5 4.5L18 8l-4.5 1.5L12 14l-1.5-4.5L6 8l4.5-1.5L12 2zM19 14l.9 2.7 2.6.8-2.6.8L19 21l-.9-2.7-2.6-.8 2.6-.8L19 14zM5 14l.7 2.1L7.8 17l-2.1.7L5 20l-.7-2.3-2.1-.7 2.1-.7L5 14z"/></svg>
</span>
<span class="ai-label" id="np-ai-label">没有图? AI 帮我生成</span>
<span class="ai-arrow">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
</span>
</a>
<div class="field" style="margin-bottom: 12px;">
<label class="field-label">商品图册<span class="req">*</span></label>
<input type="file" id="np-file" accept="image/*" multiple hidden>
<!-- 图片案例 (静态示例) -->
<div class="np-section-h">
<span class="check-ic"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 8 7 12 13 4"/></svg></span>
图片案例
</div>
<div class="np-section-sub">上传您的商品的多角度白底图和使用图</div>
<div class="np-examples">
<div class="ex"><span>白底主图</span><span class="ex-badge"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M2 21h4V9H2v12zM23 10c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg></span></div>
<div class="ex"><span>多角度</span><span class="ex-badge"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M2 21h4V9H2v12zM23 10c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg></span></div>
<div class="ex"><span>细节图</span><span class="ex-badge"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M2 21h4V9H2v12zM23 10c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg></span></div>
<div class="ex"><span>使用图</span><span class="ex-badge"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M2 21h4V9H2v12zM23 10c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg></span></div>
<div class="ex"><span>包装图</span><span class="ex-badge"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M2 21h4V9H2v12zM23 10c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg></span></div>
</div>
<!-- 我的上传 (动态 5 ) -->
<div class="np-section-h">
我的上传
<span class="counter">( <span class="num" id="np-upload-count">0</span> / 5 )</span>
</div>
<div class="upload-grid" id="np-grid"></div>
<!-- Dropzone (放在 grid 之后, 5 张时禁用) -->
<div class="upload-zone" id="np-zone" style="margin-top: 10px;">
<span class="uz-ic">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M17 8l-5-5-5 5M12 3v12"/></svg>
</span>
<span id="np-zone-text">点击或拖拽上传图片</span>
<span class="uz-hint">// 支持多选 · 最多 5 张 · JPG / PNG / WEBP</span>
</div>
</div>
</div>
<!-- 右栏: 文案 -->
<div class="np-right">
<div class="field">
<label class="field-label">商品名称<span class="req">*</span></label>
<input class="input" id="np-name" placeholder="例: 透真玻尿酸补水面膜">
</div>
<div class="field">
<label class="field-label">品类</label>
<select class="select" id="np-cat">
<option>美妆个护</option>
<option>服饰内衣</option>
<option>食品饮料</option>
<option>家居家电</option>
<option>数码 3C</option>
<option>个护清洁</option>
<option>运动户外</option>
<option>母婴亲子</option>
</select>
</div>
<div class="field">
<label class="field-label">核心卖点<span class="req">*</span></label>
<ul class="bullet-list" id="np-bullets" data-bl>
<li class="bl-add"><span class="num">+</span><input class="bl-input" placeholder=" · "></li>
</ul>
</div>
<div class="field" style="margin-bottom:0;">
<label class="field-label">目标人群</label>
<input class="input" id="np-target" placeholder="例: 22-32 岁女性、敏感肌、办公室通勤">
</div>
</div>
</div>
</div>
<div class="np-footer">
<span class="np-meta">// 上传模式 · <span class="accent">不消耗 token</span> · 一次性创建</span>
<button class="btn" type="button" onclick="Shell.closeModal('new-product-bg')">取消</button>
<button class="btn btn-primary" type="button" id="np-create" disabled style="opacity:.5;cursor:not-allowed;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l5 5L20 6"/></svg>
创建商品
</button>
</div>
</div>
</div>
<!-- Lightbox · 缩略图全屏预览 -->
<div class="np-lightbox" id="np-lightbox" onclick="Shell._closeLightbox()">
<button class="lb-x" type="button" onclick="event.stopPropagation();Shell._closeLightbox()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M18 6L6 18M6 6l12 12"/></svg>
</button>
<img id="np-lightbox-img" alt="">
<span class="lb-name" id="np-lightbox-name"></span>
</div>
`;
// 装订线 SVG 准星 · V2.1 签名元素(圆弧内凹的"+")
const cornerSvg = `<path d="M10.5 4C10.5 7.31371 7.81371 10 4.5 10H0.5V11H4.5C7.81371 11 10.5 13.6863 10.5 17V21H11.5V17C11.5 13.6863 14.1863 11 17.5 11H21.5V10H17.5C14.1863 10 11.5 7.31371 11.5 4V0H10.5V4Z" fill="currentColor"/>`;
const cornerMarks = `
<span class="corner-mark tl"><svg viewBox="0 0 22 21" fill="none">${cornerSvg}</svg></span>
<span class="corner-mark tr"><svg viewBox="0 0 22 21" fill="none">${cornerSvg}</svg></span>
<span class="corner-mark bl"><svg viewBox="0 0 22 21" fill="none">${cornerSvg}</svg></span>
<span class="corner-mark br"><svg viewBox="0 0 22 21" fill="none">${cornerSvg}</svg></span>
`;
const app = document.createElement('div');
app.className = 'app';
app.innerHTML = sidebar + `<main>${decorations}${topbar}<div class="content" id="page-content">${cornerMarks}</div></main>`;
const src = document.getElementById('page');
document.body.prepend(app);
if (src) {
// 把页面 body 内容追加到 .content,保留 4 个 corner-mark SVG
document.getElementById('page-content').insertAdjacentHTML('beforeend', src.innerHTML);
src.remove();
}
document.body.insertAdjacentHTML('beforeend', toastHtml);
document.body.insertAdjacentHTML('beforeend', lightboxHtml);
document.addEventListener('keydown', e => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault();
document.getElementById('global-search')?.focus();
}
});
},
// [DEPRECATED] 新建商品弹窗 · 已废弃,创建商品改为直跳 product-create.html
_bindNewProductModal_DEPRECATED() {
const modal = document.getElementById('new-product-bg');
if (!modal) return;
// 上传图册的内存状态 (供 collect 和 AI CTA 用)
const uploads = []; // { id, dataUrl, name, type, size }
// 收集表单状态(供 AI CTA 和创建按钮用)
const collect = () => {
const get = (id) => document.getElementById(id)?.value?.trim() || '';
return {
name: get('np-name'),
cat: get('np-cat'),
target: get('np-target'),
bullets: [...document.querySelectorAll('#np-bullets .bl-item .bl-text')].map(el => el.textContent),
uploadedCount: uploads.length,
uploads: uploads.map(u => ({ name: u.name, type: u.type })),
};
};
const MAX_UPLOADS = 5;
// 创建按钮: 商品名 + 至少 1 张图
const nameInput = document.getElementById('np-name');
const createBtn = document.getElementById('np-create');
const updateCreateBtn = () => {
const nameOk = (nameInput.value || '').trim().length > 0;
const uploadOk = uploads.length >= 1;
const ok = nameOk && uploadOk;
createBtn.disabled = !ok;
createBtn.style.opacity = ok ? '' : '.5';
createBtn.style.cursor = ok ? '' : 'not-allowed';
// 提示用户缺什么
if (!nameOk) createBtn.title = '请填写商品名称';
else if (!uploadOk) createBtn.title = '请至少上传 1 张商品图';
else createBtn.title = '';
};
nameInput.addEventListener('input', updateCreateBtn);
createBtn.addEventListener('click', () => {
if (createBtn.disabled) return;
const s = collect();
Shell.toast('商品已创建', `+ ${s.name}`);
Shell.closeModal('new-product-bg');
});
// 核心卖点: bullet-list
const list = document.getElementById('np-bullets');
const addInput = list.querySelector('.bl-add .bl-input');
const xSvg = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M4 4l8 8M12 4l-8 8"/></svg>';
const renumber = () => {
[...list.querySelectorAll('.bl-item')].forEach((li, i) => {
li.querySelector('.num').textContent = i + 1;
});
};
const bindX = (x) => {
x.addEventListener('click', () => {
const li = x.closest('li');
li.style.transition = 'opacity .15s, transform .15s';
li.style.opacity = 0;
li.style.transform = 'translateX(-8px)';
setTimeout(() => { li.remove(); renumber(); }, 150);
});
};
const addBullet = (text) => {
const t = (text || '').trim();
if (!t) return;
const li = document.createElement('li');
li.className = 'bl-item';
li.innerHTML = `<span class="num">0</span><span class="bl-text">${t.replace(/[<>&]/g, c => ({ '<':'&lt;','>':'&gt;','&':'&amp;' })[c])}</span><span class="bl-x" title="删除">${xSvg}</span>`;
list.querySelector('.bl-add').before(li);
bindX(li.querySelector('.bl-x'));
renumber();
};
addInput?.addEventListener('keydown', e => {
if (e.key === 'Enter') {
e.preventDefault();
addBullet(addInput.value);
addInput.value = '';
}
});
// 上传组件: 批量上传(MAX 5) + 5 固定槽 + 预览/删除 + AI CTA 联动
const zone = document.getElementById('np-zone');
const zoneText = document.getElementById('np-zone-text');
const grid = document.getElementById('np-grid');
const fileInput = document.getElementById('np-file');
const aiCtaEl = document.getElementById('np-ai-cta');
const aiLabel = document.getElementById('np-ai-label');
const uploadCount = document.getElementById('np-upload-count');
const syncAiCta = () => {
if (uploads.length === 0) {
aiCtaEl.classList.add('primary');
aiLabel.textContent = '没有图? 让 AI 帮我生成';
} else {
aiCtaEl.classList.remove('primary');
aiLabel.textContent = '用 AI 加工 / 生成模特上身图';
}
};
const syncZone = () => {
const full = uploads.length >= MAX_UPLOADS;
zone.classList.toggle('full', full);
zoneText.textContent = full ? `已达上限 (${MAX_UPLOADS} / ${MAX_UPLOADS})` : '点击或拖拽上传图片';
};
const syncCounter = () => {
uploadCount.textContent = uploads.length;
};
const thumbXSvg = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 4l8 8M12 4l-8 8"/></svg>';
const esc = (s) => s.replace(/[<>&"]/g, c => ({ '<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;' })[c]);
const renderGrid = () => {
const filledHtml = uploads.map(u => `
<div class="up-thumb" data-id="${u.id}">
<img src="${u.dataUrl}" alt="${esc(u.name)}">
<button class="slot-x" type="button" title="删除">${thumbXSvg}</button>
</div>
`).join('');
const emptyCount = MAX_UPLOADS - uploads.length;
const emptyHtml = Array.from({ length: emptyCount }, () => `<div class="up-slot-empty" data-action="add">无预览</div>`).join('');
grid.innerHTML = filledHtml + emptyHtml;
// 已填充槽: 点击预览 + X 删除
grid.querySelectorAll('.up-thumb').forEach(thumb => {
const id = thumb.dataset.id;
thumb.querySelector('.slot-x').addEventListener('click', (e) => {
e.stopPropagation();
const idx = uploads.findIndex(u => u.id === id);
if (idx >= 0) {
const removed = uploads.splice(idx, 1)[0];
Shell.toast('已删除', removed.name);
refreshAll();
}
});
thumb.addEventListener('click', () => {
const u = uploads.find(x => x.id === id);
if (u) Shell._openLightbox(u.dataUrl, u.name);
});
});
// 空槽: 点击触发上传
grid.querySelectorAll('[data-action="add"]').forEach(slot => {
slot.addEventListener('click', () => fileInput.click());
});
};
const refreshAll = () => {
renderGrid();
syncAiCta();
syncZone();
syncCounter();
updateCreateBtn();
};
const addFiles = (fileList) => {
const remaining = MAX_UPLOADS - uploads.length;
if (remaining <= 0) {
Shell.toast('已达上传上限', `${MAX_UPLOADS} / ${MAX_UPLOADS}`);
return;
}
const incoming = [...fileList].filter(f => f.type.startsWith('image/'));
if (!incoming.length) return;
const accepted = incoming.slice(0, remaining);
const overflow = incoming.length - accepted.length;
let added = 0;
accepted.forEach(f => {
const reader = new FileReader();
reader.onload = (e) => {
uploads.push({
id: Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
dataUrl: e.target.result,
name: f.name,
type: f.type,
size: f.size,
});
added++;
if (added === accepted.length) {
refreshAll();
const msg = overflow > 0
? `+ ${added} 张 · 超出 ${overflow} 张已忽略`
: `+ ${added} 张 · 共 ${uploads.length} / ${MAX_UPLOADS}`;
Shell.toast('图片已上传', msg);
}
};
reader.readAsDataURL(f);
});
};
fileInput.addEventListener('change', (e) => {
addFiles(e.target.files);
e.target.value = '';
});
zone.addEventListener('click', () => {
if (uploads.length < MAX_UPLOADS) fileInput.click();
});
zone.addEventListener('dragover', e => {
e.preventDefault();
if (uploads.length < MAX_UPLOADS) zone.style.borderColor = 'var(--heat)';
});
zone.addEventListener('dragleave', () => { zone.style.borderColor = ''; });
zone.addEventListener('drop', e => {
e.preventDefault();
zone.style.borderColor = '';
if (e.dataTransfer?.files?.length) addFiles(e.dataTransfer.files);
});
refreshAll();
// AI CTA: 把已填表单 + 上传图册存到 sessionStorage,跳到 AI 工作台
const aiCta = document.getElementById('np-ai-cta');
aiCta.addEventListener('click', (e) => {
e.preventDefault();
const s = collect();
sessionStorage.setItem('pending-product', JSON.stringify(s));
Shell.toast('正在跳转 AI 工作台', `带入 ${s.uploadedCount} 张图`);
setTimeout(() => { location.href = 'product-create.html'; }, 350);
});
updateCreateBtn();
},
_openLightbox(src, name) {
const lb = document.getElementById('np-lightbox');
const img = document.getElementById('np-lightbox-img');
const nm = document.getElementById('np-lightbox-name');
if (!lb || !img || lb.classList.contains('show')) return;
img.src = src;
if (nm) nm.textContent = name || '';
lb.classList.add('show');
this.lockScroll();
},
_closeLightbox() {
const lb = document.getElementById('np-lightbox');
if (!lb || !lb.classList.contains('show')) return;
lb.classList.remove('show');
this.unlockScroll();
},
toast(text, mono) {
const t = document.getElementById('__toast');
const txt = document.getElementById('__toast-txt');
if (!t || !txt) return;
txt.innerHTML = text + (mono ? `<span class="mono">[ ${mono} ]</span>` : '');
t.classList.add('show');
clearTimeout(this._tt);
this._tt = setTimeout(() => t.classList.remove('show'), 2400);
},
/* ─── Body scroll lock (引用计数 · 多 overlay 叠加安全) ─── */
_scrollLockCount: 0,
_scrollLockSnapshot: null,
lockScroll() {
if (++this._scrollLockCount > 1) return;
const docEl = document.documentElement;
const sbw = window.innerWidth - docEl.clientWidth;
this._scrollLockSnapshot = {
bodyOverflow: document.body.style.overflow,
bodyPaddingRight: document.body.style.paddingRight,
htmlOverflow: docEl.style.overflow,
};
document.body.style.overflow = 'hidden';
docEl.style.overflow = 'hidden';
if (sbw > 0) document.body.style.paddingRight = sbw + 'px';
},
unlockScroll() {
if (--this._scrollLockCount > 0) return;
this._scrollLockCount = 0;
const s = this._scrollLockSnapshot;
if (s) {
document.body.style.overflow = s.bodyOverflow;
document.body.style.paddingRight = s.bodyPaddingRight;
document.documentElement.style.overflow = s.htmlOverflow;
this._scrollLockSnapshot = null;
}
},
openModal(id) {
const el = document.getElementById(id);
if (!el || el.classList.contains('show')) return;
el.classList.add('show');
this.lockScroll();
},
closeModal(id) {
const el = document.getElementById(id);
if (!el || !el.classList.contains('show')) return;
el.classList.remove('show');
this.unlockScroll();
},
openDrawer(id) {
const el = document.getElementById(id);
const bg = document.getElementById(id + '-bg');
if (!el || el.classList.contains('show')) return;
el.classList.add('show');
if (bg) bg.classList.add('show');
this.lockScroll();
},
closeDrawer(id) {
const el = document.getElementById(id);
const bg = document.getElementById(id + '-bg');
if (!el || !el.classList.contains('show')) return;
el.classList.remove('show');
if (bg) bg.classList.remove('show');
this.unlockScroll();
}
};

1730
v2/design-system.html Normal file

File diff suppressed because it is too large Load Diff

176
v2/index.html Normal file
View File

@ -0,0 +1,176 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>工作台 · 流·Studio</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css">
<style>
.dash-grid { display: grid; grid-template-columns: 1.7fr 1fr; gap: 24px; align-items: start; }
.recent-row { display: grid; grid-template-columns: 54px 1fr 110px 130px 60px; align-items: center; gap: 16px; padding: 14px 18px; border-bottom: 1px solid var(--border-faint); cursor: pointer; }
.recent-row .prog, .recent-row .pill, .recent-row .btn { justify-self: start; }
.recent-row:last-child { border-bottom: 0; }
.recent-row:hover { background: var(--background-lighter); }
.recent-row .thumb { width: 54px; height: 70px; border-radius: var(--r-md); }
.recent-meta .name { font-weight: 600; font-size: 13.5px; color: var(--accent-black); }
.recent-meta .sub { font-size: 12px; color: var(--black-alpha-48); margin-top: 3px; font-family: var(--font-mono); letter-spacing: .01em; }
.shortcuts { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.shortcut { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 16px; display: flex; align-items: flex-start; gap: 12px; cursor: pointer; transition: background var(--t-base); }
.shortcut:hover { background: var(--black-alpha-4); }
.shortcut .ic { width: 32px; height: 32px; background: var(--heat-12); color: var(--heat); display: grid; place-items: center; border-radius: var(--r-md); flex-shrink: 0; }
.shortcut .ic svg { width: 16px; height: 16px; }
.shortcut .t { font-size: 13px; font-weight: 600; }
.shortcut .d { font-size: 11.5px; color: var(--black-alpha-48); margin-top: 3px; font-family: var(--font-mono); letter-spacing: .01em; }
.tip { background: var(--surface); border: 1px dashed var(--border-faint); padding: 14px 16px; font-size: 12.5px; color: var(--black-alpha-56); line-height: 1.6; border-radius: var(--r-md); }
.tip strong { color: var(--accent-black); font-weight: 600; display: block; margin-bottom: 4px; }
.tip .mono { font-family: var(--font-mono); color: var(--heat); background: var(--heat-12); padding: 1px 5px; border-radius: var(--r-sm); font-size: 11.5px; }
</style>
</head>
<body>
<div id="page">
<div class="page-head">
<div>
<h1>欢迎回来,小李</h1>
<div class="sub">
<span class="mono">// 05.14 · 周三</span>
<span>·</span>
<span>你有 <b style="color:var(--accent-black)">3 个项目</b> 正在进行中</span>
</div>
</div>
<div class="actions">
<a class="btn" href="javascript:void(0)" onclick="event.preventDefault(); window.NewProductDrawer && NewProductDrawer.open();">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
新建商品
</a>
<a class="btn btn-primary btn-lg" href="projects-new.html">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
新建项目
</a>
</div>
</div>
<div class="stats with-corners">
<span class="corner-tr" aria-hidden></span><span class="corner-bl" aria-hidden></span>
<a class="stat" href="projects.html">
<div class="lbl">总项目 <span class="badge">ALL</span></div>
<div class="v">8</div>
<div class="delta up"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V5M5 12l7-7 7 7"/></svg> 本月 +3</div>
</a>
<a class="stat" href="projects.html">
<div class="lbl">进行中 <span class="badge">WIP</span></div>
<div class="v">3</div>
<div class="delta">2 个待审核</div>
</a>
<a class="stat" href="projects.html">
<div class="lbl">本月成片 <span class="badge">DONE</span></div>
<div class="v">3</div>
<div class="delta up"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V5M5 12l7-7 7 7"/></svg> 较上月 +33%</div>
</a>
<a class="stat" href="account.html">
<div class="lbl">余额 <span class="badge">¥</span></div>
<div class="v">¥327<small>.40</small></div>
<div class="bar"><span style="width:33%"></span></div>
<div class="sub">已用 ¥162.60 / ¥500</div>
</a>
</div>
<div class="dash-grid">
<div>
<div class="section-h">
<h2>最近项目</h2>
<a class="more" href="projects.html">[ ALL · 8 ] <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle;"><path d="M5 12h14M12 5l7 7-7 7"/></svg></a>
</div>
<div class="card-hard">
<a class="recent-row" href="pipeline.html#stage-3">
<div class="placeholder thumb"><span class="ph-frame">9:16</span></div>
<div class="recent-meta">
<div class="name">补水面膜 · 痛点种草</div>
<div class="sub">补水面膜 / AI 全生 / 6 镜</div>
</div>
<div class="prog"><span class="done"></span><span class="done"></span><span class="cur"></span><span></span><span></span></div>
<span class="pill info"><span class="dot"></span>故事板 待确认</span>
<span class="btn btn-sm">继续</span>
</a>
<a class="recent-row" href="pipeline.html#stage-5">
<div class="placeholder thumb"><span class="ph-frame">9:16</span></div>
<div class="recent-meta">
<div class="name">蓝牙耳机 · 开箱测评</div>
<div class="sub">南卡 Lite Pro / 自带脚本 / 5 镜</div>
</div>
<div class="prog"><span class="done"></span><span class="done"></span><span class="done"></span><span class="done"></span><span class="done"></span></div>
<span class="pill ok"><span class="dot"></span>已完成</span>
<span class="btn btn-sm">打开</span>
</a>
<a class="recent-row" href="pipeline.html#stage-2">
<div class="placeholder thumb"><span class="ph-frame">9:16</span></div>
<div class="recent-meta">
<div class="name">速食牛肉面 · 一句话主题</div>
<div class="sub">滋啦速食 / 一句话 / 4 镜</div>
</div>
<div class="prog"><span class="done"></span><span class="cur"></span><span></span><span></span><span></span></div>
<span class="pill info"><span class="dot"></span>资产生成中</span>
<span class="btn btn-sm">继续</span>
</a>
<a class="recent-row" href="pipeline.html#stage-4">
<div class="placeholder thumb"><span class="ph-frame">9:16</span></div>
<div class="recent-meta">
<div class="name">防晒霜 · 对比展示</div>
<div class="sub">透真防晒 / AI 全生 / 6 镜</div>
</div>
<div class="prog"><span class="done"></span><span class="done"></span><span class="done"></span><span class="cur"></span><span></span></div>
<span class="pill info"><span class="dot"></span>视频生成 4/6</span>
<span class="btn btn-sm">继续</span>
</a>
<a class="recent-row" href="pipeline.html#stage-3">
<div class="placeholder thumb"><span class="ph-frame">9:16</span></div>
<div class="recent-meta">
<div class="name">咖啡冻干粉 · 剧情带货</div>
<div class="sub">三顿半同款 / 一句话 / 5 镜</div>
</div>
<div class="prog"><span class="done"></span><span class="done"></span><span class="fail"></span><span></span><span></span></div>
<span class="pill err"><span class="dot"></span>故事板失败</span>
<span class="btn btn-sm">查看</span>
</a>
</div>
</div>
<div style="display:flex;flex-direction:column;gap:24px">
<div>
<div class="section-h"><h2>快捷入口</h2><span class="more">[ /shortcuts ]</span></div>
<div class="shortcuts">
<a class="shortcut" href="products.html">
<div class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="m3 7 9-4 9 4-9 4-9-4z"/><path d="m3 12 9 4 9-4"/></svg></div>
<div><div class="t">商品库</div><div class="d">7 SKU</div></div>
</a>
<a class="shortcut" href="library.html">
<div class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="6" height="16"/><rect x="11" y="4" width="4" height="16"/><rect x="17" y="6" width="4" height="14"/></svg></div>
<div><div class="t">资产库</div><div class="d">人 8 · 景 14 · 片 8</div></div>
</a>
<a class="shortcut" href="account.html">
<div class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 7v10M9 10h4a1.5 1.5 0 0 1 0 3h-3a1.5 1.5 0 0 0 0 3h4"/></svg></div>
<div><div class="t">充值</div><div class="d">¥327.40</div></div>
</a>
<a class="shortcut" href="projects.html">
<div class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16"/><path d="M7 4v16M16 4v16M3 9h18M3 15h18"/></svg></div>
<div><div class="t">所有项目</div><div class="d">8 个</div></div>
</a>
</div>
</div>
<div>
<div class="section-h"><h2>提示</h2><span class="more">[ FAQ ]</span></div>
<div class="tip">
<strong>扣费规则</strong>
生成失败、超时、用户重跑 — 均不扣费。仅在你点 <span class="mono">[ 确认通过 ]</span> 时按 token 实际结算。
</div>
</div>
</div>
</div>
</div>
<script src="assets/shell.js"></script>
<script src="assets/new-product-drawer.js"></script>
<script>Shell.render({ active: 'dashboard', crumbs: [{ label: '工作台' }] });</script>
</body>
</html>

1805
v2/library.html Normal file

File diff suppressed because it is too large Load Diff

1961
v2/model-photo.html Normal file

File diff suppressed because it is too large Load Diff

1737
v2/pipeline.html Normal file

File diff suppressed because it is too large Load Diff

1360
v2/platform-cover.html Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,14 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>新建商品 · 跳转中...</title>
<script>
// 已废弃 · 新建商品改为 products.html 上的居中弹窗(Shell.openNewProduct)
// 直接访问此 URL 时,跳回商品库并自动打开弹窗
sessionStorage.setItem('auto-open-new-product', '1');
location.replace('products.html');
</script>
</head>
<body></body>
</html>

413
v2/product-create-v2.html Normal file
View File

@ -0,0 +1,413 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>新建商品 · 流·Studio</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css">
<style>
/* ─── 主表单 ─── */
.form-grid {
display: grid; grid-template-columns: 1.05fr 1fr; gap: 24px;
margin-bottom: 24px;
}
.form-card {
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
padding: 24px;
}
.form-card .card-h {
display: flex; align-items: center; gap: 10px;
margin-bottom: 16px;
}
.form-card .card-h h3 {
font-size: 14px; font-weight: 600; color: var(--accent-black);
}
.form-card .card-h .req-tag {
font-family: var(--font-mono); font-size: 10px;
padding: 2px 7px;
background: var(--crimson-bg); color: var(--accent-crimson);
border-radius: var(--r-sm); letter-spacing: .04em;
border: 1px solid var(--red-bd);
}
.form-card .card-h .opt-tag {
font-family: var(--font-mono); font-size: 10px;
padding: 2px 7px;
background: var(--background-lighter); color: var(--black-alpha-56);
border-radius: var(--r-sm); letter-spacing: .04em;
border: 1px solid var(--border-faint);
}
.form-card .card-sub {
font-family: var(--font-mono); font-size: 11.5px;
color: var(--black-alpha-48); margin: -10px 0 14px; letter-spacing: .02em;
}
/* 原图槽位 */
.photo-grid {
display: grid; grid-template-columns: repeat(5, 1fr); gap: 8px;
margin-top: 8px;
}
.photo-slot {
aspect-ratio: 1;
border-radius: var(--r-md);
border: 1px dashed var(--border-faint);
background: var(--background-lighter);
display: grid; place-items: center;
color: var(--black-alpha-32);
cursor: pointer;
overflow: hidden;
position: relative;
font-size: 10px; font-family: var(--font-mono); letter-spacing: .04em;
transition: all var(--t-base);
}
.photo-slot:hover { border-color: var(--heat); color: var(--heat); background: var(--heat-8); }
.photo-slot.filled {
border-style: solid;
background-size: cover; background-position: center;
cursor: default; color: transparent;
}
.photo-slot.filled:hover { border-color: var(--heat-40); }
.photo-slot .slot-label {
position: absolute; top: 5px; left: 5px;
font-family: var(--font-mono); font-size: 9.5px; font-weight: 600;
padding: 2px 6px;
background: rgba(255,255,255,.92); color: var(--black-alpha-72);
border-radius: var(--r-sm); letter-spacing: .04em;
}
.photo-slot .slot-main {
position: absolute; top: 5px; right: 5px;
font-family: var(--font-mono); font-size: 9.5px; font-weight: 600;
padding: 2px 6px; background: var(--heat); color: #fff;
border-radius: var(--r-sm); letter-spacing: .04em;
}
.photo-slot .slot-x {
position: absolute; top: 4px; right: 4px;
width: 18px; height: 18px;
border-radius: 999px;
background: rgba(21,20,15,.7); color: #fff;
display: none; place-items: center; cursor: pointer; border: 0;
}
.photo-slot.filled:hover .slot-x { display: grid; }
.photo-slot.filled:hover .slot-main { display: none; }
.photo-slot .slot-x svg { width: 9px; height: 9px; }
.photo-slot .plus { width: 22px; height: 22px; border: 1px solid currentColor; border-radius: var(--r-sm); display: grid; place-items: center; margin-bottom: 4px; }
.photo-slot .plus svg { width: 12px; height: 12px; }
.upload-tip {
display: flex; align-items: center; gap: 8px;
margin-top: 14px; padding: 10px 12px;
background: var(--heat-8); border: 1px dashed var(--heat-40);
border-radius: var(--r-md);
font-size: 12px; color: var(--accent-black); line-height: 1.5;
}
.upload-tip svg { width: 14px; height: 14px; color: var(--heat); flex-shrink: 0; }
.upload-tip strong { color: var(--heat); font-weight: 600; }
/* AI 提示 banner(选填字段说明) */
.ai-tip {
margin-top: -6px; margin-bottom: 16px;
padding: 10px 12px;
background: var(--background-lighter);
border-radius: var(--r-md);
border: 1px dashed var(--border-faint);
display: flex; align-items: flex-start; gap: 8px;
font-size: 12px; color: var(--black-alpha-72); line-height: 1.55;
}
.ai-tip svg { width: 13px; height: 13px; color: var(--heat); flex-shrink: 0; margin-top: 2px; }
.ai-tip strong { color: var(--accent-black); font-weight: 600; }
/* 卖点 bullet 输入 */
.sell-list { list-style: none; margin: 0; padding: 0; }
.sell-list li {
display: flex; align-items: center; gap: 8px;
padding: 8px 10px;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
margin-bottom: 6px;
font-size: 13px;
}
.sell-list li.add { background: var(--surface); border-style: dashed; }
.sell-list .num {
width: 18px; height: 18px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
font-size: 10.5px; font-family: var(--font-mono);
color: var(--black-alpha-56);
display: grid; place-items: center; flex-shrink: 0;
}
.sell-list li.add .num { background: transparent; color: var(--heat); border-color: var(--heat-40); }
.sell-list .txt { flex: 1; min-width: 0; }
.sell-list .bl-input {
flex: 1; border: 0; background: transparent;
font-size: 13px; color: var(--accent-black); padding: 0;
font-family: inherit;
}
.sell-list .bl-input::placeholder { color: var(--black-alpha-48); }
.sell-list .bl-x {
width: 20px; height: 20px;
display: grid; place-items: center;
color: var(--black-alpha-48); cursor: pointer;
background: transparent; border: 0; opacity: 0;
transition: opacity var(--t-base);
}
.sell-list li:hover .bl-x { opacity: 1; }
.sell-list .bl-x:hover { color: var(--accent-crimson); }
.sell-list .bl-x svg { width: 11px; height: 11px; }
/* 底部操作行 */
.form-foot {
position: sticky; bottom: 0;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
padding: 14px 22px;
display: flex; align-items: center; gap: 14px;
margin-top: 8px;
}
.form-foot .req-info {
font-family: var(--font-mono); font-size: 11.5px;
color: var(--black-alpha-48); letter-spacing: .02em;
}
.form-foot .req-info .ok { color: var(--accent-forest); }
.form-foot .req-info .miss { color: var(--accent-crimson); }
.form-foot .actions {
margin-left: auto;
display: flex; gap: 10px;
}
</style>
</head>
<body>
<div id="page">
<div class="page-head">
<div>
<h1>新建商品</h1>
<div class="sub"><span class="mono">// 上传原图 + 填写基本信息</span> · 保存后可在工作台逐步丰富素材</div>
</div>
</div>
<!-- ============ 表单 ============ -->
<div class="form-grid">
<!-- 左:原图 -->
<div class="form-card">
<div class="card-h">
<h3>商品原图</h3>
<span class="req-tag">必填</span>
</div>
<div class="card-sub">// 1-5 张 · 这是后续所有 AI 生成的源材料</div>
<input type="file" id="photo-input" accept="image/*" multiple hidden>
<div class="photo-grid" id="photo-grid"></div>
<div class="upload-tip">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="9"/><path d="M12 8v4M12 16h.01"/></svg>
<span>建议上传 <strong>正面 / 侧面 / 细节 / 包装</strong> 4 张,后续在工作台生成的<strong>白底三视图</strong>更准确。</span>
</div>
</div>
<!-- 右:基本信息 -->
<div class="form-card">
<div class="card-h">
<h3>基本信息</h3>
<span class="req-tag">必填</span>
</div>
<div class="field">
<label class="field-label">商品名称<span class="req">*</span></label>
<input class="input" id="p-name" placeholder="例: 透真玻尿酸补水面膜">
</div>
<div class="field">
<label class="field-label">品类<span class="req">*</span></label>
<select class="select" id="p-cat">
<option value="">— 选择品类 —</option>
<option>美妆个护</option>
<option>服饰内衣</option>
<option>食品饮料</option>
<option>家居家电</option>
<option>数码 3C</option>
<option>个护清洁</option>
<option>运动户外</option>
<option>母婴亲子</option>
</select>
</div>
<div class="field" style="margin-bottom:0;">
<label class="field-label">价格(元)</label>
<input class="input" id="p-price" type="number" placeholder="选填 · 仅用于素材生成参考">
</div>
</div>
</div>
<div class="form-card" style="margin-bottom: 24px;">
<div class="card-h">
<h3>卖点 & 人群</h3>
<span class="opt-tag">选填 · 推荐</span>
</div>
<div class="ai-tip">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l1.5 4.5L18 8l-4.5 1.5L12 14l-1.5-4.5L6 8l4.5-1.5L12 2z"/></svg>
<span>填上这两项,后续 AI 生脚本(<strong>痛点种草 / 剧情带货</strong> 等模板)质量明显更高 —— 系统会用卖点构造钩子,用人群定语气。现在不填也可以,做视频项目时仍可补。</span>
</div>
<div class="field">
<label class="field-label">核心卖点</label>
<div class="field-hint" style="margin: 4px 0 8px;">3-5 条要点,回车添加</div>
<ul class="sell-list" id="sell-list">
<li class="add"><span class="num">+</span><input class="bl-input" id="sell-input" placeholder="例: 玻尿酸双效保湿,4 小时持久水润"></li>
</ul>
</div>
<div class="field" style="margin-bottom:0;">
<label class="field-label">目标人群</label>
<input class="input" id="p-target" placeholder="例: 22-32 岁女性、敏感肌、办公室通勤">
</div>
</div>
<!-- ============ 底部操作 ============ -->
<div class="form-foot">
<span class="req-info" id="req-info">// 必填检查:<span class="miss">商品名 / 品类 / ≥1 张图</span> 未完成</span>
<div class="actions">
<a class="btn" href="products.html">取消</a>
<button class="btn btn-primary" id="save-btn" disabled>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l5 5L20 6"/></svg>
保存并进入工作台
</button>
</div>
</div>
</div>
<script src="assets/shell.js"></script>
<script>
Shell.render({
active: 'products',
crumbs: [
{ label: '工作台', href: 'index.html' },
{ label: '商品库', href: 'products.html' },
{ label: '新建' }
]
});
const MAX = 5;
const photos = []; // { id, dataUrl }
const SLOT_LABELS = ['主图', '细节 02', '细节 03', '细节 04', '细节 05'];
const $ = id => document.getElementById(id);
// 渲染槽位
function renderPhotos() {
const grid = $('photo-grid');
grid.innerHTML = '';
for (let i = 0; i < MAX; i++) {
const slot = document.createElement('div');
const p = photos[i];
if (p) {
slot.className = 'photo-slot filled';
slot.style.backgroundImage = `url("${p.dataUrl}")`;
slot.innerHTML = `
<span class="slot-label">${SLOT_LABELS[i]}</span>
${i === 0 ? '<span class="slot-main">MAIN</span>' : ''}
<button class="slot-x" type="button" data-i="${i}" aria-label="移除">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 4l8 8M12 4l-8 8"/></svg>
</button>
`;
} else if (i === photos.length) {
slot.className = 'photo-slot empty';
slot.innerHTML = `<div class="plus"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 5v14M5 12h14"/></svg></div><span>添加</span>`;
slot.addEventListener('click', () => $('photo-input').click());
} else {
slot.className = 'photo-slot';
slot.style.opacity = '.6';
}
grid.appendChild(slot);
}
// 绑定删除
grid.querySelectorAll('.slot-x').forEach(b => {
b.addEventListener('click', e => {
e.stopPropagation();
const i = +b.dataset.i;
photos.splice(i, 1);
renderPhotos();
syncSave();
});
});
}
// 文件上传
$('photo-input').addEventListener('change', e => {
const files = [...e.target.files].filter(f => f.type.startsWith('image/'));
const remain = MAX - photos.length;
files.slice(0, remain).forEach(f => {
const reader = new FileReader();
reader.onload = ev => {
photos.push({
id: Date.now().toString(36) + Math.random().toString(36).slice(2, 5),
dataUrl: ev.target.result,
});
renderPhotos();
syncSave();
};
reader.readAsDataURL(f);
});
e.target.value = '';
});
// 卖点
const sellList = $('sell-list');
const sellInput = $('sell-input');
sellInput.addEventListener('keydown', e => {
if (e.key !== 'Enter') return;
e.preventDefault();
const t = sellInput.value.trim();
if (!t) return;
const li = document.createElement('li');
li.innerHTML = `<span class="num"></span><span class="txt"></span><button class="bl-x" type="button" aria-label="删除"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M4 4l8 8M12 4l-8 8"/></svg></button>`;
li.querySelector('.txt').textContent = t;
sellList.querySelector('.add').before(li);
sellInput.value = '';
renumberSell();
li.querySelector('.bl-x').addEventListener('click', () => {
li.remove();
renumberSell();
});
});
function renumberSell() {
sellList.querySelectorAll('li:not(.add) .num').forEach((n, i) => n.textContent = i + 1);
}
// 必填检查
function syncSave() {
const hasName = $('p-name').value.trim().length > 0;
const hasCat = $('p-cat').value.length > 0;
const hasPhoto = photos.length > 0;
const ok = hasName && hasCat && hasPhoto;
$('save-btn').disabled = !ok;
const missing = [];
if (!hasName) missing.push('商品名');
if (!hasCat) missing.push('品类');
if (!hasPhoto) missing.push('≥1 张图');
const info = $('req-info');
if (ok) {
info.innerHTML = '// 必填检查:<span class="ok">已全部完成 ✓</span> · 可进入工作台';
} else {
info.innerHTML = `// 必填检查:<span class="miss">${missing.join(' / ')}</span> 未完成`;
}
}
$('p-name').addEventListener('input', syncSave);
$('p-cat').addEventListener('change', syncSave);
// 保存 → 跳工作台
$('save-btn').addEventListener('click', () => {
if ($('save-btn').disabled) return;
Shell.toast('商品已建档', `进入工作台`);
setTimeout(() => {
// 真实场景下会带商品 ID;demo 直接跳
location.href = 'product-studio.html?from=new&onboard=1';
}, 500);
});
renderPhotos();
syncSave();
</script>
</body>
</html>

54
v2/product-create.html Normal file
View File

@ -0,0 +1,54 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>新建商品 · 流·Studio</title>
<meta http-equiv="Cache-Control" content="no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<!-- 双保险:JS 没跑也能跳走 (1.5s 后强制回 products.html) -->
<meta http-equiv="refresh" content="1.5; url=products.html?npd=1">
<style>
html,body{background:#f9f9f9;margin:0;height:100%;font-family:'Inter',system-ui,sans-serif;}
.stub-msg{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:10px;color:#999;font-size:13px;}
.stub-msg .ttl{font-size:14px;color:#262626;font-weight:500;}
.stub-msg a{color:#fa5d19;text-decoration:underline;}
</style>
</head>
<body>
<div class="stub-msg">
<div class="ttl">正在打开「新建商品」…</div>
<div>如未自动跳转,<a href="products.html?npd=1">点击这里</a></div>
</div>
<script>
/* ============================================================
新建商品 · 重定向 stub
----------------------------------------------------------
旧版本是一个 3883 行的全屏 drawer 页(legacy 备份在 product-create.legacy.html)。
现在「新建商品」改为 drawer 形态在原页面弹出,直接访问此 URL 没有意义。
行为:
1. 设置 sessionStorage 标志,通知落地页自动打开 drawer
2. 优先回退到 referrer(用户来源页),否则去 products.html
3. cache-bust 参数 ?npd=1 强制浏览器重新拉新版宿主页
============================================================ */
(function () {
try { sessionStorage.setItem('npd-auto-open', '1'); } catch (e) {}
try { sessionStorage.setItem('auto-open-new-product', '1'); } catch (e) {}
const ref = document.referrer || '';
const here = location.origin + location.pathname;
// 同源 referrer 且不是自身才回跳, 否则去 products.html
let target = 'products.html';
if (ref && ref.indexOf(location.origin) === 0 && ref.split('#')[0].split('?')[0] !== here) {
target = ref;
}
// 加 cache-bust query · 强制浏览器拉新版宿主页,避免再次命中缓存旧版
const sep = target.indexOf('?') >= 0 ? '&' : '?';
target = target + sep + 'npd=' + Date.now().toString(36);
location.replace(target);
})();
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

1359
v2/product-detail.html Normal file

File diff suppressed because it is too large Load Diff

1082
v2/product-studio.html Normal file

File diff suppressed because it is too large Load Diff

1787
v2/products.html Normal file

File diff suppressed because it is too large Load Diff

1231
v2/projects-new.html Normal file

File diff suppressed because it is too large Load Diff

1063
v2/projects.html Normal file

File diff suppressed because it is too large Load Diff

524
v2/settings.html Normal file
View File

@ -0,0 +1,524 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>设置 · 流·Studio</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css">
<style>
/* ─── 设置布局:左 nav + 右 panel ─── */
.settings-grid { display: grid; grid-template-columns: 220px minmax(0, 1fr); gap: 24px; align-items: start; }
.settings-nav { position: sticky; top: 16px; }
.settings-nav .nav-h { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .06em; text-transform: uppercase; padding: 0 12px 8px; }
.settings-nav a { display: flex; align-items: center; gap: 10px; padding: 10px 12px; font-size: 13px; color: var(--accent-black); border-radius: var(--r-md); border: 1px solid transparent; cursor: pointer; text-decoration: none; transition: background var(--t-base), border-color var(--t-base); }
.settings-nav a:hover { background: var(--background-lighter); }
.settings-nav a.active { background: var(--heat-12); color: var(--heat); border-color: var(--heat-20); font-weight: 600; }
.settings-nav a svg { width: 16px; height: 16px; stroke-width: 1.5; }
/* ─── pane ─── */
.pane { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 24px; margin-bottom: 16px; }
.pane h3 { font-size: 14px; font-weight: 600; margin-bottom: 4px; }
.pane .pane-desc { font-size: 12px; color: var(--black-alpha-48); font-family: var(--font-mono); letter-spacing: .02em; margin-bottom: 18px; }
.pane.danger { border-color: rgba(180,30,30,.25); background: rgba(180,30,30,.03); }
.pane.danger h3 { color: var(--accent-crimson); }
/* ─── form row ─── */
.form-row { display: grid; grid-template-columns: 160px minmax(0, 1fr); gap: 16px; padding: 14px 0; border-bottom: 1px solid var(--border-faint); align-items: center; }
.form-row:last-child { border-bottom: 0; }
.form-row .lbl { font-size: 12.5px; color: var(--black-alpha-56); }
.form-row .lbl .req { color: var(--accent-crimson); margin-left: 2px; }
.form-row .lbl-sub { font-size: 11px; color: var(--black-alpha-48); font-family: var(--font-mono); margin-top: 2px; letter-spacing: .02em; }
.form-row .val { display: flex; align-items: center; gap: 10px; min-width: 0; }
.form-row .val .input, .form-row .val .select { width: 100%; max-width: 380px; }
.form-row .val .static { font-size: 13px; color: var(--accent-black); font-variant-numeric: tabular-nums; }
.form-row .val .static.mono { font-family: var(--font-mono); font-size: 12.5px; color: var(--black-alpha-56); }
.form-row .val .role-tag { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: var(--r-pill); font-size: 11px; font-weight: 500; background: var(--heat-12); color: var(--heat); }
.form-row .val .role-tag .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--heat); }
/* ─── 头像上传 ─── */
.avatar-edit { display: flex; align-items: center; gap: 16px; }
.avatar-edit .av-big { width: 64px; height: 64px; border-radius: 50%; background: var(--background-lighter); border: 1px solid var(--border-faint); display: grid; place-items: center; font-size: 24px; font-weight: 600; color: var(--accent-black); }
.avatar-edit .av-actions { display: flex; gap: 8px; }
/* ─── toggle switch ─── */
.switch { position: relative; width: 36px; height: 20px; flex: 0 0 36px; }
.switch input { opacity: 0; width: 0; height: 0; }
.switch .slider { position: absolute; inset: 0; background: var(--black-alpha-24); border-radius: 20px; cursor: pointer; transition: background var(--t-base); }
.switch .slider::before { content: ''; position: absolute; left: 2px; top: 2px; width: 16px; height: 16px; background: var(--accent-white); border-radius: 50%; transition: transform var(--t-base); }
.switch input:checked + .slider { background: var(--heat); }
.switch input:checked + .slider::before { transform: translateX(16px); }
/* ─── 偏好选项卡 ─── */
.pref-choices { display: grid; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); gap: 8px; max-width: 540px; }
.pref-choice { padding: 10px 12px; border: 1px solid var(--border-faint); border-radius: var(--r-md); cursor: pointer; transition: all var(--t-base); }
.pref-choice:hover { background: var(--background-lighter); }
.pref-choice.selected { border-color: var(--heat); background: var(--heat-12); }
.pref-choice .t { font-size: 12.5px; font-weight: 600; color: var(--accent-black); }
.pref-choice .d { font-size: 11px; color: var(--black-alpha-48); margin-top: 2px; font-family: var(--font-mono); letter-spacing: .02em; }
.pref-choice.selected .t { color: var(--heat); }
/* ─── 时长档 ─── */
.duration-row { display: flex; gap: 8px; }
.dur-chip { padding: 6px 14px; border: 1px solid var(--border-faint); border-radius: var(--r-md); font-size: 13px; cursor: pointer; font-family: var(--font-mono); font-variant-numeric: tabular-nums; transition: all var(--t-base); background: var(--surface); }
.dur-chip:hover { background: var(--background-lighter); }
.dur-chip.selected { border-color: var(--heat); background: var(--heat-12); color: var(--heat); font-weight: 600; }
/* ─── 设备列表 ─── */
.device-row { display: flex; align-items: center; gap: 12px; padding: 12px 0; border-bottom: 1px solid var(--border-faint); }
.device-row:last-child { border-bottom: 0; }
.device-row .ic { width: 36px; height: 36px; border-radius: var(--r-md); background: var(--background-lighter); display: grid; place-items: center; color: var(--black-alpha-56); }
.device-row .ic svg { width: 18px; height: 18px; }
.device-row .nm { font-size: 13px; font-weight: 500; }
.device-row .meta { font-size: 11.5px; color: var(--black-alpha-48); font-family: var(--font-mono); margin-top: 2px; letter-spacing: .02em; }
.device-row .tag-cur { font-family: var(--font-mono); font-size: 10.5px; padding: 1px 6px; background: var(--accent-forest); color: var(--accent-white); border-radius: var(--r-sm); margin-left: 8px; letter-spacing: .04em; font-weight: 600; }
.device-row .spacer { margin-left: auto; }
</style>
</head>
<body>
<div id="page">
<div class="page-head">
<div>
<h1>设置</h1>
<div class="sub"><span class="mono">// 个人信息 · 偏好 · 通知 · 安全</span></div>
</div>
<div class="actions">
<button class="btn" id="save-cancel" disabled style="opacity:.5; cursor:not-allowed;">取消</button>
<button class="btn btn-primary" id="save-btn" disabled style="opacity:.5; cursor:not-allowed;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l5 5L20 6"/></svg>
保存所有变更
</button>
</div>
</div>
<div class="settings-grid">
<!-- 左侧 nav -->
<aside class="settings-nav">
<div class="nav-h">个人</div>
<a href="#sec-profile" class="active" data-jump="sec-profile">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="4"/><path d="M4 21c0-4.4 3.6-8 8-8s8 3.6 8 8"/></svg>
个人信息
</a>
<a href="#sec-security" data-jump="sec-security">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
安全
</a>
<a href="#sec-notify" data-jump="sec-notify">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9M10 21a2 2 0 0 0 4 0"/></svg>
通知
</a>
<div class="nav-h" style="margin-top: 16px;">偏好</div>
<a href="#sec-pref" data-jump="sec-pref">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="m3 7 9-4 9 4-9 4-9-4z"/><path d="m3 12 9 4 9-4"/></svg>
创作默认
</a>
<a href="#sec-display" data-jump="sec-display">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M2 10h20"/></svg>
显示
</a>
<div class="nav-h" style="margin-top: 16px;">账号</div>
<a href="#sec-danger" data-jump="sec-danger" style="color: var(--accent-crimson);">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><path d="M12 9v4M12 17h.01"/></svg>
危险操作
</a>
</aside>
<!-- 右侧内容 -->
<main>
<!-- ─── 个人信息 ─── -->
<section class="pane" id="sec-profile">
<h3>个人信息</h3>
<div class="pane-desc">// 头像、姓名、联系方式 · 邮箱用于接收通知</div>
<div class="form-row">
<div class="lbl">头像</div>
<div class="val">
<div class="avatar-edit">
<div class="av-big"></div>
<div class="av-actions">
<button class="btn btn-sm" onclick="Shell.toast('上传头像', '占位 · 选择本地图片')">上传新头像</button>
<button class="btn btn-ghost btn-sm" onclick="Shell.toast('恢复默认头像', '已重置')">恢复默认</button>
</div>
</div>
</div>
</div>
<div class="form-row">
<div class="lbl">显示名称<span class="req">*</span></div>
<div class="val"><input class="input" id="prof-name" value="小李" data-track></div>
</div>
<div class="form-row">
<div class="lbl">登录邮箱</div>
<div class="val">
<input class="input" id="prof-email" value="li@shop.com" data-track>
<button class="btn btn-ghost btn-sm" onclick="Shell.toast('已发送验证邮件', 'li@shop.com')">验证</button>
</div>
</div>
<div class="form-row">
<div class="lbl">手机号</div>
<div class="val">
<input class="input" id="prof-phone" value="138****8000" data-track>
<button class="btn btn-ghost btn-sm" onclick="Shell.toast('已发送短信验证码', '+86 138****8000')">更换</button>
</div>
</div>
<div class="form-row">
<div class="lbl">所属团队<div class="lbl-sub">// 一人一团队</div></div>
<div class="val">
<span class="static">小李的店</span>
<span class="role-tag"><span class="dot"></span>超管 · 创建者</span>
<a href="team.html" style="font-size: 12px; color: var(--heat); text-decoration: none; margin-left: auto;">管理团队 →</a>
</div>
</div>
<div class="form-row">
<div class="lbl">用户 ID<div class="lbl-sub">// 不可改</div></div>
<div class="val"><span class="static mono">USR-2026-A8F2-001</span></div>
</div>
</section>
<!-- ─── 安全 ─── -->
<section class="pane" id="sec-security">
<h3>安全</h3>
<div class="pane-desc">// 登录密码、双因素、在用设备</div>
<div class="form-row">
<div class="lbl">登录密码</div>
<div class="val">
<span class="static mono">●●●●●●●●●●</span>
<span class="muted-2 mono" style="font-size: 11px; margin-left: auto;">上次修改 2026-04-12</span>
<button class="btn btn-sm" onclick="Shell.toast('修改密码', '/settings/password')" style="margin-left: 10px;">修改</button>
</div>
</div>
<div class="form-row">
<div class="lbl">两步验证<div class="lbl-sub">// 推荐开启</div></div>
<div class="val">
<label class="switch"><input type="checkbox" id="opt-2fa"><span class="slider"></span></label>
<span class="static mono" style="font-size: 11.5px; color: var(--black-alpha-48);">短信 + Authenticator</span>
</div>
</div>
<h3 style="margin-top: 24px;">在用设备</h3>
<div class="pane-desc">// 不在此列表上的设备登录会触发短信告警</div>
<div>
<div class="device-row">
<div class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="14" rx="2"/><path d="M2 20h20"/></svg></div>
<div>
<div class="nm">MacBook Pro · Chrome<span class="tag-cur">CURRENT</span></div>
<div class="meta">// 上海 · 2026-05-21 14:08 · IP 116.xxx.xxx.42</div>
</div>
<div class="spacer"></div>
<span class="muted-2 mono" style="font-size: 11px;">当前会话</span>
</div>
<div class="device-row">
<div class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="2" width="12" height="20" rx="2"/><path d="M11 18h2"/></svg></div>
<div>
<div class="nm">iPhone 15 · Safari</div>
<div class="meta">// 上海 · 2026-05-20 21:43</div>
</div>
<div class="spacer"></div>
<button class="btn btn-ghost btn-sm" onclick="Shell.toast('已下线', 'iPhone 15')">下线</button>
</div>
<div class="device-row">
<div class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="14" rx="2"/><path d="M2 20h20"/></svg></div>
<div>
<div class="nm">Windows · Edge</div>
<div class="meta">// 杭州 · 2026-05-18 09:12</div>
</div>
<div class="spacer"></div>
<button class="btn btn-ghost btn-sm" onclick="Shell.toast('已下线', 'Windows Edge')">下线</button>
</div>
</div>
<div style="margin-top: 14px;">
<button class="btn" onclick="if(confirm('下线所有其他设备?')) Shell.toast('已下线其他设备', '2 个')">下线所有其他设备</button>
</div>
</section>
<!-- ─── 通知 ─── -->
<section class="pane" id="sec-notify">
<h3>通知</h3>
<div class="pane-desc">// 邮件、短信、站内提示开关</div>
<div class="form-row">
<div class="lbl">项目完成通知<div class="lbl-sub">// 视频导出后</div></div>
<div class="val">
<label class="switch"><input type="checkbox" id="n-export" checked><span class="slider"></span></label>
<span class="static mono" style="font-size: 11.5px; color: var(--black-alpha-48);">站内 · 邮件 · 短信</span>
</div>
</div>
<div class="form-row">
<div class="lbl">任务失败告警</div>
<div class="val">
<label class="switch"><input type="checkbox" id="n-fail" checked><span class="slider"></span></label>
<span class="static mono" style="font-size: 11.5px; color: var(--black-alpha-48);">站内 · 邮件</span>
</div>
</div>
<div class="form-row">
<div class="lbl">额度不足提醒<div class="lbl-sub">// 团队或个人剩余 < 20%</div></div>
<div class="val">
<label class="switch"><input type="checkbox" id="n-quota" checked><span class="slider"></span></label>
<span class="static mono" style="font-size: 11.5px; color: var(--black-alpha-48);">站内 · 短信</span>
</div>
</div>
<div class="form-row">
<div class="lbl">异地登录告警</div>
<div class="val">
<label class="switch"><input type="checkbox" id="n-login" checked><span class="slider"></span></label>
<span class="static mono" style="font-size: 11.5px; color: var(--black-alpha-48);">短信</span>
</div>
</div>
<div class="form-row">
<div class="lbl">营销 / 产品更新</div>
<div class="val">
<label class="switch"><input type="checkbox" id="n-marketing"><span class="slider"></span></label>
<span class="static mono" style="font-size: 11.5px; color: var(--black-alpha-48);">邮件</span>
</div>
</div>
</section>
<!-- ─── 创作默认 ─── -->
<section class="pane" id="sec-pref">
<h3>创作默认</h3>
<div class="pane-desc">// 新建项目时的预填值,可在向导中改</div>
<div class="form-row" style="grid-template-columns: 160px 1fr; align-items: flex-start;">
<div class="lbl" style="padding-top: 4px;">默认模板</div>
<div class="val" style="display: block;">
<div class="pref-choices" id="pref-template">
<div class="pref-choice selected" data-v="pain"><div class="t">痛点种草</div><div class="d">// 30s 默认档</div></div>
<div class="pref-choice" data-v="unbox"><div class="t">开箱测评</div><div class="d">// 45s 默认档</div></div>
<div class="pref-choice" data-v="compare"><div class="t">对比展示</div><div class="d">// 45s 默认档</div></div>
<div class="pref-choice" data-v="howto"><div class="t">教程演示</div><div class="d">// 60s 默认档</div></div>
<div class="pref-choice" data-v="drama"><div class="t">剧情带货</div><div class="d">// 60s 默认档</div></div>
</div>
</div>
</div>
<div class="form-row">
<div class="lbl">默认时长档</div>
<div class="val">
<div class="duration-row" id="pref-duration">
<span class="dur-chip" data-v="30">30s</span>
<span class="dur-chip" data-v="45">45s</span>
<span class="dur-chip selected" data-v="60">60s</span>
</div>
<span class="static mono" style="font-size: 11.5px; color: var(--black-alpha-48); margin-left: 10px;">// 60s = 4 段 × 15s</span>
</div>
</div>
<div class="form-row" style="grid-template-columns: 160px 1fr; align-items: flex-start;">
<div class="lbl" style="padding-top: 4px;">默认字幕样式</div>
<div class="val" style="display: block;">
<div class="pref-choices" id="pref-subtitle">
<div class="pref-choice selected" data-v="big-variety"><div class="t">大字综艺</div><div class="d">// 抖音热门</div></div>
<div class="pref-choice" data-v="clean-ec"><div class="t">简洁电商</div><div class="d">// 信息清晰</div></div>
<div class="pref-choice" data-v="premium"><div class="t">高级排版</div><div class="d">// 居中衬线</div></div>
<div class="pref-choice" data-v="bullet"><div class="t">弹幕轻量</div><div class="d">// 滚动出现</div></div>
<div class="pref-choice" data-v="emphasis"><div class="t">强调爆款</div><div class="d">// 高对比</div></div>
</div>
</div>
</div>
<div class="form-row">
<div class="lbl">默认 BGM 库</div>
<div class="val">
<select class="select" id="pref-bgm" data-track>
<option value="kapian">抖音 Top10 卡点曲库</option>
<option value="emotion">情绪向 · 治愈/悬念</option>
<option value="urban">都市电子 · 通勤场景</option>
<option value="none">无 BGM</option>
</select>
</div>
</div>
<div class="form-row">
<div class="lbl">默认转场</div>
<div class="val">
<select class="select" id="pref-transition" data-track>
<option>无转场</option>
<option selected>淡入淡出 · 0.3s</option>
<option>滑动 · 0.3s</option>
<option>缩放 · 0.3s</option>
</select>
</div>
</div>
<div class="form-row">
<div class="lbl">导出水印<div class="lbl-sub">// VIP 可关闭</div></div>
<div class="val">
<label class="switch"><input type="checkbox" id="opt-watermark" checked disabled><span class="slider"></span></label>
<span class="static mono" style="font-size: 11.5px; color: var(--black-alpha-48);">右下角 · 流·Studio</span>
<a href="account.html" style="font-size: 12px; color: var(--heat); text-decoration: none; margin-left: auto;">升级 VIP →</a>
</div>
</div>
</section>
<!-- ─── 显示 ─── -->
<section class="pane" id="sec-display">
<h3>显示</h3>
<div class="pane-desc">// 界面外观与语言</div>
<div class="form-row">
<div class="lbl">外观</div>
<div class="val">
<select class="select" id="pref-theme" data-track>
<option selected>跟随系统</option>
<option>浅色</option>
<option disabled>深色(V2)</option>
</select>
</div>
</div>
<div class="form-row">
<div class="lbl">语言</div>
<div class="val">
<select class="select" id="pref-lang" data-track>
<option selected>简体中文</option>
<option disabled>English(V2)</option>
</select>
</div>
</div>
<div class="form-row">
<div class="lbl">表格密度</div>
<div class="val">
<select class="select" id="pref-density" data-track>
<option>紧凑</option>
<option selected>标准</option>
<option>宽松</option>
</select>
</div>
</div>
</section>
<!-- ─── 危险操作 ─── -->
<section class="pane danger" id="sec-danger">
<h3>危险操作</h3>
<div class="pane-desc">// 这些操作不可撤销,请确认后再执行</div>
<div class="form-row">
<div class="lbl">导出我的数据<div class="lbl-sub">// 项目 + 资产元数据</div></div>
<div class="val">
<span class="static mono" style="font-size: 11.5px; color: var(--black-alpha-56);">// 准备时间约 24 小时,完成后邮件通知</span>
<button class="btn btn-sm" style="margin-left: auto;" onclick="Shell.toast('已申请导出', '约 24 小时后邮件发送')">申请导出</button>
</div>
</div>
<div class="form-row">
<div class="lbl">退出登录</div>
<div class="val">
<span class="static mono" style="font-size: 11.5px; color: var(--black-alpha-56);">// 仅退出当前设备,数据保留</span>
<button class="btn btn-sm" style="margin-left: auto; color: var(--accent-crimson); border-color: var(--accent-crimson);" onclick="if(confirm('确定退出登录?')) Shell.toast('已退出', '正在跳转登录页')">退出登录</button>
</div>
</div>
<div class="form-row">
<div class="lbl">注销账号</div>
<div class="val">
<span class="static mono" style="font-size: 11.5px; color: var(--black-alpha-56);">// 团队余额清零、所有项目作为孤儿归档</span>
<button class="btn btn-sm" style="margin-left: auto; color: var(--accent-crimson); border-color: var(--accent-crimson);" onclick="if(confirm('彻底注销当前账号? 此操作不可恢复')) Shell.toast('已提交注销申请', '24 小时内人工复核')">注销账号</button>
</div>
</div>
</section>
<div style="text-align: center; padding: 24px 0 8px; color: var(--black-alpha-32); font-family: var(--font-mono); font-size: 11px; letter-spacing: .04em;">
// 流·Studio · v2.1 · build 20260521
</div>
</main>
</div>
</div>
<script src="assets/shell.js"></script>
<script>
Shell.render({
active: 'settings',
crumbs: [{ label: '工作台', href: 'index.html' }, { label: '设置' }]
});
/* ─── 侧边 nav 高亮 + 滚动联动 ─── */
const sections = ['sec-profile', 'sec-security', 'sec-notify', 'sec-pref', 'sec-display', 'sec-danger'];
const navLinks = document.querySelectorAll('.settings-nav a[data-jump]');
navLinks.forEach(a => {
a.addEventListener('click', e => {
e.preventDefault();
const id = a.dataset.jump;
const el = document.getElementById(id);
if (el) {
const contentEl = document.getElementById('page-content') || document.querySelector('.content');
const offset = 16;
if (contentEl) {
contentEl.scrollTo({ top: el.offsetTop - contentEl.offsetTop - offset, behavior: 'smooth' });
} else {
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
});
});
const contentEl = document.getElementById('page-content') || document.querySelector('.content');
const observer = new IntersectionObserver(entries => {
entries.forEach(en => {
if (en.isIntersecting) {
const id = en.target.id;
navLinks.forEach(a => a.classList.toggle('active', a.dataset.jump === id));
}
});
}, { root: contentEl || null, rootMargin: '-20% 0px -60% 0px', threshold: 0 });
sections.forEach(id => {
const el = document.getElementById(id);
if (el) observer.observe(el);
});
/* ─── 偏好 chip 选择 ─── */
function bindChoice(containerId, label) {
const ct = document.getElementById(containerId);
if (!ct) return;
ct.querySelectorAll('.pref-choice').forEach(c => {
c.addEventListener('click', () => {
ct.querySelectorAll('.pref-choice').forEach(x => x.classList.remove('selected'));
c.classList.add('selected');
markDirty();
});
});
}
bindChoice('pref-template');
bindChoice('pref-subtitle');
document.querySelectorAll('#pref-duration .dur-chip').forEach(c => {
c.addEventListener('click', () => {
document.querySelectorAll('#pref-duration .dur-chip').forEach(x => x.classList.remove('selected'));
c.classList.add('selected');
markDirty();
});
});
/* ─── dirty state ─── */
let dirty = false;
function markDirty() {
if (dirty) return;
dirty = true;
const saveBtn = document.getElementById('save-btn');
const cancelBtn = document.getElementById('save-cancel');
[saveBtn, cancelBtn].forEach(b => {
b.disabled = false;
b.style.opacity = '';
b.style.cursor = '';
});
}
function clearDirty() {
dirty = false;
const saveBtn = document.getElementById('save-btn');
const cancelBtn = document.getElementById('save-cancel');
[saveBtn, cancelBtn].forEach(b => {
b.disabled = true;
b.style.opacity = '.5';
b.style.cursor = 'not-allowed';
});
}
document.querySelectorAll('[data-track], input[type="checkbox"], select').forEach(el => {
el.addEventListener('change', markDirty);
if (el.tagName === 'INPUT' && el.type !== 'checkbox') el.addEventListener('input', markDirty);
});
document.getElementById('save-btn').addEventListener('click', () => {
if (!dirty) return;
Shell.toast('设置已保存', '所有变更已生效');
clearDirty();
});
document.getElementById('save-cancel').addEventListener('click', () => {
if (!dirty) return;
if (confirm('放弃未保存的变更?')) location.reload();
});
</script>
</body>
</html>

1402
v2/studio-v2.html Normal file

File diff suppressed because it is too large Load Diff

1634
v2/studio.html Normal file

File diff suppressed because it is too large Load Diff

518
v2/team.html Normal file
View File

@ -0,0 +1,518 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>团队 · 流·Studio</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css">
<style>
/* ─── 团队信息卡(深色 banner · 上标题行 + 下统计行)─── */
.team-banner {
background: var(--accent-black);
color: var(--accent-white);
padding: 22px 28px 24px;
margin-bottom: 24px;
position: relative;
border: 1px solid var(--accent-black);
border-radius: var(--r-md);
}
/* 4 个装订线小十字(2 个 pseudo + 2 个 span)*/
.team-banner::before, .team-banner::after,
.team-banner > .corner-tr, .team-banner > .corner-bl {
content: ''; position: absolute; width: 14px; height: 14px;
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 21' fill='%23e8e8e8'%3E%3Cpath d='M10.5 4C10.5 7.31371 7.81371 10 4.5 10H0.5V11H4.5C7.81371 11 10.5 13.6863 10.5 17V21H11.5V17C11.5 13.6863 14.1863 11 17.5 11H21.5V10H17.5C14.1863 10 11.5 7.31371 11.5 4V0H10.5V4Z'/%3E%3C/svg%3E") no-repeat center;
background-size: contain; pointer-events: none;
}
.team-banner::before { top: -7px; left: -7px; }
.team-banner::after { bottom: -7px; right: -7px; }
.team-banner > .corner-tr { top: -7px; right: -7px; }
.team-banner > .corner-bl { bottom: -7px; left: -7px; }
/* 第 1 行:标题 + 主操作 */
.banner-head { display: flex; align-items: flex-start; gap: 20px; }
.banner-id { flex: 1; min-width: 0; }
.banner-id .lbl { font-family: var(--font-mono); font-size: 10.5px; color: rgba(255,255,255,.55); letter-spacing: .06em; text-transform: uppercase; }
.banner-id .nm { font-size: 22px; font-weight: 700; letter-spacing: -.012em; margin-top: 4px; display: flex; align-items: baseline; gap: 10px; }
.banner-id .nm .tag { font-size: 10.5px; font-family: var(--font-mono); padding: 2px 8px; background: rgba(255,255,255,.12); border-radius: var(--r-pill); letter-spacing: .04em; font-weight: 500; }
.banner-id .meta { font-size: 12px; color: rgba(255,255,255,.5); margin-top: 6px; font-family: var(--font-mono); letter-spacing: .02em; }
.banner-actions { display: flex; gap: 8px; flex-shrink: 0; }
.banner-actions .btn { background: var(--accent-white); color: var(--accent-black); border-color: var(--accent-white); }
.banner-actions .btn:hover { background: var(--background-base); }
.banner-actions .btn-ghost { background: transparent; color: var(--accent-white); border: 1px solid rgba(255,255,255,.25); }
.banner-actions .btn-ghost:hover { background: rgba(255,255,255,.08); color: var(--accent-white); }
/* 分隔线 */
.banner-divider { height: 1px; background: rgba(255,255,255,.1); margin: 20px 0 18px; }
/* 第 2 行:4 列统计 */
.banner-stats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 24px; }
.banner-stats .stat { min-width: 0; }
.banner-stats .stat .lbl { font-family: var(--font-mono); font-size: 10.5px; color: rgba(255,255,255,.55); letter-spacing: .06em; text-transform: uppercase; }
.banner-stats .stat .v { font-size: 22px; font-weight: 700; font-variant-numeric: tabular-nums; letter-spacing: -.012em; margin-top: 6px; }
.banner-stats .stat .v.warn { color: #FFB870; }
.banner-stats .stat .sub { font-size: 11px; color: rgba(255,255,255,.5); margin-top: 4px; font-family: var(--font-mono); letter-spacing: .02em; }
/* ─── 主体两栏 ─── */
.team-grid { display: grid; grid-template-columns: minmax(0, 1fr) 320px; gap: 24px; align-items: start; }
.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: 14px; display: flex; align-items: center; gap: 8px; }
.pane h3 .ct { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); font-weight: 400; }
.pane h3 .spacer { margin-left: auto; }
/* ─── 成员表 ─── */
.members-table .av { width: 32px; height: 32px; border-radius: 50%; background: var(--background-lighter); display: inline-grid; place-items: center; font-weight: 600; font-size: 13px; color: var(--accent-black); border: 1px solid var(--border-faint); }
.members-table .who { display: flex; align-items: center; gap: 10px; }
.members-table .nm { font-weight: 500; font-size: 13.5px; line-height: 1.2; }
.members-table .em { font-size: 11.5px; color: var(--black-alpha-48); font-family: var(--font-mono); }
.members-table .role-pill { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: var(--r-pill); font-size: 11px; font-weight: 500; }
.members-table .role-pill .dot { width: 6px; height: 6px; border-radius: 50%; }
.members-table .role-super { background: var(--heat-12); color: var(--heat); }
.members-table .role-super .dot { background: var(--heat); }
.members-table .role-admin { background: rgba(30,64,175,.1); color: #1E40AF; }
.members-table .role-admin .dot { background: #1E40AF; }
.members-table .role-member { background: var(--background-lighter); color: var(--black-alpha-56); }
.members-table .role-member .dot { background: var(--black-alpha-56); }
.members-table .quota-cell { font-variant-numeric: tabular-nums; font-family: var(--font-mono); font-size: 12px; }
.members-table .quota-cell .lbl { color: var(--black-alpha-48); }
.members-table .quota-cell .v { color: var(--accent-black); font-weight: 600; }
.members-table .used-bar { width: 80px; height: 4px; background: var(--background-lighter); border-radius: 2px; overflow: hidden; margin-top: 4px; }
.members-table .used-bar > span { display: block; height: 100%; background: var(--heat); }
.members-table .used-bar > span.ok { background: var(--accent-forest); }
.members-table .used-bar > span.warn { background: #B45309; }
.members-table .acts { display: flex; gap: 4px; justify-content: flex-end; }
.members-table .icon-btn-sm { width: 28px; height: 28px; display: inline-grid; place-items: center; border: 1px solid var(--border-faint); border-radius: var(--r-sm); background: var(--surface); cursor: pointer; color: var(--black-alpha-56); transition: all var(--t-base); }
.members-table .icon-btn-sm:hover { color: var(--heat); border-color: var(--heat-20); }
.members-table .icon-btn-sm svg { width: 14px; height: 14px; }
.members-table .icon-btn-sm.danger:hover { color: var(--accent-crimson); border-color: var(--accent-crimson); }
.members-table tr.pending td { opacity: .65; }
.members-table tr.pending .nm::after { content: '· 待激活'; font-size: 11px; color: var(--black-alpha-48); margin-left: 6px; font-weight: 400; font-family: var(--font-mono); }
/* ─── 角色权限矩阵 ─── */
.perm-table { width: 100%; border-collapse: collapse; font-size: 12.5px; }
.perm-table th, .perm-table td { padding: 8px 4px; border-bottom: 1px solid var(--border-faint); }
.perm-table th { font-family: var(--font-mono); font-size: 10.5px; font-weight: 500; color: var(--black-alpha-48); letter-spacing: .04em; text-transform: uppercase; text-align: left; }
.perm-table th:not(:first-child), .perm-table td:not(:first-child) { text-align: center; }
.perm-table tbody td:first-child { color: var(--accent-black); }
.perm-table .yes { color: var(--accent-forest); font-weight: 600; }
.perm-table .no { color: var(--black-alpha-32); }
/* ─── 额度检查规则 ─── */
.quota-rules { font-size: 12.5px; color: var(--black-alpha-56); line-height: 1.8; }
.quota-rules .step { display: flex; gap: 10px; padding: 6px 0; align-items: flex-start; }
.quota-rules .num { width: 18px; height: 18px; border-radius: 50%; background: var(--heat-12); color: var(--heat); font-family: var(--font-mono); font-size: 10.5px; font-weight: 600; display: grid; place-items: center; flex: 0 0 18px; margin-top: 1px; }
.quota-rules .v { color: var(--accent-black); font-weight: 500; }
.quota-rules .formula { font-family: var(--font-mono); font-size: 11px; color: var(--heat); background: var(--heat-12); padding: 1px 6px; }
/* ─── 邀请 modal ─── */
.invite-modal { width: min(480px, 92vw); }
.invite-modal .field { margin-bottom: 14px; }
.invite-modal label.field-label { display: block; font-size: 12px; color: var(--black-alpha-56); margin-bottom: 6px; font-family: var(--font-mono); letter-spacing: .02em; }
.invite-modal label.field-label .req { color: var(--accent-crimson); margin-left: 2px; }
.role-choices { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.role-choice { padding: 12px 14px; border: 1px solid var(--border-faint); border-radius: var(--r-md); cursor: pointer; transition: all var(--t-base); }
.role-choice:hover { background: var(--background-lighter); }
.role-choice.selected { border-color: var(--heat); background: var(--heat-12); }
.role-choice .title { font-size: 13px; font-weight: 600; color: var(--accent-black); }
.role-choice .desc { font-size: 11px; color: var(--black-alpha-56); margin-top: 2px; font-family: var(--font-mono); letter-spacing: .02em; }
.quota-input-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.quota-input-row .input { font-variant-numeric: tabular-nums; }
</style>
</head>
<body>
<div id="page">
<div class="page-head">
<div>
<h1>团队管理</h1>
<div class="sub"><span class="mono">// 成员 · 角色 · 额度 · 共享资产库</span></div>
</div>
<div class="actions">
<button class="btn" onclick="Shell.toast('团队设置', '/team/settings')">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8 2 2 0 0 1-2.8 2.8 1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5 2 2 0 0 1-4 0 1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3 2 2 0 0 1-2.8-2.8 1.7 1.7 0 0 0 .3-1.8"/></svg>
团队设置
</button>
<button class="btn btn-primary" id="open-invite">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M19 8v6M22 11h-6"/></svg>
邀请成员
</button>
</div>
</div>
<!-- 团队信息 banner -->
<div class="team-banner">
<span class="corner-tr" aria-hidden></span><span class="corner-bl" aria-hidden></span>
<div class="banner-head">
<div class="banner-id">
<div class="lbl">[ TEAM ]</div>
<div class="nm">小李的店 <span class="tag">企业</span></div>
<div class="meta">// 团队 ID: T-2026-A8F2 · 创建于 2026-04-12 · 5 名成员</div>
</div>
<div class="banner-actions">
<button class="btn btn-sm" onclick="location.href='account.html'">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 7v10M9 10h4a1.5 1.5 0 0 1 0 3h-3a1.5 1.5 0 0 0 0 3h4"/></svg>
充值
</button>
<button class="btn btn-ghost btn-sm" onclick="Shell.toast('编辑月限额', '/team/limit')">设置月限额</button>
</div>
</div>
<div class="banner-divider"></div>
<div class="banner-stats">
<div class="stat">
<div class="lbl">[ 充值余额 ]</div>
<div class="v">¥327.40</div>
<div class="sub">// 团队总池</div>
</div>
<div class="stat">
<div class="lbl">[ 月限额 ]</div>
<div class="v">¥3,000</div>
<div class="sub">// 自然月重置</div>
</div>
<div class="stat">
<div class="lbl">[ 当月已用 ]</div>
<div class="v">¥162.60</div>
<div class="sub">// 占月限 5.4%</div>
</div>
<div class="stat">
<div class="lbl">[ 当月剩余 ]</div>
<div class="v warn">¥2,837.40</div>
<div class="sub">// 还可生成约 280 个项目</div>
</div>
</div>
</div>
<div class="team-grid">
<!-- 左:成员表 -->
<div>
<div class="pane" style="padding: 0;">
<div style="display: flex; align-items: center; padding: 16px 20px; border-bottom: 1px solid var(--border-faint);">
<h3 style="margin: 0;">成员列表 <span class="ct">// 5 人 · 1 超管 / 1 团管 / 3 成员</span></h3>
<span class="spacer"></span>
<input class="input" id="member-search" placeholder="搜索姓名 / 手机号" style="height: 32px; font-size: 12px; width: 220px;">
</div>
<table class="t members-table" style="border: 0; border-radius: 0;">
<thead>
<tr>
<th>成员</th>
<th>角色</th>
<th>每日额度</th>
<th>月度额度</th>
<th style="width: 140px;">当月已用</th>
<th style="text-align: right; width: 88px;">操作</th>
</tr>
</thead>
<tbody id="members-tbody">
<!-- JS 注入 -->
</tbody>
</table>
</div>
</div>
<!-- 右:权限矩阵 + 额度规则 -->
<div>
<div class="pane">
<h3>角色权限</h3>
<div style="font-size: 12px; color: var(--black-alpha-48); margin-top: -10px; margin-bottom: 12px; font-family: var(--font-mono); letter-spacing: .02em;">// PRD §10.2 权限矩阵节选</div>
<table class="perm-table">
<thead>
<tr><th>能力</th><th>超管</th><th>团管</th><th>成员</th></tr>
</thead>
<tbody>
<tr><td>邀请 / 移除成员</td><td class="yes"></td><td class="yes"></td><td class="no"></td></tr>
<tr><td>设置成员额度</td><td class="yes"></td><td class="yes"></td><td class="no"></td></tr>
<tr><td>团队充值</td><td class="yes"></td><td class="no"></td><td class="no"></td></tr>
<tr><td>设置月限额</td><td class="yes"></td><td class="no"></td><td class="no"></td></tr>
<tr><td>编辑别人项目</td><td class="yes"></td><td class="yes"></td><td class="no"></td></tr>
<tr><td>团队共享资产库管理</td><td class="yes"></td><td class="yes"></td><td class="no">仅自传</td></tr>
<tr><td>查看团队消费明细</td><td class="yes"></td><td class="yes"></td><td class="no">仅自己</td></tr>
<tr style="border-bottom: 0;"><td>创建项目 / 用 AI 流程</td><td class="yes"></td><td class="yes"></td><td class="yes"></td></tr>
</tbody>
</table>
</div>
<div class="pane">
<h3>额度预检规则</h3>
<div style="font-size: 12px; color: var(--black-alpha-48); margin-top: -10px; margin-bottom: 12px; font-family: var(--font-mono); letter-spacing: .02em;">// 任一不通过即拦截</div>
<div class="quota-rules">
<div class="step"><span class="num">1</span><span><span class="v">个人日剩余</span> ≥ 任务预估 × <span class="formula">1.2</span></span></div>
<div class="step"><span class="num">2</span><span><span class="v">个人月剩余</span> ≥ 同上</span></div>
<div class="step"><span class="num">3</span><span><span class="v">团队月剩余</span> ≥ 同上</span></div>
<div class="step"><span class="num">4</span><span><span class="v">团队总余额</span> ≥ 同上</span></div>
</div>
</div>
<div class="pane" style="background: var(--heat-12); border-color: var(--heat-20);">
<h3 style="color: var(--heat);">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 8v4M12 16h.01"/></svg>
失败不扣费
</h3>
<div style="font-size: 12.5px; color: var(--black-alpha-56); line-height: 1.7;">
所有生成任务<strong style="color: var(--accent-black);">仅在用户 <span class="mono" style="background: var(--surface); padding: 1px 5px; font-family: var(--font-mono); color: var(--heat); font-size: 11.5px;">[ 通过 ]</span> 时才扣费</strong>。失败 / 超时 / 重跑(旧版本作废)一律不扣。
</div>
</div>
</div>
</div>
<!-- 邀请成员 modal -->
<div class="modal-bg" id="invite-bg" onclick="if(event.target===this)Shell.closeModal('invite-bg')">
<div class="modal invite-modal">
<span class="corner-tr" aria-hidden></span><span class="corner-bl" aria-hidden></span>
<div class="modal-h">
<div class="ic-m">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M19 8v6M22 11h-6"/></svg>
</div>
<div class="ti">邀请成员<span>// 短信邀请 · 接受后自动加入团队</span></div>
</div>
<div class="modal-b">
<div class="field">
<label class="field-label">手机号 <span class="req">*</span></label>
<input class="input" id="inv-phone" placeholder="例: 138 0013 8000">
</div>
<div class="field">
<label class="field-label">备注姓名(可选)</label>
<input class="input" id="inv-name" placeholder="例: 张某">
</div>
<div class="field">
<label class="field-label">分配角色 <span class="req">*</span></label>
<div class="role-choices">
<div class="role-choice selected" data-role="member">
<div class="title">成员</div>
<div class="desc">// 创建项目 + 用资产</div>
</div>
<div class="role-choice" data-role="admin">
<div class="title">团管</div>
<div class="desc">// 管成员 + 改额度</div>
</div>
</div>
</div>
<div class="field" style="margin-bottom: 0;">
<label class="field-label">默认额度</label>
<div class="quota-input-row">
<input class="input" id="inv-daily" placeholder="每日 (¥)" value="100">
<input class="input" id="inv-monthly" placeholder="每月 (¥)" value="2000">
</div>
</div>
</div>
<div class="modal-f">
<button class="btn" type="button" onclick="Shell.closeModal('invite-bg')">取消</button>
<button class="btn btn-primary" type="button" id="inv-send">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="m22 2-7 20-4-9-9-4z"/><path d="m22 2-11 11"/></svg>
发送邀请
</button>
</div>
</div>
</div>
<!-- 编辑成员 modal -->
<div class="modal-bg" id="edit-member-bg" onclick="if(event.target===this)Shell.closeModal('edit-member-bg')">
<div class="modal invite-modal">
<span class="corner-tr" aria-hidden></span><span class="corner-bl" aria-hidden></span>
<div class="modal-h">
<div class="ic-m">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4z"/></svg>
</div>
<div class="ti" id="edit-title">编辑成员<span id="edit-sub">// 调整角色 / 额度</span></div>
</div>
<div class="modal-b">
<div class="field">
<label class="field-label">角色</label>
<div class="role-choices">
<div class="role-choice" data-edit-role="member">
<div class="title">成员</div>
<div class="desc">// 创建项目 + 用资产</div>
</div>
<div class="role-choice" data-edit-role="admin">
<div class="title">团管</div>
<div class="desc">// 管成员 + 改额度</div>
</div>
</div>
</div>
<div class="field" style="margin-bottom: 0;">
<label class="field-label">额度</label>
<div class="quota-input-row">
<input class="input" id="edit-daily" placeholder="每日 (¥)">
<input class="input" id="edit-monthly" placeholder="每月 (¥)">
</div>
</div>
</div>
<div class="modal-f">
<button class="btn" type="button" style="margin-right: auto; color: var(--accent-crimson); border-color: var(--accent-crimson);" id="edit-remove">移出团队</button>
<button class="btn" type="button" onclick="Shell.closeModal('edit-member-bg')">取消</button>
<button class="btn btn-primary" type="button" id="edit-save">保存</button>
</div>
</div>
</div>
</div>
<script src="assets/shell.js"></script>
<script>
Shell.render({
active: 'team',
crumbs: [{ label: '工作台', href: 'index.html' }, { label: '团队' }]
});
/* ─── 团队成员 mock + 渲染 ─── */
const ROLE_META = {
super: { cls: 'role-super', label: '超管' },
admin: { cls: 'role-admin', label: '团管' },
member: { cls: 'role-member', label: '成员' },
};
const MEMBERS = [
{ id: 'u1', av: '李', name: '小李', email: 'li@shop.com', role: 'super', daily: 500, monthly: 10000, used: 162.60, pending: false, creator: true },
{ id: 'u2', av: '张', name: '张运营', email: 'zhang@shop.com', role: 'admin', daily: 300, monthly: 6000, used: 98.40, pending: false },
{ id: 'u3', av: '王', name: '王小姐', email: 'wang@shop.com', role: 'member', daily: 100, monthly: 2000, used: 45.20, pending: false },
{ id: 'u4', av: '陈', name: '陈策划', email: 'chen@shop.com', role: 'member', daily: 100, monthly: 2000, used: 12.80, pending: false },
{ id: 'u5', av: '林', name: '林新人', email: '186****1102', role: 'member', daily: 100, monthly: 2000, used: 0, pending: true },
];
function fmtMoney(n) { return '¥' + n.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ','); }
function usedClass(pct) { if (pct < 0.5) return 'ok'; if (pct < 0.85) return ''; return 'warn'; }
function renderMembers(filter = '') {
const tb = document.getElementById('members-tbody');
const list = MEMBERS.filter(m => {
if (!filter) return true;
const q = filter.toLowerCase();
return m.name.toLowerCase().includes(q) || m.email.toLowerCase().includes(q);
});
tb.innerHTML = list.map(m => {
const r = ROLE_META[m.role];
const pct = m.monthly > 0 ? m.used / m.monthly : 0;
return `
<tr data-id="${m.id}"${m.pending ? ' class="pending"' : ''}>
<td>
<div class="who">
<div class="av">${m.av}</div>
<div>
<div class="nm">${m.name}${m.creator ? ' <span style="font-family:var(--font-mono);font-size:10px;color:var(--black-alpha-48);">· 创建者</span>' : ''}</div>
<div class="em">${m.email}</div>
</div>
</div>
</td>
<td><span class="role-pill ${r.cls}"><span class="dot"></span>${r.label}</span></td>
<td><span class="quota-cell"><span class="v">${fmtMoney(m.daily)}</span></span></td>
<td><span class="quota-cell"><span class="v">${fmtMoney(m.monthly)}</span></span></td>
<td>
<div class="quota-cell"><span class="v">${fmtMoney(m.used)}</span> <span class="lbl">/ ${(pct * 100).toFixed(0)}%</span></div>
<div class="used-bar"><span class="${usedClass(pct)}" style="width: ${Math.min(100, pct * 100).toFixed(1)}%"></span></div>
</td>
<td>
<div class="acts">
${m.creator ? '<span style="font-family:var(--font-mono);font-size:10.5px;color:var(--black-alpha-32);align-self:center;">不可编辑</span>' : `
<button class="icon-btn-sm" data-act="edit" data-id="${m.id}" title="编辑">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4z"/></svg>
</button>
<button class="icon-btn-sm danger" data-act="remove" data-id="${m.id}" title="移出">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
</button>
`}
</div>
</td>
</tr>
`;
}).join('');
tb.querySelectorAll('[data-act="edit"]').forEach(btn => {
btn.addEventListener('click', e => {
e.stopPropagation();
openEdit(btn.dataset.id);
});
});
tb.querySelectorAll('[data-act="remove"]').forEach(btn => {
btn.addEventListener('click', e => {
e.stopPropagation();
const m = MEMBERS.find(x => x.id === btn.dataset.id);
if (!m) return;
if (confirm('确定将「' + m.name + '」移出团队?')) {
const i = MEMBERS.findIndex(x => x.id === m.id);
MEMBERS.splice(i, 1);
Shell.toast('已移除成员', m.name);
renderMembers(document.getElementById('member-search').value);
}
});
});
}
renderMembers();
document.getElementById('member-search').addEventListener('input', e => {
renderMembers(e.target.value);
});
/* ─── 邀请 modal ─── */
document.getElementById('open-invite').addEventListener('click', () => {
Shell.openModal('invite-bg');
});
document.querySelectorAll('#invite-bg .role-choice').forEach(c => {
c.addEventListener('click', () => {
document.querySelectorAll('#invite-bg .role-choice').forEach(x => x.classList.remove('selected'));
c.classList.add('selected');
});
});
document.getElementById('inv-send').addEventListener('click', () => {
const phone = document.getElementById('inv-phone').value.trim();
if (!phone) { Shell.toast('请填手机号', '邀请失败'); return; }
const role = document.querySelector('#invite-bg .role-choice.selected')?.dataset.role || 'member';
const name = document.getElementById('inv-name').value.trim() || '待激活成员';
const daily = Number(document.getElementById('inv-daily').value) || 100;
const monthly = Number(document.getElementById('inv-monthly').value) || 2000;
MEMBERS.push({
id: 'u' + Date.now(),
av: name[0] || '新',
name, email: phone, role,
daily, monthly, used: 0, pending: true,
});
renderMembers();
Shell.closeModal('invite-bg');
Shell.toast('邀请已发送', phone);
document.getElementById('inv-phone').value = '';
document.getElementById('inv-name').value = '';
});
/* ─── 编辑 modal ─── */
let editingId = null;
function openEdit(id) {
const m = MEMBERS.find(x => x.id === id);
if (!m) return;
editingId = id;
document.getElementById('edit-title').innerHTML = '编辑「' + m.name + '」<span id="edit-sub">// ' + m.email + '</span>';
document.querySelectorAll('#edit-member-bg .role-choice').forEach(c => {
c.classList.toggle('selected', c.dataset.editRole === (m.role === 'super' ? 'admin' : m.role));
});
document.getElementById('edit-daily').value = m.daily;
document.getElementById('edit-monthly').value = m.monthly;
Shell.openModal('edit-member-bg');
}
document.querySelectorAll('#edit-member-bg .role-choice').forEach(c => {
c.addEventListener('click', () => {
document.querySelectorAll('#edit-member-bg .role-choice').forEach(x => x.classList.remove('selected'));
c.classList.add('selected');
});
});
document.getElementById('edit-save').addEventListener('click', () => {
const m = MEMBERS.find(x => x.id === editingId);
if (!m) return;
m.role = document.querySelector('#edit-member-bg .role-choice.selected')?.dataset.editRole || m.role;
m.daily = Number(document.getElementById('edit-daily').value) || m.daily;
m.monthly = Number(document.getElementById('edit-monthly').value) || m.monthly;
Shell.closeModal('edit-member-bg');
Shell.toast('已保存', m.name);
renderMembers(document.getElementById('member-search').value);
});
document.getElementById('edit-remove').addEventListener('click', () => {
const m = MEMBERS.find(x => x.id === editingId);
if (!m) return;
if (confirm('确定将「' + m.name + '」移出团队?')) {
const i = MEMBERS.findIndex(x => x.id === m.id);
MEMBERS.splice(i, 1);
Shell.closeModal('edit-member-bg');
Shell.toast('已移除', m.name);
renderMembers(document.getElementById('member-search').value);
}
});
</script>
</body>
</html>