init: 电商AI平台 v2.1 静态设计稿

- 10 个页面 (工作台/项目/商品/流水线/资产/账户/创建向导)
- V2.1 Restraint 设计规范 (冷灰底 + #FA5D19 + 8px 圆角)
- 完整 design-system.html 组件库参考
- SVG line icon · stroke 1.5 全合规

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
iye 2026-05-15 17:55:11 +08:00
commit cae935588b
15 changed files with 7655 additions and 0 deletions

28
.gitignore vendored Normal file
View File

@ -0,0 +1,28 @@
# 工程文件
node_modules/
.next/
.turbo/
dist/
build/
# OS / IDE
.DS_Store
Thumbs.db
.vscode/
.idea/
*.swp
# 日志
*.log
npm-debug.log*
pnpm-debug.log*
# 本地环境
.env
.env.local
.env.*.local
# 临时
*.tmp
*.bak
screenshots/

33
README.md Normal file
View File

@ -0,0 +1,33 @@
# AirShelf · UI 设计稿货架
@zyc / iye 的 UI 设计稿合集。每个子目录是一个独立项目,各自有自己的设计规范和静态稿。
## 货架内容
| 项目 | 风格 | 说明 |
| --- | --- | --- |
| [电商AI平台/](电商AI平台/) | Restraint V2.1 (Firecrawl-aligned) | AI 短视频带货生成平台 · 10 个页面 · 完整 5 阶段流水线 |
---
## 浏览方式
直接 clone + 用浏览器打开任意 `*.html`:
```bash
git clone https://gitea.airlabs.art/zyc/AirShelf.git
cd AirShelf/电商AI平台
# 浏览器直开 index.html · 或本地起 server
npx http-server . -p 8080
```
## 添加新项目
新项目作为根目录下的兄弟文件夹(中文命名 OK),保持各自独立:
```
AirShelf/
├── 电商AI平台/
├── <未来项目 B>/
└── <未来项目 C>/
```

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)(保留作为历史)

53
电商AI平台/README.md Normal file
View File

@ -0,0 +1,53 @@
# 电商AI平台 · 流·Studio
AI 短视频带货生成平台 · 静态设计稿(V2.1 Restraint)。
## 快速浏览
直接用浏览器打开任意 HTML,或本地起服务:
```bash
npx http-server . -p 8080
# 然后访问 http://localhost:8080/
```
入口页:[index.html](index.html)(工作台)
## 页面索引
| 页面 | 文件 | 说明 |
| --- | --- | --- |
| 工作台 | [index.html](index.html) | 进入后首屏 · 4 块 KPI + 最近项目 + 快捷入口 |
| 视频项目 | [projects.html](projects.html) | 项目列表 · 表格 + 状态 + 进度 |
| 新建项目 | [projects-new.html](projects-new.html) | 4 步向导 · 含实时预估面板 + 推荐气泡 |
| 商品库 | [products.html](products.html) | SKU 列表 |
| 创建商品 | [product-create.html](product-create.html) | 4 步向导 + 子流程(挑模特 → 生上身) |
| 流水线 | [pipeline.html](pipeline.html) | 5 阶段:脚本 → 资产 → 故事板 → 片段 → 拼接 |
| 资产库 | [library.html](library.html) | 人物 / 场景 / 视频片段管理 |
| 账户 | [account.html](account.html) | 余额 / 充值 / 消费明细 |
| 设计系统 | [design-system.html](design-system.html) | 完整 V2.1 token + 组件 + 状态 + Modal/Toast |
## 设计规范
详见 [DESIGN_SPEC_V2.md](DESIGN_SPEC_V2.md) · 当前 V2.1。
**核心视觉特征:**
- 配色:冷灰底 `#f9f9f9` + 单橙 accent `#FA5D19`(Firecrawl-aligned)
- 圆角:统一 8 px / pill 999 px / 微元素 4 px
- Icon:SVG line · stroke 1.5 · linecap round · `currentColor` 继承
- 签名元素:容器装订线(左右 1 px 边)+ 四角 22×21 px SVG 准星(圆弧内凹)
- 字体:Inter Tight + JetBrains Mono + Alibaba PuHuiTi
- 主 CTA:橙底 + 4 层橙阴影(全场唯一允许阴影的组件)
## 目录结构
```
电商AI平台/
├── README.md ← 本文档
├── DESIGN_SPEC_V2.md ← 设计规范 source of truth
├── design-system.html ← 可交互组件参考(token + 状态 demo)
├── assets/
│ ├── restraint.css ← V2.1 共享样式 · 无 V1 legacy alias
│ └── shell.js ← sidebar/topbar/Toast/Modal 渲染器
└── *.html ← 8 个页面
```

170
电商AI平台/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>

View File

@ -0,0 +1,885 @@
/* ============================================================
·Studio · Restraint V2.1 · Firecrawl-aligned · 纯净版
============================================================
严格遵循 DESIGN_SPEC_V2.md V2.1 · 不含任何 V1/V2 legacy alias
============================================================ */
/* ============ Web font · Alibaba PuHuiTi 3.0 ============ */
@font-face {
font-family: 'Alibaba PuHuiTi';
font-weight: 400;
font-style: normal;
font-display: swap;
src: local('Alibaba PuHuiTi 3.0'),
local('AlibabaPuHuiTi-3-55-Regular'),
local('Alibaba PuHuiTi 2.0'),
local('AlibabaPuHuiTi-2-55-Regular'),
url('https://chinese-fonts-cdn.deno.dev/packages/alibaba_puhuiti/dist/AlibabaPuHuiTi-3-55-Regular/AlibabaPuHuiTi-3-55-Regular.woff2') format('woff2');
}
@font-face {
font-family: 'Alibaba PuHuiTi';
font-weight: 500;
font-style: normal;
font-display: swap;
src: local('Alibaba PuHuiTi 3.0 Medium'),
local('AlibabaPuHuiTi-3-65-Medium'),
local('AlibabaPuHuiTi-2-65-Medium'),
url('https://chinese-fonts-cdn.deno.dev/packages/alibaba_puhuiti/dist/AlibabaPuHuiTi-3-65-Medium/AlibabaPuHuiTi-3-65-Medium.woff2') format('woff2');
}
@font-face {
font-family: 'Alibaba PuHuiTi';
font-weight: 600;
font-style: normal;
font-display: swap;
src: local('AlibabaPuHuiTi-3-75-SemiBold'),
local('AlibabaPuHuiTi-2-75-SemiBold'),
url('https://chinese-fonts-cdn.deno.dev/packages/alibaba_puhuiti/dist/AlibabaPuHuiTi-3-75-SemiBold/AlibabaPuHuiTi-3-75-SemiBold.woff2') format('woff2');
}
@font-face {
font-family: 'Alibaba PuHuiTi';
font-weight: 700;
font-style: normal;
font-display: swap;
src: local('Alibaba PuHuiTi 3.0 Bold'),
local('AlibabaPuHuiTi-3-85-Bold'),
local('AlibabaPuHuiTi-2-85-Bold'),
url('https://chinese-fonts-cdn.deno.dev/packages/alibaba_puhuiti/dist/AlibabaPuHuiTi-3-85-Bold/AlibabaPuHuiTi-3-85-Bold.woff2') format('woff2');
}
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
/* ===== Backgrounds (冷灰) ===== */
--background-base: #f9f9f9;
--background-lighter: #fbfbfb;
--surface: #ffffff;
--surface-raised: #ffffff;
/* ===== Borders (冷灰 3 档,语义优先) ===== */
--border-faint: #ededed;
--border-muted: #e8e8e8;
--border-loud: #e6e6e6;
/* ===== Accent multi-color (5 色信号) ===== */
--accent-black: #262626;
--accent-white: #ffffff;
--accent-amethyst: #9061ff;
--accent-bluetron: #2a6dfb;
--accent-crimson: #eb3424;
--accent-forest: #42c366;
--accent-honey: #ecb730;
/* status 配套底/边 */
--forest-bg: rgba(66, 195, 102, .08);
--forest-bd: rgba(66, 195, 102, .20);
--crimson-bg: rgba(235, 52, 36, .08);
--crimson-bd: rgba(235, 52, 36, .20);
--honey-bg: rgba(236, 183, 48, .08);
--honey-bd: rgba(236, 183, 48, .20);
/* ===== 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 档) ===== */
--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);
/* ===== Radius ===== */
--r-sm: 4px;
--r-md: 8px;
--r-pill: 999px;
/* ===== Font · 关键:--font-mono 必须含 PuHuiTi 中文 fallback ===== */
/* Inter / JetBrains Mono 都不含 CJK 字形
字体链按字符级 fallthrough,中文字符会找下一个候选 必须包含 PuHuiTi */
--font-sans: 'Inter', 'Alibaba PuHuiTi', 'PingFang SC', 'Microsoft YaHei', system-ui, sans-serif;
--font-inter: 'Inter', system-ui, sans-serif;
--font-mono: 'JetBrains Mono', 'Geist Mono', ui-monospace, 'Alibaba PuHuiTi', 'PingFang SC', 'Microsoft YaHei', monospace;
/* ===== Transition ===== */
--t-fast: 100ms ease;
--t-base: 200ms ease;
--t-slow: 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
/* ===== Shadows ===== */
--shadow-cta:
inset 0 -4px 8px rgba(250, 93, 25, .20),
0 1px 1px rgba(250, 93, 25, .12),
0 2px 4px rgba(250, 93, 25, .10),
0 .5px .5px rgba(250, 93, 25, .16);
--shadow-cta-hover:
inset 0 -4px 8px rgba(250, 93, 25, .20),
0 1px 1px rgba(250, 93, 25, .16),
0 4px 8px rgba(250, 93, 25, .20),
0 .5px .5px rgba(250, 93, 25, .16);
--shadow-cta-active:
inset 0 -4px 8px rgba(250, 93, 25, .28),
0 1px 2px rgba(250, 93, 25, .16);
--shadow-floating: 0 4px 20px rgba(21, 20, 15, .06);
}
::selection { background: var(--heat-20); color: var(--heat); }
html, body {
background: var(--background-base);
color: var(--accent-black);
font-family: var(--font-sans);
font-size: 14px;
line-height: 1.65;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}
a { color: inherit; text-decoration: none; }
button { font: inherit; cursor: pointer; border: 0; background: none; color: inherit; }
input, textarea, select { font: inherit; color: inherit; outline: none; }
img, svg, video { display: block; max-width: 100%; }
.num, .tnum { font-variant-numeric: tabular-nums; }
.mono { font-family: var(--font-mono); }
.muted { color: var(--black-alpha-56); }
.muted-2 { color: var(--black-alpha-48); }
.spacer { flex: 1; }
.hstack { display: flex; align-items: center; gap: 8px; }
.vstack { display: flex; flex-direction: column; gap: 8px; }
.divider { height: 1px; background: var(--border-faint); margin: 16px 0; }
/* ─── App shell ─── */
.app { display: grid; grid-template-columns: 248px 1fr; min-height: 100vh; }
/* ─── Sidebar ─── */
aside.sidebar {
padding: 22px 16px;
border-right: 1px solid var(--border-faint);
background: var(--background-base);
position: sticky;
top: 0;
height: 100vh;
overflow-y: auto;
}
.brand { display: flex; align-items: center; gap: 10px; padding: 6px 8px 16px; }
.flame { width: 22px; height: 22px; color: var(--heat); }
.flame svg { width: 100%; height: 100%; }
.brand .name { font-weight: 600; font-size: 18px; letter-spacing: -.012em; color: var(--accent-black); }
/* sidebar search · Ctrl K Inter Bold 平铺 */
.search-box {
display: flex; align-items: center; gap: 10px;
padding: 9px 12px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
color: var(--black-alpha-48);
margin-bottom: 18px;
cursor: text;
transition: border-color var(--t-base);
}
.search-box:hover { border-color: var(--black-alpha-24); }
.search-box:focus-within { border-color: var(--heat-40); box-shadow: inset 0 0 0 1px var(--heat-40); }
.search-box svg { width: 14px; height: 14px; flex-shrink: 0; color: var(--black-alpha-56); }
.search-box input {
flex: 1; min-width: 0;
border: 0; background: transparent;
font-size: 13.5px; color: var(--accent-black);
padding: 0;
}
.search-box input::placeholder { color: var(--black-alpha-48); }
.search-box .kbd {
margin-left: auto;
flex-shrink: 0;
white-space: nowrap;
font-family: var(--font-inter);
font-weight: 700;
font-size: 11px;
color: var(--black-alpha-48);
letter-spacing: .02em;
}
.nav-section {
font-size: 11px; color: var(--black-alpha-48);
padding: 16px 12px 8px;
letter-spacing: .04em;
font-weight: 500;
/* 中文标签用 sans 字体,不用 mono + uppercase */
}
nav { display: flex; flex-direction: column; gap: 2px; }
nav a {
display: flex; align-items: center; gap: 11px;
padding: 9px 12px;
color: var(--black-alpha-56);
font-size: 13.5px; font-weight: 500;
border-radius: var(--r-md);
cursor: pointer;
user-select: none;
transition: background var(--t-base), color var(--t-base);
}
nav a:hover { background: var(--black-alpha-4); color: var(--accent-black); }
nav a.active { background: var(--heat-12); color: var(--heat); }
nav a svg { width: 14px; height: 14px; opacity: .85; }
nav a.active svg { opacity: 1; }
nav a .pill-mini {
margin-left: auto;
font-family: var(--font-mono);
font-size: 9.5px; font-weight: 600;
padding: 2px 7px;
background: var(--surface);
color: var(--black-alpha-48);
border: 1px solid var(--border-faint);
border-radius: var(--r-pill);
letter-spacing: .04em;
}
nav a.disabled { color: var(--black-alpha-32); cursor: not-allowed; }
nav a.disabled:hover { background: transparent; color: var(--black-alpha-32); }
.aside-foot { margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--border-faint); }
.user {
display: flex; align-items: center; gap: 10px;
padding: 8px 10px;
border-radius: var(--r-md);
cursor: pointer;
transition: background var(--t-base);
}
.user:hover { background: var(--black-alpha-4); }
.user .av {
width: 26px; height: 26px;
border-radius: 6px;
background: var(--accent-black);
color: var(--accent-white);
display: flex; align-items: center; justify-content: center;
font-weight: 600; font-size: 11px;
}
.user .em { font-size: 13px; color: var(--accent-black); }
/* ─── Main + grid background ─── */
main { position: relative; overflow: hidden; background: var(--background-base); }
.grid-bg {
position: absolute; inset: 0; pointer-events: none;
background-image:
url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='240' height='240'><g stroke='%23c8c8c8' stroke-width='1' fill='none'><path d='M-5 0 L5 0 M0 -5 L0 5'/><path d='M235 0 L245 0 M240 -5 L240 5'/><path d='M-5 240 L5 240 M0 235 L0 245'/><path d='M235 240 L245 240 M240 235 L240 245'/></g></svg>"),
url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='60' height='60'><circle cx='60' cy='60' r='0.9' fill='%23d8d8d8'/></svg>"),
url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='240' height='240'><g stroke='%23ebebeb' stroke-width='1' fill='none' stroke-dasharray='1.5 4'><path d='M240 0 L240 240'/><path d='M0 240 L240 240'/></g></svg>");
background-size: 240px 240px, 60px 60px, 240px 240px;
mask-image: radial-gradient(ellipse 95% 80% at 50% 35%, #000 25%, transparent 95%);
-webkit-mask-image: radial-gradient(ellipse 95% 80% at 50% 35%, #000 25%, transparent 95%);
}
.scatter {
position: absolute;
font-family: var(--font-mono); font-size: 8.5px; line-height: 1.05;
color: var(--black-alpha-20); white-space: pre; pointer-events: none;
opacity: .85; letter-spacing: .04em;
}
.tag-corner {
position: absolute;
color: var(--black-alpha-48);
font-family: var(--font-mono);
font-size: 10.5px; letter-spacing: .06em;
pointer-events: none; opacity: .85; z-index: 1;
}
.sq-mark { position: absolute; width: 5px; height: 5px; background: var(--black-alpha-24); pointer-events: none; }
/* ─── Topbar ─── */
.topbar {
display: flex; align-items: center; gap: 12px;
padding: 14px 28px;
border-bottom: 1px solid var(--border-faint);
background: var(--background-base);
position: relative; z-index: 2;
}
.crumbs { display: flex; align-items: center; gap: 8px; font-size: 13.5px; color: var(--black-alpha-48); }
.crumbs .sep { color: var(--black-alpha-32); }
.crumbs .here { color: var(--accent-black); font-weight: 500; }
.crumbs a:hover { color: var(--accent-black); }
.topbar .right { margin-left: auto; display: flex; align-items: center; gap: 10px; }
.balance-chip {
display: inline-flex; align-items: center; gap: 7px;
height: 36px;
padding: 0 14px 0 12px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-pill);
font-size: 13px;
color: var(--black-alpha-56);
cursor: pointer;
transition: background var(--t-base), border-color var(--t-base);
}
.balance-chip:hover { background: var(--black-alpha-4); border-color: var(--black-alpha-24); }
.balance-chip strong { color: var(--accent-black); font-weight: 600; font-variant-numeric: tabular-nums; }
.balance-chip svg { width: 13px; height: 13px; color: var(--heat); }
.icon-btn {
width: 36px; height: 36px;
display: flex; align-items: center; justify-content: center;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
color: var(--black-alpha-56);
cursor: pointer;
position: relative;
transition: background var(--t-base), border-color var(--t-base), color var(--t-base);
}
.icon-btn:hover { background: var(--black-alpha-4); color: var(--accent-black); border-color: var(--black-alpha-24); }
.icon-btn svg { width: 15px; height: 15px; }
.icon-btn .dot-noti {
position: absolute; top: 8px; right: 9px;
width: 7px; height: 7px; border-radius: 50%;
background: var(--heat); border: 1.5px solid var(--surface);
}
/* ─── Content ─── */
.content {
padding: 48px 56px 72px;
position: relative;
z-index: 1;
max-width: 1480px;
margin: 0 auto;
min-height: calc(100vh - 64px);
}
.content > .corner-mark { display: none; }
.page-head {
display: flex; align-items: flex-start; justify-content: space-between;
margin-bottom: 36px; gap: 16px; flex-wrap: wrap;
}
.page-head h1 {
font-size: 28px; font-weight: 600; letter-spacing: -.02em; line-height: 1.25;
color: var(--accent-black);
}
.page-head .sub {
font-size: 14px; color: var(--black-alpha-56);
margin-top: 10px;
display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
}
.page-head .sub .mono { font-family: var(--font-mono); font-size: 11.5px; color: var(--black-alpha-48); letter-spacing: .04em; }
.page-head .actions { display: flex; gap: 10px; align-items: center; }
.section-h { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 14px; }
.section-h h2 { font-size: 16px; font-weight: 600; letter-spacing: -.01em; color: var(--accent-black); }
.section-h .more {
font-family: var(--font-mono); font-size: 11.5px;
color: var(--black-alpha-48); letter-spacing: .04em;
cursor: pointer;
transition: color var(--t-base);
}
.section-h .more:hover { color: var(--heat); }
/* ─── Buttons · 统一高度 36 / 8 px 圆角 / 真 1px border / hover bg 切换 ─── */
.btn {
display: inline-flex; align-items: center; justify-content: center; gap: 6px;
height: 36px;
padding: 0 16px;
border-radius: var(--r-md);
font-size: 13px; font-weight: 500;
background: var(--surface);
color: var(--accent-black);
cursor: pointer;
white-space: nowrap;
font-family: inherit;
border: 1px solid var(--black-alpha-12);
transition: background var(--t-base), border-color var(--t-base), transform var(--t-fast);
}
.btn:hover { background: var(--black-alpha-4); border-color: var(--black-alpha-24); }
.btn:active { background: var(--black-alpha-7); transform: scale(.99); }
.btn:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--background-base), 0 0 0 4px var(--heat-40); }
.btn:disabled, .btn.disabled {
color: var(--black-alpha-32);
background: var(--black-alpha-5);
border-color: var(--black-alpha-12);
cursor: not-allowed;
transform: none;
}
.btn:disabled:hover, .btn.disabled:hover {
background: var(--black-alpha-5);
border-color: var(--black-alpha-12);
}
.btn svg { width: 13px; height: 13px; }
.btn-primary {
background: var(--heat);
color: var(--accent-white);
border-color: var(--heat);
font-weight: 600;
/* 继承 .btn 的 height: 36px / padding: 0 16px */
box-shadow: var(--shadow-cta);
}
.btn-primary:hover {
background: var(--heat);
border-color: var(--heat);
box-shadow: var(--shadow-cta-hover);
}
.btn-primary:active {
background: var(--heat);
transform: scale(.995);
box-shadow: var(--shadow-cta-active);
}
.btn-primary:disabled, .btn-primary.disabled {
background: var(--heat-40);
color: var(--accent-white);
border-color: var(--heat-40);
box-shadow: none;
}
.btn-ghost {
background: transparent;
border-color: transparent;
color: var(--black-alpha-56);
}
.btn-ghost:hover { background: var(--black-alpha-4); border-color: transparent; color: var(--accent-black); }
.btn-ghost:active { background: var(--black-alpha-7); }
.btn-sm { height: 28px; padding: 0 12px; font-size: 12px; border-radius: var(--r-md); }
.btn-lg { height: 40px; padding: 0 20px; font-size: 13.5px; }
/* ─── Pills ─── */
.pill {
display: inline-flex; align-items: center; gap: 6px;
padding: 4px 10px;
border-radius: var(--r-pill);
font-size: 11.5px; font-weight: 500;
border: 1px solid var(--border-faint);
background: var(--surface);
color: var(--black-alpha-56);
white-space: nowrap;
}
.pill.info { background: var(--heat-12); color: var(--heat); border-color: var(--heat-20); }
.pill.ok { background: var(--forest-bg); color: var(--accent-forest); border-color: var(--forest-bd); }
.pill.err { background: var(--crimson-bg); color: var(--accent-crimson); border-color: var(--crimson-bd); }
.pill.neutral { background: var(--black-alpha-4); color: var(--black-alpha-56); border-color: var(--border-faint); }
.pill .dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
/* ─── Cards / containers · V2.1 统一 8 px 圆角 ─── */
.card-hard {
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
position: relative;
}
/* V2.1 spec §5.3 · 容器四角准星 · SVG 圆弧内凹(22×21 viewBox) */
.card-hard.with-corners::before, .card-hard.with-corners::after,
.with-corners .corner-tr, .with-corners .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;
}
.card-hard.with-corners::before { top: -7px; left: -7px; }
.card-hard.with-corners::after { bottom: -7px; right: -7px; }
.with-corners .corner-tr { top: -7px; right: -7px; }
.with-corners .corner-bl { bottom: -7px; left: -7px; }
/* ─── Stats (KPI bar) ─── */
.stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
position: relative;
margin-bottom: 36px;
overflow: hidden;
}
.stat {
padding: 24px 28px;
border-right: 1px solid var(--border-faint);
position: relative;
cursor: pointer;
transition: background var(--t-base);
}
.stat:hover { background: var(--black-alpha-4); }
.stat:last-child { border-right: 0; }
.stat .lbl {
font-size: 12.5px; color: var(--black-alpha-48);
font-weight: 500; display: flex; align-items: center; gap: 8px;
}
.stat .lbl .badge {
font-family: var(--font-mono);
font-size: 10px; color: var(--black-alpha-48);
background: var(--black-alpha-4);
padding: 1px 7px;
border-radius: var(--r-sm);
border: 1px solid var(--border-faint);
letter-spacing: .04em;
}
.stat .v {
font-size: 32px; font-weight: 600; letter-spacing: -.02em;
line-height: 1.1; margin-top: 14px;
font-variant-numeric: tabular-nums;
color: var(--accent-black);
}
.stat .v small { font-size: 15px; color: var(--black-alpha-48); font-weight: 500; margin-left: 2px; }
.stat .delta {
font-family: var(--font-mono); font-size: 11px;
margin-top: 10px; color: var(--black-alpha-48); letter-spacing: .02em;
}
.stat .delta.up { color: var(--accent-forest); }
.stat .bar { height: 5px; background: var(--black-alpha-7); border-radius: 3px; margin-top: 14px; overflow: hidden; }
.stat .bar > span { display: block; height: 100%; background: var(--heat); border-radius: 3px; }
.stat .sub {
font-family: var(--font-mono); font-size: 10.5px;
color: var(--black-alpha-48); margin-top: 10px; letter-spacing: .02em;
}
/* ─── Form fields ─── */
.field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 18px; }
.field-label { font-size: 13px; font-weight: 500; color: var(--accent-black); }
.field-label .req { color: var(--accent-crimson); margin-left: 2px; }
.field-hint { font-size: 12px; color: var(--black-alpha-48); }
.input, .textarea, .select {
height: 36px;
padding: 0 14px;
background: var(--surface);
border: 1px solid var(--black-alpha-12);
border-radius: var(--r-md);
font-size: 13.5px;
width: 100%;
font-family: inherit;
color: var(--accent-black);
transition: border-color var(--t-base), box-shadow var(--t-base), background var(--t-base);
}
.input:hover, .textarea:hover, .select:hover { border-color: var(--black-alpha-24); }
.input:focus, .textarea:focus, .select:focus {
border-color: var(--heat-40);
box-shadow: inset 0 0 0 1px var(--heat-40);
}
.input::placeholder, .textarea::placeholder { color: var(--black-alpha-48); }
.input:disabled, .textarea:disabled, .select:disabled {
background: var(--black-alpha-5);
color: var(--black-alpha-32);
cursor: not-allowed;
}
.textarea { height: auto; min-height: 88px; padding: 12px 14px; line-height: 1.6; resize: vertical; }
.select {
appearance: none;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 16 16' fill='none'><path d='M4 6l4 4 4-4' stroke='%237a7a7a' stroke-width='1.4'/></svg>");
background-repeat: no-repeat;
background-position: right 12px center;
padding-right: 32px;
}
/* ─── Tabs ─── */
.tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--border-faint); margin-bottom: 20px; }
.tab {
padding: 10px 14px; font-size: 13px;
color: var(--black-alpha-56);
border-bottom: 2px solid transparent;
margin-bottom: -1px;
cursor: pointer;
font-weight: 500; user-select: none;
transition: color var(--t-base), background var(--t-base);
border-radius: var(--r-md) var(--r-md) 0 0;
}
.tab:hover { color: var(--accent-black); background: var(--black-alpha-4); }
.tab.active { color: var(--accent-black); border-bottom-color: var(--heat); }
.tab .count {
font-family: var(--font-mono); font-size: 10.5px;
color: var(--black-alpha-48); margin-left: 6px;
padding: 1px 7px;
background: var(--black-alpha-4);
border-radius: var(--r-sm); letter-spacing: .04em;
}
.tab.active .count { background: var(--heat-12); color: var(--heat); }
/* ─── Filter chips · 统一高度 36 px 与 .btn / .input 对齐 ─── */
.chip {
height: 36px; padding: 0 14px;
border: 1px solid var(--border-faint);
background: var(--surface);
border-radius: var(--r-md);
font-size: 13px;
color: var(--black-alpha-56);
display: inline-flex; align-items: center; gap: 6px;
cursor: pointer;
font-family: inherit;
transition: background var(--t-base), border-color var(--t-base), color var(--t-base);
}
.chip:hover { background: var(--black-alpha-4); border-color: var(--black-alpha-24); color: var(--accent-black); }
.chip.active { border-color: var(--heat-40); color: var(--heat); background: var(--heat-12); }
.chip svg { width: 12px; height: 12px; }
/* ─── Toolbar ─── */
.toolbar { display: flex; align-items: center; gap: 10px; margin-bottom: 18px; flex-wrap: wrap; }
.toolbar .search-inline {
position: relative; flex: 1; max-width: 360px;
}
.toolbar .search-inline svg {
position: absolute; left: 12px; top: 50%; transform: translateY(-50%);
color: var(--black-alpha-56); width: 14px; height: 14px;
z-index: 2; pointer-events: none;
}
.toolbar .search-inline input { padding-left: 36px; }
/* ─── Progress (5 段流水线 · V2.1 语义色 + 脉动) ─── */
.prog { display: flex; gap: 3px; }
.prog span {
width: 18px; height: 5px; border-radius: 2px;
background: var(--black-alpha-8);
transition: background var(--t-base);
}
.prog span.done { background: var(--accent-forest); }
.prog span.cur {
background: var(--heat);
animation: prog-pulse 1.4s ease-in-out infinite;
}
.prog span.fail { background: var(--accent-crimson); }
@keyframes prog-pulse {
0%, 100% { opacity: 1; transform: scaleY(1); }
50% { opacity: .55; transform: scaleY(.7); }
}
/* ─── Table ─── */
table.t {
width: 100%; border-collapse: collapse;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
overflow: hidden;
}
table.t thead th {
text-align: left;
font-size: 11.5px;
font-weight: 500;
color: var(--black-alpha-48);
padding: 14px 16px;
background: var(--black-alpha-3);
border-bottom: 1px solid var(--border-faint);
letter-spacing: .04em;
text-transform: uppercase;
font-family: var(--font-mono);
}
table.t tbody td {
padding: 16px;
border-bottom: 1px solid var(--border-faint);
font-size: 13px;
vertical-align: middle;
color: var(--accent-black);
}
table.t tbody tr:last-child td { border-bottom: 0; }
table.t tbody tr { cursor: pointer; transition: background var(--t-base); }
table.t tbody tr:hover { background: var(--black-alpha-4); }
/* ─── Placeholder thumb ─── */
.placeholder {
background:
repeating-linear-gradient(135deg, rgba(0,0,0,0.025) 0 1px, transparent 1px 12px),
var(--black-alpha-4);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
display: grid; place-items: center;
color: var(--black-alpha-48);
font-family: var(--font-mono);
font-size: 10px;
letter-spacing: .04em;
user-select: none;
overflow: hidden;
position: relative;
text-align: center;
padding: 6px;
}
.placeholder .ph-frame {
background: rgba(255, 255, 255, .92);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
padding: 3px 8px;
font-size: 10.5px;
color: var(--black-alpha-56);
font-weight: 500;
}
/* ─── Toast ─── */
.toast {
position: fixed; bottom: 24px; right: 24px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
padding: 14px 18px;
display: flex; align-items: center; gap: 12px;
box-shadow: var(--shadow-floating);
transform: translateX(420px);
transition: transform var(--t-slow);
z-index: 1000;
min-width: 260px;
}
.toast.show { transform: translateX(0); }
.toast .ic-t {
width: 26px; height: 26px;
background: var(--heat-12);
color: var(--heat);
border-radius: var(--r-md);
display: grid; place-items: center;
flex-shrink: 0;
}
.toast .ic-t svg { width: 13px; height: 13px; }
.toast .txt { font-size: 13px; color: var(--accent-black); font-weight: 500; }
.toast .txt .mono {
font-family: var(--font-mono); font-size: 11px;
color: var(--black-alpha-48); display: block; margin-top: 2px;
letter-spacing: .04em; font-weight: 400;
}
/* ─── Modal ─── */
.modal-bg {
position: fixed; inset: 0;
background: rgba(21, 20, 15, .42);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
display: none; align-items: center; justify-content: center;
z-index: 999;
opacity: 0;
transition: opacity .2s;
}
.modal-bg.show { display: flex; opacity: 1; }
.modal {
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
max-width: 480px; width: 90%;
position: relative;
transform: scale(.96);
transition: transform .25s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.modal-bg.show .modal { transform: scale(1); }
.modal::before, .modal::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;
color: var(--black-alpha-48);
font-family: var(--font-mono);
font-size: 13px; line-height: 1;
}
.modal::before { top: -7px; left: -7px; }
.modal::after { bottom: -7px; right: -7px; }
.modal .corner-tr, .modal .corner-bl {
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;
}
.modal .corner-tr { top: -7px; right: -7px; }
.modal .corner-bl { bottom: -7px; left: -7px; }
.modal-h {
padding: 22px 24px 16px;
border-bottom: 1px solid var(--border-faint);
display: flex; align-items: center; gap: 14px;
}
.modal-h .ic-m {
width: 36px; height: 36px;
background: var(--heat-12);
color: var(--heat);
border-radius: var(--r-md);
display: flex; align-items: center; justify-content: center;
flex-shrink: 0;
}
.modal-h .ic-m svg { width: 17px; height: 17px; }
.modal-h .ti { font-size: 16px; font-weight: 600; line-height: 1.4; color: var(--accent-black); }
.modal-h .ti span {
display: block; font-family: var(--font-mono); font-size: 11px;
color: var(--black-alpha-48); font-weight: 400; margin-top: 4px; letter-spacing: .04em;
}
.modal-b { padding: 20px 24px; font-size: 13.5px; color: var(--black-alpha-72); line-height: 1.75; }
.modal-b .mono-acc {
font-family: var(--font-mono); color: var(--heat);
background: var(--heat-12); padding: 2px 6px;
font-size: 11.5px; border-radius: var(--r-sm);
}
.modal-f {
padding: 16px 24px;
border-top: 1px solid var(--border-faint);
display: flex; justify-content: flex-end; gap: 10px;
}
/* ─── Drawer ─── */
.drawer-bg {
position: fixed; inset: 0;
background: rgba(21, 20, 15, .32);
display: none;
z-index: 90;
}
.drawer-bg.show { display: block; }
.drawer {
position: fixed; right: 0; top: 0; bottom: 0;
width: 540px; max-width: 100vw;
background: var(--surface);
border-left: 1px solid var(--border-faint);
z-index: 95;
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.show { transform: translateX(0); }
.drawer-h {
padding: 20px 24px;
border-bottom: 1px solid var(--border-faint);
display: flex; align-items: center;
}
.drawer-h h3 { font-size: 16px; font-weight: 600; color: var(--accent-black); }
.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;
transition: background var(--t-base);
}
.drawer-h .x:hover { background: var(--black-alpha-4); color: var(--accent-black); }
.drawer-b { padding: 24px; overflow-y: auto; flex: 1; }
.drawer-f {
padding: 16px 24px;
border-top: 1px solid var(--border-faint);
display: flex; gap: 10px; justify-content: flex-end;
background: var(--background-lighter);
}
/* ─── Scrollbar ─── */
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--black-alpha-12); border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: var(--black-alpha-24); }
/* ─── Responsive ─── */
@media (max-width: 1100px) {
.app { grid-template-columns: 1fr; }
aside.sidebar { display: none; }
.stats { grid-template-columns: repeat(2, 1fr); }
.stat:nth-child(2) { border-right: 0; }
.stat:nth-child(1), .stat:nth-child(2) { border-bottom: 1px solid var(--border-faint); }
.content { padding: 28px 24px 48px; }
}
/* ─── Spinner ─── */
.spinner {
width: 18px; height: 18px;
border: 2px solid var(--border-faint);
border-top-color: var(--heat);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

View File

@ -0,0 +1,195 @@
/* ============================================================
·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: '12',
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',
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: '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: '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: '#',
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>'
}
];
const TEAM_NAV = {
label: '团队', 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>',
badge: 'V1.5'
};
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="nav-section">协作</div>
<nav>
<a class="disabled" title="V1.5 上线,敬请期待">
${TEAM_NAV.icon}
<span>${TEAM_NAV.label}</span>
<span class="pill-mini">${TEAM_NAV.badge}</span>
</a>
</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="icon-btn" onclick="Shell.toast('通知中心', '3 条未读')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><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="dot-noti"></span>
</button>
${topActions}
</div>
</header>
`;
const decorations = `
<div class="grid-bg"></div>
<pre class="scatter" style="top:96px;left:280px"> · · +
· +XX+
+XXXX·
+X· </pre>
<pre class="scatter" style="top:340px;right:96px">+ · ·
XX· ·
·XXXX·+
·++· </pre>
<pre class="scatter" style="bottom:160px;left:42%"> · +
+·XX·
·X+ ·
· </pre>
<pre class="scatter" style="top:580px;left:60px"> +X·
·XX·
+·X·+</pre>
<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>
<span class="tag-corner" style="top:158px;left:34px">[ 200 OK ]</span>
<span class="tag-corner" style="top:158px;right:34px">[ /v2 ]</span>
<span class="tag-corner" style="bottom:36px;left:34px">[ .MP4 · 9:16 ]</span>
<span class="tag-corner" style="bottom:36px;right:34px">[ STUDIO ]</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>
`;
// 装订线 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.addEventListener('keydown', e => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault();
document.getElementById('global-search')?.focus();
}
});
},
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);
},
openModal(id) { document.getElementById(id)?.classList.add('show'); },
closeModal(id) { document.getElementById(id)?.classList.remove('show'); },
openDrawer(id) {
document.getElementById(id)?.classList.add('show');
document.getElementById(id + '-bg')?.classList.add('show');
},
closeDrawer(id) {
document.getElementById(id)?.classList.remove('show');
document.getElementById(id + '-bg')?.classList.remove('show');
}
};

File diff suppressed because it is too large Load Diff

173
电商AI平台/index.html Normal file
View File

@ -0,0 +1,173 @@
<!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 auto auto auto; align-items: center; gap: 16px; padding: 14px 18px; border-bottom: 1px solid var(--border-faint); cursor: pointer; }
.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="products.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>
<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">12</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">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> 较上月 +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 · 12 ] <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">12 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">12 个</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>Shell.render({ active: 'dashboard', crumbs: [{ label: '工作台' }] });</script>
</body>
</html>

120
电商AI平台/library.html Normal file
View File

@ -0,0 +1,120 @@
<!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>
.asset-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 14px; }
.asset-card { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); cursor: pointer; transition: background .15s; }
.asset-card:hover { background: var(--background-lighter); border-color: var(--black-alpha-48); }
.asset-thumb { aspect-ratio: 1; }
.asset-card.video .asset-thumb { aspect-ratio: 9/16; max-height: 280px; }
.asset-body { padding: 12px 14px; }
.asset-name { font-size: 13px; font-weight: 600; }
.asset-meta { font-size: 11px; color: var(--black-alpha-48); margin-top: 3px; font-family: var(--font-mono); letter-spacing: .02em; }
</style>
</head>
<body>
<div id="page">
<div class="page-head">
<div>
<h1>资产库</h1>
<div class="sub"><span class="mono">// 跨项目复用 · 人 8 · 景 14 · 商 12 · 片 8</span></div>
</div>
<div class="actions">
<button class="btn" onclick="Shell.toast('上传资产', '/library/upload')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 5v14M5 12h14"/></svg>
上传资产
</button>
</div>
</div>
<div class="tabs">
<div class="tab active">人物 <span class="count">8</span></div>
<div class="tab">场景 <span class="count">14</span></div>
<div class="tab">商品图 <span class="count">12</span></div>
<div class="tab">成片 <span class="count">8</span></div>
<div class="tab">我的上传 <span class="count">3</span></div>
</div>
<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" placeholder="搜索人物特征">
</div>
<button class="chip active">全部 <span class="mono" style="opacity:.7">8</span></button>
<button class="chip">女性</button>
<button class="chip">男性</button>
<button class="chip">25-30 岁</button>
<button class="chip">都市白领</button>
<span class="spacer"></span>
<button class="chip">最近使用 <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg></button>
</div>
<div class="asset-grid">
<div class="asset-card" onclick="Shell.toast('查看资产', '林夕')">
<div class="placeholder asset-thumb"><span class="ph-frame">林夕 · 都市白领</span></div>
<div class="asset-body">
<div class="asset-name">林夕</div>
<div class="asset-meta">女 · 25-30 · 用过 4 次</div>
</div>
</div>
<div class="asset-card" onclick="Shell.toast('查看资产', '阿楠')">
<div class="placeholder asset-thumb"><span class="ph-frame">阿楠 · 同事女</span></div>
<div class="asset-body">
<div class="asset-name">阿楠</div>
<div class="asset-meta">女 · 25-30 · 用过 2 次</div>
</div>
</div>
<div class="asset-card" onclick="Shell.toast('查看资产', '小七')">
<div class="placeholder asset-thumb"><span class="ph-frame">小七 · 学生女</span></div>
<div class="asset-body">
<div class="asset-name">小七</div>
<div class="asset-meta">女 · 18-22 · 用过 3 次</div>
</div>
</div>
<div class="asset-card" onclick="Shell.toast('查看资产', '阿杰')">
<div class="placeholder asset-thumb"><span class="ph-frame">阿杰 · 通勤男</span></div>
<div class="asset-body">
<div class="asset-name">阿杰</div>
<div class="asset-meta">男 · 28-35 · 用过 2 次</div>
</div>
</div>
<div class="asset-card" onclick="Shell.toast('查看资产', '王姐')">
<div class="placeholder asset-thumb"><span class="ph-frame">妈妈 · 居家</span></div>
<div class="asset-body">
<div class="asset-name">妈妈 · 王姐</div>
<div class="asset-meta">女 · 38-45 · 用过 1 次</div>
</div>
</div>
<div class="asset-card" onclick="Shell.toast('查看资产', '阿强')">
<div class="placeholder asset-thumb"><span class="ph-frame">阿强 · 健身男</span></div>
<div class="asset-body">
<div class="asset-name">阿强</div>
<div class="asset-meta">男 · 22-28 · 用过 2 次</div>
</div>
</div>
<div class="asset-card" onclick="Shell.toast('查看资产', '小苏')">
<div class="placeholder asset-thumb"><span class="ph-frame">研究生 · 文艺女</span></div>
<div class="asset-body">
<div class="asset-name">小苏</div>
<div class="asset-meta">女 · 22-26 · 用过 1 次</div>
</div>
</div>
<div class="asset-card" onclick="Shell.toast('查看资产', '闺蜜组合')">
<div class="placeholder asset-thumb"><span class="ph-frame">闺蜜组合 · 双人</span></div>
<div class="asset-body">
<div class="asset-name">闺蜜组合</div>
<div class="asset-meta">双人 · 25-30 · 用过 1 次</div>
</div>
</div>
</div>
</div>
<script src="assets/shell.js"></script>
<script>Shell.render({ active: 'library', crumbs: [{ label: '工作台', href: 'index.html' }, { label: '资产库' }] });</script>
</body>
</html>

View File

@ -0,0 +1,784 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>补水面膜 · v3 · 流水线 · 流·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>
/* ─── Project header ─── */
.proj-head { display: flex; justify-content: space-between; gap: 16px; margin-bottom: 22px; align-items: flex-start; }
.proj-head h1 { font-size: 20px; font-weight: 700; letter-spacing: -.012em; }
/* ─── Stepper ─── */
.stepper { display: flex; align-items: center; gap: 0; margin-bottom: 28px; padding: 14px 18px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); position: relative; }
.stepper::before, .stepper::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; }
.stepper::before { top: -7px; left: -7px; }
.stepper::after { bottom: -7px; right: -7px; }
.stepper .corner-tr, .stepper .corner-bl { 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; }
.stepper .corner-tr { top: -7px; right: -7px; }
.stepper .corner-bl { bottom: -7px; left: -7px; }
.stage-step { display: flex; align-items: center; gap: 10px; padding: 6px 0; cursor: pointer; user-select: none; }
.stage-step .num { width: 26px; height: 26px; display: grid; place-items: center; font-family: var(--font-mono); font-size: 12px; font-weight: 600; border: 1px solid var(--border-faint); border-radius: var(--r-sm); background: var(--surface); color: var(--black-alpha-48); flex-shrink: 0; }
.stage-step.done .num { background: var(--accent-black); border-color: var(--accent-black); color: var(--accent-white); }
.stage-step.active .num { background: var(--heat); border-color: var(--heat); color: var(--accent-white); }
.stage-step.locked { opacity: .5; cursor: not-allowed; }
.stage-step .lbl { font-size: 13px; font-weight: 500; color: var(--black-alpha-56); }
.stage-step.active .lbl { color: var(--accent-black); font-weight: 600; }
.stage-step .st { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); margin-left: 4px; padding: 1px 6px; background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-sm); letter-spacing: .04em; }
.stage-step.active .st { background: var(--heat-12); color: var(--heat); border-color: var(--heat-20); }
.stage-line { flex: 1; height: 1px; background: var(--border-faint); margin: 0 14px; min-width: 30px; }
.stage-line.done { background: var(--accent-black); }
/* ─── Stage panes ─── */
.stage { display: none; }
.stage.active { display: block; }
/* Common pane */
.pane { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); }
.pane-h { display: flex; align-items: center; gap: 8px; padding: 14px 18px; border-bottom: 1px solid var(--border-faint); }
.pane-h strong { font-size: 14px; font-weight: 600; }
/* Stage foot */
.stage-foot { display: flex; justify-content: space-between; align-items: center; padding: 18px 0 0; margin-top: 18px; border-top: 1px solid var(--border-faint); }
.stage-foot .info { font-size: 12.5px; color: var(--black-alpha-56); }
.stage-foot .info .mono { font-family: var(--font-mono); color: var(--black-alpha-48); font-size: 11.5px; letter-spacing: .02em; }
/* === STAGE 1 · 脚本 === */
.stage-script { display: grid; grid-template-columns: 1fr 1.2fr; gap: 16px; min-height: 560px; }
.chat-pane { display: flex; flex-direction: column; }
.chat-body { padding: 16px 18px; flex: 1; overflow-y: auto; max-height: 460px; display: flex; flex-direction: column; gap: 14px; }
.msg .bubble { max-width: 90%; padding: 10px 14px; font-size: 13px; line-height: 1.6; border: 1px solid var(--border-faint); border-radius: var(--r-md); }
.msg.ai .bubble { background: var(--surface); }
.msg.user { display: flex; flex-direction: column; align-items: flex-end; }
.msg.user .bubble { background: var(--heat-12); color: var(--accent-black); border-color: var(--heat-20); }
.msg .time { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); margin-top: 4px; letter-spacing: .02em; }
.msg .actions { display: flex; gap: 6px; margin-top: 6px; }
.ai-avatar { width: 26px; height: 26px; background: var(--heat); color: var(--accent-white); display: grid; place-items: center; font-size: 11px; font-weight: 700; border: 1px solid var(--heat); border-radius: 50%; }
.del { text-decoration: line-through; color: var(--black-alpha-48); }
.ins { background: var(--forest-bg); color: var(--accent-forest); padding: 0 3px; }
.chat-input { padding: 14px 18px; border-top: 1px solid var(--border-faint); }
.shot-list { display: flex; flex-direction: column; }
.shots-body { padding: 12px 16px; flex: 1; overflow-y: auto; max-height: 540px; display: flex; flex-direction: column; gap: 10px; }
.shot-card { background: var(--background-base); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 12px 14px; }
.shot-card.highlight { border-color: var(--heat); background: var(--heat-12); }
.shot-head { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
.shot-num { width: 22px; height: 22px; background: var(--accent-black); color: var(--accent-white); display: grid; place-items: center; font-family: var(--font-mono); font-size: 11px; font-weight: 700; border-radius: var(--r-sm); }
.shot-time { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); padding: 2px 6px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-sm); }
.shot-row { display: grid; grid-template-columns: 36px 1fr; gap: 8px; padding: 4px 0; }
.shot-k { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); padding-top: 2px; letter-spacing: .04em; }
.shot-v { font-size: 12.5px; color: var(--accent-black); line-height: 1.55; }
.icon-mini-btn { width: 24px; height: 24px; display: grid; place-items: center; color: var(--black-alpha-48); background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-sm); cursor: pointer; font-size: 14px; }
.icon-mini-btn:hover { color: var(--heat); border-color: var(--heat); }
/* === STAGE 2 · 基础资产 === */
.stage-assets { display: grid; grid-template-columns: 200px 1fr; gap: 24px; }
.asset-side .ttab { padding: 10px 12px; font-size: 13px; cursor: pointer; display: flex; align-items: center; gap: 8px; border: 1px solid transparent; border-radius: var(--r-md); }
.asset-side .ttab:hover { background: var(--background-lighter); }
.asset-side .ttab.active { background: var(--heat-12); color: var(--heat); border-color: var(--heat-20); font-weight: 600; }
.asset-side .ttab .num { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); margin-left: auto; }
.asset-side .ttab.active .num { color: var(--heat); }
.asset-side .info { font-size: 12px; color: var(--black-alpha-48); padding: 14px 12px; line-height: 1.6; margin-top: 14px; border-top: 1px solid var(--border-faint); }
.asset-side .info strong { color: var(--black-alpha-56); display: block; }
.asset-side .info .mono { font-family: var(--font-mono); }
.asset-grid-2 { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 14px; }
.asset-card-2 { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); }
.asset-card-2 .thumb-2 { aspect-ratio: 1; }
.asset-card-2 .body-2 { padding: 12px 14px; }
.prompt-box { background: var(--background-base); border: 1px solid var(--border-faint); border-radius: var(--r-sm); padding: 10px 12px; font-size: 12px; color: var(--black-alpha-56); margin-top: 8px; line-height: 1.55; font-family: var(--font-mono); letter-spacing: .01em; }
.fail-icon { width: 28px; height: 28px; background: var(--accent-crimson); color: var(--accent-white); display: grid; place-items: center; font-weight: 700; font-size: 16px; border-radius: 50%; }
/* === STAGE 3 · 故事板 === */
.stage-storyboard { display: grid; grid-template-columns: 1.7fr 1fr; gap: 16px; align-items: start; }
.sb-canvas { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); }
.sb-row { display: grid; grid-template-columns: 60px 1fr 1fr; gap: 0; border-bottom: 1px solid var(--border-faint); }
.sb-row:last-child { border-bottom: 0; }
.sb-row.head { background: var(--background-lighter); font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .04em; text-transform: uppercase; }
.sb-row.head > div { padding: 10px 14px; }
.sb-num { padding: 14px; font-family: var(--font-mono); font-size: 14px; font-weight: 700; color: var(--accent-black); border-right: 1px solid var(--border-faint); }
.sb-num .t { display: block; font-size: 10.5px; color: var(--black-alpha-48); font-weight: 400; margin-top: 4px; letter-spacing: .02em; }
.sb-img { padding: 10px; border-right: 1px solid var(--border-faint); }
.sb-img .placeholder { aspect-ratio: 16/9; }
.sb-text { padding: 14px; display: flex; flex-direction: column; gap: 6px; }
.sb-text .meta { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .04em; }
.sb-text .dialog { font-size: 12.5px; color: var(--accent-black); line-height: 1.55; }
.sb-text .sfx { font-size: 11.5px; color: var(--black-alpha-48); }
.sb-side .pane { padding: 18px; }
.prompt-edit { background: var(--background-base); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 12px 14px; font-family: var(--font-mono); font-size: 11.5px; line-height: 1.7; color: var(--black-alpha-56); white-space: pre-wrap; min-height: 200px; outline: none; letter-spacing: .01em; }
.prompt-edit:focus { border-color: var(--heat); box-shadow: 0 0 0 3px var(--heat-12); }
.asset-tag { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-pill); font-size: 11.5px; }
.asset-tag .dotc { width: 14px; height: 14px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: 50%; }
/* === STAGE 4 · 视频片段 === */
.queue-bar { display: flex; align-items: center; gap: 16px; padding: 14px 18px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); margin-bottom: 18px; }
.queue-bar .bar-wrap { flex: 1; height: 6px; background: var(--background-lighter); overflow: hidden; }
.queue-bar .bar-wrap > span { display: block; height: 100%; background: var(--heat); }
.video-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 14px; }
.video-card { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); }
.video-thumb { aspect-ratio: 9/16; max-height: 320px; position: relative; border-radius: var(--r-md) var(--r-md) 0 0; }
.video-thumb .play { position: absolute; inset: 0; display: grid; place-items: center; background: rgba(0,0,0,0.05); cursor: pointer; opacity: 0; transition: opacity .15s; }
.video-thumb:hover .play { opacity: 1; }
.video-thumb .btn-play { width: 36px; height: 36px; background: rgba(0,0,0,.7); color: var(--accent-white); border-radius: 50%; display: grid; place-items: center; }
.video-card .body { padding: 10px 12px; }
/* === STAGE 5 · 编辑器 === */
.editor { display: grid; grid-template-columns: 1fr 280px; grid-template-rows: 1fr auto; gap: 0; height: 580px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); }
.editor-preview { padding: 16px; border-right: 1px solid var(--border-faint); border-bottom: 1px solid var(--border-faint); display: flex; flex-direction: column; gap: 12px; }
.editor-preview .canvas { flex: 1; aspect-ratio: 9/16; max-height: 380px; margin: 0 auto; background:
repeating-linear-gradient(135deg, rgba(0,0,0,0.03) 0 1px, transparent 1px 12px),
var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
display: grid; place-items: center;
color: var(--black-alpha-48);
font-family: var(--font-mono);
font-size: 12px; }
.editor-preview .controls { display: flex; align-items: center; gap: 8px; justify-content: center; }
.ctl-btn { width: 36px; height: 36px; border: 1px solid var(--border-faint); background: var(--surface); color: var(--black-alpha-56); border-radius: var(--r-md); display: grid; place-items: center; cursor: pointer; transition: background var(--t-base), border-color var(--t-base), color var(--t-base); }
.ctl-btn:hover { color: var(--heat); border-color: var(--heat-40); background: var(--heat-12); }
.editor-props { padding: 16px; border-bottom: 1px solid var(--border-faint); overflow-y: auto; }
.props-tabs { display: flex; gap: 0; margin-bottom: 14px; border-bottom: 1px solid var(--border-faint); }
.props-tabs > div { padding: 8px 12px; font-size: 12.5px; color: var(--black-alpha-56); cursor: pointer; border-bottom: 2px solid transparent; margin-bottom: -1px; }
.props-tabs > div.active { color: var(--heat); border-bottom-color: var(--heat); font-weight: 600; }
.style-swatch { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.swatch-card { padding: 10px; border: 1px solid var(--border-faint); border-radius: var(--r-md); cursor: pointer; }
.swatch-card:hover { background: var(--background-lighter); }
.swatch-card.selected { border-color: var(--heat); background: var(--heat-12); }
.swatch-card .demo { font-size: 12px; padding: 6px 8px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-sm); margin-bottom: 4px; text-align: center; }
.swatch-card .demo.b { background: var(--accent-black); color: var(--accent-white); font-family: serif; }
.swatch-card .demo.c { color: var(--heat); -webkit-text-stroke: 0.5px var(--accent-black); }
.swatch-card .demo.d { background: var(--accent-honey); color: var(--accent-black); font-weight: 700; }
.swatch-card .nm { font-size: 11px; color: var(--black-alpha-48); font-family: var(--font-mono); letter-spacing: .02em; }
.props-row { display: flex; align-items: center; padding: 8px 0; border-bottom: 1px solid var(--border-faint); font-size: 12.5px; }
.props-row:last-child { border-bottom: 0; }
.props-row .k { color: var(--black-alpha-48); flex: 1; font-family: var(--font-mono); font-size: 11px; letter-spacing: .02em; }
.input-mini { width: 90px; padding: 0 10px; height: 28px; font-size: 12px; border-radius: var(--r-md); background: var(--surface); border: 1px solid var(--black-alpha-12); }
.timeline { grid-column: 1 / -1; padding: 14px 16px; background: var(--background-base); }
.tl-toolbar { display: flex; align-items: center; gap: 6px; margin-bottom: 10px; padding-bottom: 10px; border-bottom: 1px solid var(--border-faint); }
.tl-ruler { display: grid; grid-template-columns: 80px 1fr; align-items: center; padding: 4px 0; font-size: 10.5px; }
.tl-ruler .l { font-family: var(--font-mono); color: var(--black-alpha-48); padding-left: 4px; }
.tl-ruler .ticks { display: flex; justify-content: space-between; font-family: var(--font-mono); color: var(--black-alpha-48); padding: 0 4px; letter-spacing: .04em; }
.tl-track { display: grid; grid-template-columns: 80px 1fr; align-items: center; gap: 0; padding: 6px 0; }
.tl-track .label { font-size: 11.5px; color: var(--black-alpha-56); display: flex; align-items: center; gap: 6px; padding-left: 4px; }
.tl-track .label .dot { width: 8px; height: 8px; }
.tl-track .lane { display: flex; gap: 2px; height: 30px; position: relative; }
.clip { padding: 0 8px; font-size: 11px; display: flex; align-items: center; cursor: pointer; overflow: hidden; white-space: nowrap; user-select: none; }
.clip.video { background: var(--heat-12); border: 1px solid var(--heat-40); color: var(--heat); }
.clip.video.selected { background: var(--heat); color: var(--accent-white); border-color: var(--heat); }
.clip.subtitle { background: var(--forest-bg); border: 1px solid var(--forest-bd); color: var(--accent-forest); }
.clip.bgm { background: rgba(144, 97, 255, 0.10); border: 1px solid rgba(144, 97, 255, 0.30); color: var(--accent-amethyst); }
.clip .num { font-family: var(--font-mono); font-weight: 700; margin-right: 6px; opacity: .7; }
.playhead { position: absolute; top: -16px; bottom: -54px; width: 1px; background: var(--heat); pointer-events: none; }
.playhead::before { content: ''; position: absolute; top: -2px; left: -4px; width: 9px; height: 9px; background: var(--heat); transform: rotate(45deg); }
</style>
</head>
<body>
<div id="page">
<!-- Project header -->
<div class="proj-head">
<div style="display:flex; gap:14px; align-items:center;">
<div class="placeholder" style="width:42px;height:54px;"><span class="ph-frame">9:16</span></div>
<div>
<div style="display:flex; gap:8px; align-items:center;">
<h1>补水面膜 · 痛点种草 · v3</h1>
<span class="pill info"><span class="dot"></span>进行中</span>
</div>
<div class="muted-2 mono" style="font-size:11.5px; margin-top:4px; letter-spacing:.02em;">// 透真补水面膜 · AI 全生 · 6 镜 · 0-15s · 9:16</div>
</div>
</div>
<div class="hstack">
<button class="btn btn-ghost btn-sm" onclick="Shell.toast('分享', '/projects/p3/share')">分享</button>
<button class="btn btn-sm" onclick="Shell.toast('已复制项目', '补水面膜 · v4')">复制项目</button>
<button class="btn btn-sm" onclick="Shell.toast('归档项目', '/projects/p3/archive')">归档</button>
</div>
</div>
<!-- Stage stepper -->
<div class="stepper">
<span class="corner-tr" aria-hidden></span><span class="corner-bl" aria-hidden></span>
<a class="stage-step done" data-stage="1" href="#stage-1"><div class="num">1</div><div class="lbl">脚本</div><div class="st">已确认</div></a>
<div class="stage-line done"></div>
<a class="stage-step done" data-stage="2" href="#stage-2"><div class="num">2</div><div class="lbl">基础资产</div><div class="st">已确认</div></a>
<div class="stage-line done"></div>
<a class="stage-step active" data-stage="3" href="#stage-3"><div class="num">3</div><div class="lbl">故事板</div><div class="st">待确认</div></a>
<div class="stage-line"></div>
<a class="stage-step" data-stage="4" href="#stage-4"><div class="num">4</div><div class="lbl">视频片段</div><div class="st">未开始</div></a>
<div class="stage-line"></div>
<a class="stage-step" data-stage="5" href="#stage-5"><div class="num">5</div><div class="lbl">拼接导出</div><div class="st">未开始</div></a>
</div>
<!-- ============= STAGE 1 · 脚本 ============= -->
<section class="stage" data-stage-pane="1">
<div class="stage-script">
<div class="pane chat-pane">
<div class="pane-h">
<div class="ai-avatar">AI</div>
<strong>脚本助手</strong>
<span class="muted-2 mono" style="font-size:11px;">· GPT-4o</span>
<span class="spacer"></span>
<button class="btn btn-ghost btn-sm" onclick="Shell.toast('已清空对话')">清空对话</button>
</div>
<div class="chat-body">
<div class="msg ai">
<div style="display:flex; gap:10px; align-items:flex-start;">
<div class="ai-avatar" style="margin-top:2px;">AI</div>
<div class="bubble">根据 <strong>透真补水面膜</strong> 和"痛点种草"风格,我先生成了一版 6 镜的脚本,主线是"加班党的深夜急救"。你可以直接编辑右侧的镜头,或者让我重写某一镜。</div>
</div>
<div class="time" style="margin-left:36px;">14:02</div>
</div>
<div class="msg user">
<div class="bubble">第 4 镜对白太硬了,能不能更口语化?</div>
<div class="time">14:05</div>
</div>
<div class="msg ai">
<div style="display:flex; gap:10px; align-items:flex-start;">
<div class="ai-avatar" style="margin-top:2px;">AI</div>
<div>
<div class="bubble">把 "<span class="del">补水力极强,锁水持久</span>" 改成 "<span class="ins">真的,敷完第二天起来脸是软的,不是绷着的</span>",更像真实分享。已替换右侧第 4 镜,确认要的话点 [接受]。</div>
<div class="actions">
<button class="btn btn-sm" onclick="Shell.toast('已接受改动', 'shot-4')">接受</button>
<button class="btn btn-ghost btn-sm" onclick="Shell.toast('再来一版')">再来一版</button>
</div>
</div>
</div>
<div class="time" style="margin-left:36px;">14:05</div>
</div>
</div>
<div class="chat-input">
<textarea class="textarea" placeholder="对脚本的修改诉求 · 比如:让第 5 镜更夸张一点、整体加 1 镜结尾……" rows="2"></textarea>
<div class="hstack" style="margin-top:8px;">
<button class="btn btn-ghost btn-sm">↻ 整体重写</button>
<span class="spacer"></span>
<button class="btn btn-primary" onclick="Shell.toast('已发送', 'POST /chat')">发送 ⌘↵</button>
</div>
</div>
</div>
<div class="pane shot-list">
<div class="pane-h">
<strong>镜头脚本</strong>
<span class="muted-2 mono" style="font-size:11px;">· 6 镜 · 0-15s</span>
<span class="spacer"></span>
<button class="btn btn-ghost btn-sm" onclick="Shell.toast('已加一镜')">+ 加一镜</button>
</div>
<div class="shots-body">
<div class="shot-card">
<div class="shot-head"><div class="shot-num">1</div><div class="shot-time">0-2s</div><span class="spacer"></span><button class="icon-mini-btn" title="重写"></button></div>
<div class="shot-row"><span class="shot-k">画面</span><div class="shot-v">深夜的办公桌,电脑屏幕亮着,女主对着镜子叹气,皮肤干燥起皮特写。</div></div>
<div class="shot-row"><span class="shot-k">对白</span><div class="shot-v">(叹气)"加班三天,脸已经不能看了……"</div></div>
</div>
<div class="shot-card">
<div class="shot-head"><div class="shot-num">2</div><div class="shot-time">2-5s</div><span class="spacer"></span><button class="icon-mini-btn"></button></div>
<div class="shot-row"><span class="shot-k">画面</span><div class="shot-v">女主从抽屉拿出补水面膜,包装特写,光线柔和。</div></div>
<div class="shot-row"><span class="shot-k">对白</span><div class="shot-v">"还好我有这个 —— 透真玻尿酸面膜。"</div></div>
</div>
<div class="shot-card">
<div class="shot-head"><div class="shot-num">3</div><div class="shot-time">5-8s</div><span class="spacer"></span><button class="icon-mini-btn"></button></div>
<div class="shot-row"><span class="shot-k">画面</span><div class="shot-v">面膜布展开,30g 精华液从布上滴落特写,慢镜头。</div></div>
<div class="shot-row"><span class="shot-k">对白</span><div class="shot-v">"30g 精华液,一片顶三片的量。"</div></div>
</div>
<div class="shot-card highlight">
<div class="shot-head"><div class="shot-num">4</div><div class="shot-time">8-11s</div><span class="pill info" style="margin-left:6px;"><span class="dot"></span>刚改</span><span class="spacer"></span><button class="icon-mini-btn"></button></div>
<div class="shot-row"><span class="shot-k">画面</span><div class="shot-v">女主敷面膜,闭眼平躺,灯光暖。床头闹钟显示 23:41。</div></div>
<div class="shot-row"><span class="shot-k">对白</span><div class="shot-v">"真的,敷完第二天起来脸是软的,不是绷着的。"</div></div>
</div>
<div class="shot-card">
<div class="shot-head"><div class="shot-num">5</div><div class="shot-time">11-13s</div><span class="spacer"></span><button class="icon-mini-btn"></button></div>
<div class="shot-row"><span class="shot-k">画面</span><div class="shot-v">第二天早上,女主对镜化妆,皮肤透亮,状态饱满。</div></div>
<div class="shot-row"><span class="shot-k">对白</span><div class="shot-v">"早上化妆都能看出来,不假吹。"</div></div>
</div>
<div class="shot-card">
<div class="shot-head"><div class="shot-num">6</div><div class="shot-time">13-15s</div><span class="spacer"></span><button class="icon-mini-btn"></button></div>
<div class="shot-row"><span class="shot-k">画面</span><div class="shot-v">面膜产品大图,价格标签 "5 片装 ¥39.9",购物车浮动按钮。</div></div>
<div class="shot-row"><span class="shot-k">对白</span><div class="shot-v">"618 五片才 39.9,囤起来。"</div></div>
</div>
</div>
</div>
</div>
<div class="stage-foot">
<div class="info"><span class="mono">[ LLM 用量 ~2.4k tokens · ¥0.04 ]</span></div>
<div class="hstack">
<button class="btn" onclick="Shell.toast('重新生成', 'POST /script/regen')"><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 12a8 8 0 0 1 14-5.5L21 9"/><path d="M21 4v5h-5"/><path d="M20 12a8 8 0 0 1-14 5.5L3 15"/><path d="M3 20v-5h5"/></svg> 重新生成全部</button>
<button class="btn btn-primary btn-lg" onclick="location.hash='#stage-2'">确认脚本,进入下一步 <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></button>
</div>
</div>
</section>
<!-- ============= STAGE 2 · 基础资产 ============= -->
<section class="stage" data-stage-pane="2">
<div class="stage-assets">
<div class="asset-side">
<div class="ttab active"><span>人物</span><span class="num">2/2</span></div>
<div class="ttab"><span>场景</span><span class="num">3/3</span></div>
<div class="ttab"><span>商品</span><span class="num">3 张</span></div>
<div class="info">
基础资产是后续故事板的素材。生成后可以单独修改提示词重跑,或上传你已有的图替换。
<br><br>
<strong class="mono">// 人物 +¥0.20/张</strong>
<strong class="mono">// 场景 +¥0.15/张</strong>
<span style="color:var(--black-alpha-48);">商品图无成本(直接复用商品库)</span>
</div>
</div>
<div>
<div class="hstack" style="margin-bottom:14px;">
<h3 style="font-size:15px; font-weight:600;">人物 · 2 个</h3>
<span class="spacer"></span>
<button class="btn btn-ghost btn-sm">从我的资产库选</button>
<button class="btn btn-sm" onclick="Shell.toast('+ 新增人物')">+ 新增人物</button>
</div>
<div class="asset-grid-2">
<div class="asset-card-2">
<div class="placeholder thumb-2"><span class="ph-frame">林夕 · 都市白领</span></div>
<div class="body-2">
<div class="hstack"><strong style="font-size:13.5px;">主角 · 林夕</strong><span class="spacer"></span><span class="pill ok"><span class="dot"></span>已确认</span></div>
<div class="prompt-box">25-30 岁都市白领,长发,穿宽松米色家居服,温柔但带点疲倦感,肤色偏黄/略干。</div>
<div class="hstack" style="margin-top:10px;">
<button class="btn btn-ghost btn-sm">改提示词</button>
<button class="btn btn-ghost btn-sm">↻ 重跑</button>
<span class="spacer"></span>
<button class="btn btn-ghost btn-sm">⤓ 上传替换</button>
</div>
</div>
</div>
<div class="asset-card-2">
<div class="placeholder thumb-2">
<div style="display:flex; flex-direction:column; gap:8px; align-items:center;">
<div class="spinner"></div>
<span class="ph-frame">生成中 · 约 8s</span>
</div>
</div>
<div class="body-2">
<div class="hstack"><strong style="font-size:13.5px;">朋友/同事 · 阿楠</strong><span class="spacer"></span><span class="pill info"><span class="dot"></span>生成中</span></div>
<div class="prompt-box">25-30 岁同龄女性,短发,穿白色衬衫,妆容精致皮肤好,作为对比。</div>
<div class="hstack" style="margin-top:10px;">
<button class="btn btn-ghost btn-sm" disabled>改提示词</button>
<button class="btn btn-ghost btn-sm" disabled>↻ 重跑</button>
<span class="spacer"></span>
<button class="btn btn-ghost btn-sm" disabled>⤓ 上传替换</button>
</div>
</div>
</div>
</div>
<div class="hstack" style="margin:28px 0 14px;">
<h3 style="font-size:15px; font-weight:600;">场景 · 3 个</h3>
<span class="spacer"></span>
<button class="btn btn-ghost btn-sm">从我的资产库选</button>
<button class="btn btn-sm">+ 新增场景</button>
</div>
<div class="asset-grid-2">
<div class="asset-card-2">
<div class="placeholder thumb-2"><span class="ph-frame">深夜办公桌</span></div>
<div class="body-2">
<div class="hstack"><strong style="font-size:13.5px;">深夜办公桌</strong><span class="spacer"></span><span class="pill ok"><span class="dot"></span>已确认</span></div>
<div class="prompt-box">深夜居家办公环境,木质书桌,台灯暖光,电脑屏幕亮着,背景虚化。</div>
</div>
</div>
<div class="asset-card-2">
<div class="placeholder thumb-2"><span class="ph-frame">床头特写</span></div>
<div class="body-2">
<div class="hstack"><strong style="font-size:13.5px;">卧室床头</strong><span class="spacer"></span><span class="pill ok"><span class="dot"></span>已确认</span></div>
<div class="prompt-box">米白色床品,木质床头柜,闹钟显示晚间时间,氛围温柔安静。</div>
</div>
</div>
<div class="asset-card-2">
<div class="placeholder thumb-2">
<div style="display:flex; flex-direction:column; gap:6px; align-items:center;">
<div class="fail-icon">!</div>
<span class="ph-frame">生成失败</span>
</div>
</div>
<div class="body-2">
<div class="hstack"><strong style="font-size:13.5px;">通勤地铁</strong><span class="spacer"></span><span class="pill err"><span class="dot"></span>失败</span></div>
<div class="prompt-box">早高峰地铁车厢,光线偏冷,年轻通勤族,氛围紧张。</div>
<div class="muted-2" style="font-size:12px; margin-top:6px; display:flex; align-items:center; gap:6px;"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="color:var(--accent-crimson); flex-shrink:0;"><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 9v4"/><path d="M12 17h.01"/></svg>提示词被安全审核拦截,请调整后重试(不扣费)</div>
<div class="hstack" style="margin-top:8px;">
<button class="btn btn-sm">改提示词</button>
<button class="btn btn-ghost btn-sm">↻ 重跑</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="stage-foot">
<div class="info"><span class="mono">[ 已确认 ¥0.85 · 待生成 ¥0.20 · 失败 ¥0(不扣) ]</span></div>
<div class="hstack">
<button class="btn" onclick="location.hash='#stage-1'">← 返回脚本</button>
<button class="btn btn-primary btn-lg" disabled>1 个资产生成中 · 等待</button>
</div>
</div>
</section>
<!-- ============= STAGE 3 · 故事板 ============= -->
<section class="stage active" data-stage-pane="3">
<div class="stage-storyboard">
<div class="sb-canvas">
<div class="sb-row head">
<div>镜号</div><div>画面</div><div>机位 / 对白 / 音效</div>
</div>
<div class="sb-row">
<div class="sb-num">1<span class="t">0-2s</span></div>
<div class="sb-img"><div class="placeholder"><span class="ph-frame">深夜办公桌 · 女主对镜叹气 · 暖光</span></div></div>
<div class="sb-text">
<div class="meta">中景 / 固定机位</div>
<div class="dialog">"加班三天,脸已经不能看了……"</div>
<div class="sfx">SFX:键盘声 + 远处空调嗡鸣</div>
</div>
</div>
<div class="sb-row">
<div class="sb-num">2<span class="t">2-5s</span></div>
<div class="sb-img"><div class="placeholder"><span class="ph-frame">面膜包装特写 · 抽屉光线柔和</span></div></div>
<div class="sb-text">
<div class="meta">特写 / 缓推</div>
<div class="dialog">"还好我有这个 —— 透真玻尿酸面膜。"</div>
<div class="sfx">SFX:抽屉滑动声</div>
</div>
</div>
<div class="sb-row">
<div class="sb-num">3<span class="t">5-8s</span></div>
<div class="sb-img"><div class="placeholder"><span class="ph-frame">面膜布展开 · 30g 精华滴落 · 慢动作</span></div></div>
<div class="sb-text">
<div class="meta">微距 / 慢镜头</div>
<div class="dialog">"30g 精华液,一片顶三片的量。"</div>
<div class="sfx">SFX:水滴慢速回弹</div>
</div>
</div>
<div class="sb-row">
<div class="sb-num">4<span class="t">8-11s</span></div>
<div class="sb-img"><div class="placeholder"><span class="ph-frame">女主敷面膜平躺 · 闹钟 23:41</span></div></div>
<div class="sb-text">
<div class="meta">中近景 / 固定</div>
<div class="dialog">"敷完第二天起来脸是软的,不是绷着的。"</div>
<div class="sfx">SFX:呼吸声 + 窗外风声</div>
</div>
</div>
<div class="sb-row">
<div class="sb-num">5<span class="t">11-13s</span></div>
<div class="sb-img"><div class="placeholder"><span class="ph-frame">早晨化妆台 · 女主对镜上妆 · 透亮</span></div></div>
<div class="sb-text">
<div class="meta">中景 / 固定</div>
<div class="dialog">"早上化妆都能看出来,不假吹。"</div>
<div class="sfx">SFX:化妆刷轻扫声</div>
</div>
</div>
<div class="sb-row">
<div class="sb-num">6<span class="t">13-15s</span></div>
<div class="sb-img"><div class="placeholder"><span class="ph-frame">产品大图 · 价格 5片¥39.9 · 购物车</span></div></div>
<div class="sb-text">
<div class="meta">产品定格 / 静止</div>
<div class="dialog">"618 五片才 39.9,囤起来。"</div>
<div class="sfx">SFX:清脆叮咚音效</div>
</div>
</div>
</div>
<div class="sb-side">
<div class="pane">
<div class="hstack" style="margin-bottom:10px;">
<strong style="font-size:14px;">故事板</strong>
<span class="spacer"></span>
<span class="pill ok"><span class="dot"></span>已生成</span>
</div>
<div class="muted-2" style="font-size:12px; line-height:1.55; margin-bottom:14px;">
整张故事板由 image-2 一次性输出,包含画面 + 镜头说明。如需修改请编辑下方提示词整张重跑(不能局部改)。
</div>
<div class="muted mono" style="font-size:11px; font-weight:500; margin-bottom:6px; letter-spacing:.04em;">// 视觉提示词</div>
<div class="prompt-edit" contenteditable="true">风格:日系小清新短视频,暖色调,午夜→清晨光线变化。
镜头列表(6 镜,0-15s):
1. 中景 · 深夜办公桌女主对镜叹气
2. 特写 · 面膜包装从抽屉拿出
3. 微距 · 面膜布展开 + 精华液滴落慢镜
4. 中近景 · 女主敷面膜平躺,床头闹钟 23:41
5. 中景 · 早晨化妆台 + 女主透亮上妆
6. 产品定格 · 面膜盒 + 价格标签 ¥39.9
人物:林夕(参考 R1)
场景:深夜办公桌(S1) + 卧室床头(S2)
要求:表格化布局,每镜含画面 + 文字说明</div>
<div class="hstack" style="margin-top:12px;">
<button class="btn btn-sm" onclick="Shell.toast('整张重跑', 'POST /storyboard/regen ¥0.45')">↻ 整张重跑</button>
<span class="spacer"></span>
<span class="muted-2 mono" style="font-size:11px;">~¥0.45</span>
</div>
<div class="divider"></div>
<div class="muted mono" style="font-size:11px; font-weight:500; margin-bottom:8px; letter-spacing:.04em;">// 绑定的资产</div>
<div style="display:flex; gap:6px; flex-wrap:wrap;">
<span class="asset-tag"><span class="dotc"></span>林夕(人物)</span>
<span class="asset-tag"><span class="dotc"></span>深夜办公桌(场景)</span>
<span class="asset-tag"><span class="dotc"></span>卧室床头(场景)</span>
<span class="asset-tag"><span class="dotc"></span>面膜盒(商品)</span>
</div>
</div>
</div>
</div>
<div class="stage-foot">
<div class="info"><span class="mono">[ image-2 一次 ¥0.45 · 累计 ¥1.50 ]</span></div>
<div class="hstack">
<button class="btn" onclick="location.hash='#stage-2'"><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="M19 12H5M12 19l-7-7 7-7"/></svg> 返回资产</button>
<button class="btn btn-primary btn-lg" onclick="location.hash='#stage-4'">确认故事板,开始生成视频片段 <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></button>
</div>
</div>
</section>
<!-- ============= STAGE 4 · 视频片段 ============= -->
<section class="stage" data-stage-pane="4">
<div class="queue-bar">
<div>
<div style="font-size:14px; font-weight:600;">视频生成中 · 4 / 6 完成</div>
<div class="muted-2 mono" style="font-size:11px; margin-top:3px; letter-spacing:.02em;">// 每镜 Seedance 调用 ~30s · 预计还需 1 分钟</div>
</div>
<div class="bar-wrap"><span style="width:67%"></span></div>
<span class="muted mono" style="font-size:12px;">67%</span>
<button class="btn btn-sm" onclick="Shell.toast('全部重跑', 'POST /video/regen-all')">↻ 全部重跑</button>
</div>
<div class="video-grid">
<div class="video-card">
<div class="placeholder video-thumb">
<span class="ph-frame">镜 1 · 0-2s</span>
<div class="play"><div class="btn-play"><svg width="14" height="14" viewBox="0 0 16 16"><path d="M5 4l6 4-6 4z" fill="currentColor"/></svg></div></div>
</div>
<div class="body">
<div class="hstack"><strong style="font-size:13px;">镜 1 · 深夜办公桌</strong><span class="spacer"></span><span class="pill ok"><span class="dot"></span>完成</span></div>
<div class="muted-2 mono" style="font-size:11px; margin-top:4px;">2.0s · 1080×1920 · ¥0.18</div>
<div class="hstack" style="margin-top:8px;">
<button class="btn btn-ghost btn-sm">↻ 重跑</button>
<button class="btn btn-ghost btn-sm">⤓ 下载</button>
</div>
</div>
</div>
<div class="video-card">
<div class="placeholder video-thumb">
<span class="ph-frame">镜 2 · 2-5s</span>
<div class="play"><div class="btn-play"><svg width="14" height="14" viewBox="0 0 16 16"><path d="M5 4l6 4-6 4z" fill="currentColor"/></svg></div></div>
</div>
<div class="body">
<div class="hstack"><strong style="font-size:13px;">镜 2 · 面膜包装</strong><span class="spacer"></span><span class="pill ok"><span class="dot"></span>完成</span></div>
<div class="muted-2 mono" style="font-size:11px; margin-top:4px;">3.0s · 1080×1920 · ¥0.22</div>
<div class="hstack" style="margin-top:8px;">
<button class="btn btn-ghost btn-sm">↻ 重跑</button>
<button class="btn btn-ghost btn-sm">⤓ 下载</button>
</div>
</div>
</div>
<div class="video-card">
<div class="placeholder video-thumb">
<span class="ph-frame">镜 3 · 5-8s</span>
<div class="play"><div class="btn-play"><svg width="14" height="14" viewBox="0 0 16 16"><path d="M5 4l6 4-6 4z" fill="currentColor"/></svg></div></div>
</div>
<div class="body">
<div class="hstack"><strong style="font-size:13px;">镜 3 · 精华液微距</strong><span class="spacer"></span><span class="pill ok"><span class="dot"></span>完成</span></div>
<div class="muted-2 mono" style="font-size:11px; margin-top:4px;">3.0s · 1080×1920 · ¥0.22</div>
<div class="hstack" style="margin-top:8px;">
<button class="btn btn-ghost btn-sm">↻ 重跑</button>
<button class="btn btn-ghost btn-sm">⤓ 下载</button>
</div>
</div>
</div>
<div class="video-card">
<div class="placeholder video-thumb">
<span class="ph-frame">镜 4 · 8-11s</span>
<div class="play"><div class="btn-play"><svg width="14" height="14" viewBox="0 0 16 16"><path d="M5 4l6 4-6 4z" fill="currentColor"/></svg></div></div>
</div>
<div class="body">
<div class="hstack"><strong style="font-size:13px;">镜 4 · 敷面膜平躺</strong><span class="spacer"></span><span class="pill ok"><span class="dot"></span>完成</span></div>
<div class="muted-2 mono" style="font-size:11px; margin-top:4px;">3.0s · 1080×1920 · ¥0.22</div>
<div class="hstack" style="margin-top:8px;">
<button class="btn btn-ghost btn-sm">↻ 重跑</button>
<button class="btn btn-ghost btn-sm">⤓ 下载</button>
</div>
</div>
</div>
<div class="video-card">
<div class="placeholder video-thumb">
<div style="display:flex; flex-direction:column; gap:8px; align-items:center;">
<div class="spinner"></div>
<span class="ph-frame">镜 5 · 生成中 18s</span>
</div>
</div>
<div class="body">
<div class="hstack"><strong style="font-size:13px;">镜 5 · 化妆台</strong><span class="spacer"></span><span class="pill info"><span class="dot"></span>生成中</span></div>
<div class="muted-2 mono" style="font-size:11px; margin-top:4px;">2.0s · 排队中 · ~¥0.18</div>
<div class="hstack" style="margin-top:8px;">
<button class="btn btn-ghost btn-sm" disabled>↻ 重跑</button>
<button class="btn btn-ghost btn-sm" disabled>⤓ 下载</button>
</div>
</div>
</div>
<div class="video-card">
<div class="placeholder video-thumb"><span class="ph-frame">镜 6 · 排队</span></div>
<div class="body">
<div class="hstack"><strong style="font-size:13px;">镜 6 · 产品定格</strong><span class="spacer"></span><span class="pill neutral"><span class="dot"></span>排队中</span></div>
<div class="muted-2 mono" style="font-size:11px; margin-top:4px;">2.0s · 等待中 · ~¥0.18</div>
</div>
</div>
</div>
<div class="stage-foot">
<div class="info"><span class="mono">[ 已确认 ¥0.84 · 待生成 ¥0.36 · 累计 ¥2.34 ]</span></div>
<div class="hstack">
<button class="btn" onclick="location.hash='#stage-3'">← 返回故事板</button>
<button class="btn btn-primary btn-lg" disabled>2 镜生成中 · 等待</button>
</div>
</div>
</section>
<!-- ============= STAGE 5 · 拼接编辑器 ============= -->
<section class="stage" data-stage-pane="5">
<div class="editor">
<div class="editor-preview">
<div class="canvas">9:16 预览 · 1080×1920</div>
<div class="controls">
<button class="ctl-btn" title="上一帧"><svg width="14" height="14" viewBox="0 0 16 16"><path d="M3 3v10l4-5zM9 3v10l4-5z" fill="currentColor"/></svg></button>
<button class="ctl-btn" title="播放" onclick="Shell.toast('播放', '00:08.42 / 00:15.00')"><svg width="16" height="16" viewBox="0 0 16 16"><path d="M5 4l7 4-7 4z" fill="currentColor"/></svg></button>
<button class="ctl-btn" title="下一帧"><svg width="14" height="14" viewBox="0 0 16 16"><path d="M13 3v10l-4-5zM7 3v10l-4-5z" fill="currentColor"/></svg></button>
<span class="muted mono" style="font-size:12px; margin-left:8px;">00:08.42 / 00:15.00</span>
</div>
</div>
<div class="editor-props">
<div class="props-tabs">
<div class="active">字幕</div>
<div>转场</div>
<div>BGM</div>
</div>
<div class="muted mono" style="font-size:11px; font-weight:500; margin-bottom:8px; letter-spacing:.04em;">// 字幕样式</div>
<div class="style-swatch">
<div class="swatch-card selected"><div class="demo">真实分享</div><div class="nm">朴素白底</div></div>
<div class="swatch-card"><div class="demo b">真实分享</div><div class="nm">影视黑底</div></div>
<div class="swatch-card"><div class="demo c">真实分享</div><div class="nm">手写描边</div></div>
<div class="swatch-card"><div class="demo d">真实分享</div><div class="nm">综艺暖黄</div></div>
</div>
<div class="divider"></div>
<div class="muted mono" style="font-size:11px; font-weight:500; margin-bottom:8px; letter-spacing:.04em;">// 当前选中(镜 4)</div>
<div class="props-row"><span class="k">起始</span><input class="input-mini" value="00:08.00"></div>
<div class="props-row"><span class="k">时长</span><input class="input-mini" value="3.00s"></div>
<div class="props-row"><span class="k">音量</span><input class="input-mini" value="100"></div>
<div class="props-row"><span class="k">速度</span><input class="input-mini" value="1.0x"></div>
<div class="props-row"><span class="k">入场</span><span class="mono" style="font-size:11.5px;">交叉淡化</span></div>
<div class="divider"></div>
<div class="muted mono" style="font-size:11px; font-weight:500; margin-bottom:8px; letter-spacing:.04em;">// BGM</div>
<div class="props-row" style="border-bottom:0;">
<span style="font-size:12px; flex:1;">温柔治愈钢琴 · 0:42</span>
<button class="btn btn-ghost btn-sm">替换</button>
</div>
</div>
<div class="timeline">
<div class="tl-toolbar">
<button class="btn btn-ghost btn-sm"></button>
<button class="btn btn-ghost btn-sm"></button>
<span class="muted-2" style="font-size:12px;">|</span>
<button class="btn btn-ghost btn-sm">分割</button>
<button class="btn btn-ghost btn-sm">复制</button>
<button class="btn btn-ghost btn-sm">删除</button>
<span class="spacer"></span>
<span class="muted mono" style="font-size:11px;">缩放</span>
<input type="range" min="50" max="200" value="100" style="width:120px;">
</div>
<div class="tl-ruler">
<div class="l">// time</div>
<div class="ticks">
<span>0s</span><span>2s</span><span>5s</span><span>8s</span><span>11s</span><span>13s</span><span>15s</span>
</div>
</div>
<div class="tl-track">
<div class="label"><span class="dot" style="background:var(--heat);"></span>视频</div>
<div class="lane">
<div class="clip video" style="flex:2;"><span class="num">1</span> 深夜办公桌</div>
<div class="clip video" style="flex:3;"><span class="num">2</span> 面膜包装</div>
<div class="clip video" style="flex:3;"><span class="num">3</span> 精华液微距</div>
<div class="clip video selected" style="flex:3;"><span class="num">4</span> 敷面膜平躺</div>
<div class="clip video" style="flex:2;"><span class="num">5</span> 化妆台</div>
<div class="clip video" style="flex:2;"><span class="num">6</span> 产品定格</div>
</div>
</div>
<div class="tl-track">
<div class="label"><span class="dot" style="background:var(--accent-forest);"></span>字幕</div>
<div class="lane" style="position:relative;">
<div class="clip subtitle" style="flex:2;">加班三天 脸已经不能看了…</div>
<div class="clip subtitle" style="flex:3;">还好我有这个 透真玻尿酸面膜</div>
<div class="clip subtitle" style="flex:3;">30g 精华 一片顶三片</div>
<div class="clip subtitle" style="flex:3;">敷完起来脸是软的</div>
<div class="clip subtitle" style="flex:2;">化妆都能看出来</div>
<div class="clip subtitle" style="flex:2;">5 片 ¥39.9 囤起来</div>
<div class="playhead" style="left:56%;"></div>
</div>
</div>
<div class="tl-track">
<div class="label"><span class="dot" style="background:var(--accent-amethyst);"></span>BGM</div>
<div class="lane">
<div class="clip bgm" style="flex:15;">温柔治愈钢琴 · 0:42(循环 1 次,淡入淡出)</div>
</div>
</div>
</div>
</div>
<div class="stage-foot">
<div class="info"><span class="mono">[ 合成预估 ~30s · 不消耗 token ]</span></div>
<div class="hstack">
<button class="btn" onclick="location.hash='#stage-4'"><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="M19 12H5M12 19l-7-7 7-7"/></svg> 返回片段</button>
<button class="btn" onclick="Shell.toast('已保存草稿', '/projects/p3/draft')">保存草稿</button>
<button class="btn btn-primary btn-lg" onclick="Shell.toast('开始导出', 'POST /export · 1080P 9:16')">导出 MP4 · 1080P 9:16 <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 4v12m0 0l-5-5m5 5l5-5M4 20h16"/></svg></button>
</div>
</div>
</section>
</div>
<script src="assets/shell.js"></script>
<script>
Shell.render({
active: 'projects',
crumbs: [{ label: '工作台', href: 'index.html' }, { label: '视频项目', href: 'projects.html' }, { label: '补水面膜 · v3' }]
});
// hash routing
function activateStage(n) {
document.querySelectorAll('.stage').forEach(s => s.classList.remove('active'));
document.querySelector(`[data-stage-pane="${n}"]`)?.classList.add('active');
document.querySelectorAll('.stage-step').forEach(s => {
s.classList.remove('active');
if (+s.dataset.stage === +n) s.classList.add('active');
});
// update top breadcrumb fragment indicator with shell.toast
const stageNames = { 1:'脚本', 2:'基础资产', 3:'故事板', 4:'视频片段', 5:'拼接导出' };
Shell.toast('进入 Stage ' + n + ' · ' + stageNames[n], 'pipeline#stage-' + n);
}
function readHash() {
const m = location.hash.match(/stage-(\d)/);
if (m) activateStage(+m[1]);
}
window.addEventListener('hashchange', readHash);
readHash();
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,128 @@
<!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>
.product-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 16px; }
.product-card { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); cursor: pointer; transition: background .15s; }
.product-card:hover { background: var(--background-lighter); border-color: var(--black-alpha-48); }
.product-thumb { aspect-ratio: 1.4 / 1; }
.product-body { padding: 14px; }
.product-name { font-size: 14px; font-weight: 600; color: var(--accent-black); }
.product-meta { font-size: 11.5px; color: var(--black-alpha-48); margin-top: 4px; font-family: var(--font-mono); letter-spacing: .02em; }
.product-tags { display: flex; gap: 6px; margin-top: 10px; flex-wrap: wrap; }
.tag-chip { font-size: 11px; padding: 2px 9px; background: var(--background-lighter); color: var(--black-alpha-56); border: 1px solid var(--border-faint); border-radius: var(--r-sm); }
.product-card.add { border: 1px dashed var(--border-faint); border-radius: var(--r-md); background: transparent; display: grid; place-items: center; min-height: 220px; color: var(--black-alpha-56); gap: 8px; padding: 16px; }
.product-card.add:hover { border-color: var(--heat); color: var(--heat); background: var(--heat-12); }
.product-card.add .plus-ic { width: 36px; height: 36px; border: 1px solid currentColor; border-radius: var(--r-md); display: grid; place-items: center; }
.upload-zone { border: 1px dashed var(--border-faint); border-radius: var(--r-md); padding: 24px; text-align: center; background: var(--background-lighter); color: var(--black-alpha-56); font-size: 13px; }
.upload-zone strong { color: var(--heat); font-weight: 600; }
.upload-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-top: 10px; }
.upload-grid .placeholder { aspect-ratio: 1; }
.bullet-list { list-style: none; }
.bullet-list li { display: flex; gap: 8px; align-items: center; padding: 10px 12px; background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-sm); margin-bottom: 6px; font-size: 13px; }
.bullet-list .num { width: 18px; height: 18px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-sm); font-size: 11px; color: var(--black-alpha-56); display: grid; place-items: center; flex-shrink: 0; font-family: var(--font-mono); }
</style>
</head>
<body>
<div id="page">
<div class="page-head">
<div>
<h1>商品库</h1>
<div class="sub"><span class="mono">// 12 SKU</span> · 商品信息会作为脚本和资产生成的素材</div>
</div>
<div class="actions">
<a class="btn btn-primary" href="product-create.html">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 5v14M5 12h14"/></svg>
新建商品
</a>
</div>
</div>
<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" placeholder="搜索商品名称、品牌">
</div>
<button class="chip active">全部 <span class="mono" style="opacity:.7">12</span></button>
<button class="chip">美妆个护</button>
<button class="chip">数码 3C</button>
<button class="chip">食品饮料</button>
<button class="chip">服饰</button>
<span class="spacer"></span>
<button class="chip">最近添加 <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg></button>
</div>
<div class="product-grid">
<div class="product-card" onclick="Shell.toast('打开商品', '透真玻尿酸补水面膜')">
<div class="placeholder product-thumb"><span class="ph-frame">补水面膜 · 1200×800</span></div>
<div class="product-body">
<div class="product-name">透真玻尿酸补水面膜</div>
<div class="product-meta">美妆个护 · 3 张图</div>
<div class="product-tags"><span class="tag-chip">熬夜党</span><span class="tag-chip">敏感肌</span><span class="tag-chip">¥39.9</span></div>
</div>
</div>
<div class="product-card" onclick="Shell.toast('打开商品', '南卡 Lite Pro')">
<div class="placeholder product-thumb"><span class="ph-frame">蓝牙耳机 · 1200×800</span></div>
<div class="product-body">
<div class="product-name">南卡 Lite Pro 蓝牙耳机</div>
<div class="product-meta">数码 3C · 5 张图</div>
<div class="product-tags"><span class="tag-chip">通勤</span><span class="tag-chip">运动</span><span class="tag-chip">¥199</span></div>
</div>
</div>
<div class="product-card" onclick="Shell.toast('打开商品', '滋啦速食')">
<div class="placeholder product-thumb"><span class="ph-frame">速食牛肉面 · 1200×800</span></div>
<div class="product-body">
<div class="product-name">滋啦速食牛肉面 · 6 桶装</div>
<div class="product-meta">食品饮料 · 4 张图</div>
<div class="product-tags"><span class="tag-chip">加班</span><span class="tag-chip">独居</span><span class="tag-chip">¥49.9</span></div>
</div>
</div>
<div class="product-card" onclick="Shell.toast('打开商品', '透真清透防晒')">
<div class="placeholder product-thumb"><span class="ph-frame">防晒霜 · 1200×800</span></div>
<div class="product-body">
<div class="product-name">透真清透物理防晒霜</div>
<div class="product-meta">美妆个护 · 4 张图</div>
<div class="product-tags"><span class="tag-chip">SPF50</span><span class="tag-chip">通勤</span><span class="tag-chip">¥69</span></div>
</div>
</div>
<div class="product-card" onclick="Shell.toast('打开商品', '三顿半同款')">
<div class="placeholder product-thumb"><span class="ph-frame">咖啡冻干粉 · 1200×800</span></div>
<div class="product-body">
<div class="product-name">三顿半同款冻干咖啡粉</div>
<div class="product-meta">食品饮料 · 6 张图</div>
<div class="product-tags"><span class="tag-chip">提神</span><span class="tag-chip">早八</span><span class="tag-chip">¥89/24 颗</span></div>
</div>
</div>
<div class="product-card" onclick="Shell.toast('打开商品', '小熊 4L 空气炸锅')">
<div class="placeholder product-thumb"><span class="ph-frame">空气炸锅 · 1200×800</span></div>
<div class="product-body">
<div class="product-name">小熊 4L 可视空气炸锅</div>
<div class="product-meta">家电 · 5 张图</div>
<div class="product-tags"><span class="tag-chip">小户型</span><span class="tag-chip">健康</span><span class="tag-chip">¥159</span></div>
</div>
</div>
<div class="product-card" onclick="Shell.toast('打开商品', '露露同款瑜伽裤')">
<div class="placeholder product-thumb"><span class="ph-frame">瑜伽裤 · 1200×800</span></div>
<div class="product-body">
<div class="product-name">露露同款裸感瑜伽裤</div>
<div class="product-meta">服饰 · 8 张图</div>
<div class="product-tags"><span class="tag-chip">健身房</span><span class="tag-chip">通勤</span><span class="tag-chip">¥119</span></div>
</div>
</div>
<a class="product-card add" href="product-create.html">
<div class="plus-ic"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 5v14M5 12h14"/></svg></div>
<div style="font-size:13px; font-weight:500;">新建商品</div>
</a>
</div>
</div>
<script src="assets/shell.js"></script>
<script>Shell.render({ active: 'products', crumbs: [{ label: '工作台', href: 'index.html' }, { label: '商品库' }] });</script>
</body>
</html>

View File

@ -0,0 +1,287 @@
<!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>
.wizard { display: grid; grid-template-columns: 200px minmax(0, 1fr) 300px; gap: 36px; align-items: start; max-width: 1400px; }
@media (max-width: 1180px) { .wizard { grid-template-columns: 200px minmax(0, 1fr); } .wiz-preview { display: none; } }
.steps { position: sticky; top: 24px; align-self: start; }
.step { display: flex; gap: 12px; padding: 12px 0; position: relative; }
.step:not(:last-child)::after { content: ''; position: absolute; left: 11px; top: 36px; width: 1px; height: calc(100% - 24px); background: var(--border-faint); }
.step .num { width: 24px; height: 24px; border: 1px solid var(--border-faint); border-radius: var(--r-sm); background: var(--surface); display: grid; place-items: center; font-size: 11px; font-weight: 600; color: var(--black-alpha-48); flex-shrink: 0; z-index: 1; font-family: var(--font-mono); }
.step.done .num { background: var(--accent-black); border-color: var(--accent-black); color: var(--accent-white); }
.step.active .num { background: var(--heat); border-color: var(--heat); color: var(--accent-white); }
.step .label { font-size: 13.5px; font-weight: 500; color: var(--black-alpha-56); padding-top: 2px; }
.step .desc { font-size: 11.5px; color: var(--black-alpha-48); padding-top: 3px; line-height: 1.4; font-family: var(--font-mono); letter-spacing: .02em; }
.step.active .label { color: var(--accent-black); font-weight: 600; }
.step.done .label { color: var(--black-alpha-56); }
.step.done:not(:last-child)::after { background: var(--accent-black); }
.wiz-pane { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 22px 24px; margin-bottom: 14px; }
.wiz-pane.active { padding: 26px 28px; position: relative; }
.wiz-pane.active::before, .wiz-pane.active::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; }
.wiz-pane.active::before { top: -7px; left: -7px; }
.wiz-pane.active::after { bottom: -7px; right: -7px; }
.wiz-pane.collapsed { padding: 16px 20px; }
.wiz-pane-h { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; }
.wiz-pane-h h3 { font-size: 14px; font-weight: 600; }
.wiz-step-h h2 { font-size: 20px; font-weight: 600; letter-spacing: -.015em; }
.wiz-step-h p { font-size: 13px; color: var(--black-alpha-56); margin-top: 6px; }
.opt-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
.opt-row.cols-4 { grid-template-columns: repeat(4, 1fr); }
.opt-row.cols-6 { grid-template-columns: repeat(3, 1fr); }
@media (min-width: 1280px) { .opt-row.cols-6 { grid-template-columns: repeat(6, 1fr); } }
.opt-card { border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 14px; background: var(--surface); cursor: pointer; position: relative; display: flex; flex-direction: column; min-width: 0; }
.opt-card:hover { background: var(--background-lighter); }
.opt-card.selected { border-color: var(--heat); background: var(--heat-12); }
.opt-card.selected::after { content: ''; position: absolute; top: 8px; right: 10px; width: 16px; height: 16px; background-color: var(--heat); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: center; background-size: 10px 10px; border-radius: var(--r-sm, 4px); }
.opt-card h4 { font-size: 13px; font-weight: 600; }
.opt-card .sub { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); margin-top: 3px; letter-spacing: .02em; }
.opt-card .note { font-size: 11.5px; color: var(--black-alpha-56); margin-top: 6px; line-height: 1.5; }
.opt-card .metric { margin-top: auto; padding-top: 10px; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; }
.opt-card .metric .val { color: var(--accent-black); font-weight: 500; }
.opt-card.selected .metric .val { color: var(--heat); }
.opt-card .badge { font-family: var(--font-mono); font-size: 9.5px; padding: 1px 6px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-sm); color: var(--black-alpha-48); display: inline-block; margin-top: 8px; letter-spacing: .04em; align-self: flex-start; }
.opt-card.selected .badge { color: var(--heat); border-color: var(--heat-20); }
.theme-pill { display: inline-flex; gap: 4px; height: 28px; align-items: center; padding: 0 12px; border: 1px solid var(--border-faint); border-radius: 999px; background: var(--surface); font-size: 12.5px; cursor: pointer; color: var(--black-alpha-56); }
.theme-pill:hover { background: var(--background-lighter); }
.theme-pill.active { background: var(--heat-12); color: var(--heat); border-color: var(--heat-20); font-weight: 600; }
.reco-bubble { position: relative; margin-top: 10px; padding: 10px 14px; background: var(--heat-12); border: 1px solid var(--heat-20); border-radius: var(--r-md); display: flex; align-items: center; gap: 12px; font-size: 12.5px; color: var(--accent-black); }
.reco-bubble::before { content: ''; position: absolute; top: -5px; left: 28px; width: 9px; height: 9px; background: var(--heat-12); border-left: 1px solid var(--heat-20); border-top: 1px solid var(--heat-20); transform: rotate(45deg); }
.reco-bubble .ic { color: var(--heat); flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px; }
.reco-bubble .ic svg, .reco-bubble .dismiss svg { display: block; }
.reco-bubble .txt { flex: 1; line-height: 1.5; }
.reco-bubble .txt strong { color: var(--heat); font-weight: 600; }
.reco-bubble .txt .meta { display: block; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); margin-top: 2px; letter-spacing: .02em; }
.reco-bubble .btn-apply { height: 28px; padding: 0 12px; background: var(--heat); color: var(--accent-white); border: 1px solid var(--heat); border-radius: var(--r-md); font-size: 12px; font-weight: 600; cursor: pointer; flex-shrink: 0; box-shadow: var(--shadow-cta); transition: box-shadow var(--t-base); }
.reco-bubble .btn-apply:hover { box-shadow: var(--shadow-cta-hover); }
.reco-bubble .dismiss { background: transparent; color: var(--black-alpha-48); border: 0; width: 24px; height: 24px; padding: 0; display: inline-flex; align-items: center; justify-content: center; cursor: pointer; }
.reco-bubble .dismiss:hover { color: var(--accent-black); }
.wiz-foot { display: flex; justify-content: space-between; align-items: center; margin-top: 18px; padding-top: 18px; border-top: 1px solid var(--border-faint); }
/* preview panel */
.wiz-preview { position: sticky; top: 24px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 18px; }
.wiz-preview::before, .wiz-preview::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; }
.wiz-preview::before { top: -7px; left: -7px; }
.wiz-preview::after { bottom: -7px; right: -7px; }
.pv-h { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .08em; margin-bottom: 12px; text-transform: uppercase; display: flex; justify-content: space-between; }
.pv-h .live { display: inline-flex; align-items: center; gap: 5px; color: var(--heat); }
.pv-h .live::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: var(--heat); animation: pulse 1.6s ease-in-out infinite; }
@keyframes pulse { 0%, 100% { opacity: 1 } 50% { opacity: .35 } }
.pv-title { font-size: 14px; font-weight: 600; line-height: 1.3; margin-bottom: 14px; }
.pv-metrics { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: var(--border-faint); border: 1px solid var(--border-faint); margin-bottom: 14px; }
.pv-metric { padding: 10px 12px; background: var(--surface); }
.pv-metric .l { font-family: var(--font-mono); font-size: 9.5px; color: var(--black-alpha-48); letter-spacing: .04em; text-transform: uppercase; }
.pv-metric .v { font-size: 18px; font-weight: 600; margin-top: 3px; font-variant-numeric: tabular-nums; color: var(--accent-black); }
.pv-metric .v small { font-size: 11px; color: var(--black-alpha-48); font-weight: 500; }
.pv-metric.accent .v { color: var(--heat); }
.pv-section { margin-top: 14px; }
.pv-section .lbl { font-family: var(--font-mono); font-size: 9.5px; color: var(--black-alpha-48); letter-spacing: .06em; text-transform: uppercase; margin-bottom: 8px; }
.pv-flow { display: flex; flex-wrap: wrap; gap: 4px 0; font-size: 11.5px; color: var(--black-alpha-56); align-items: center; line-height: 1.7; }
.pv-flow .node { padding: 2px 7px; background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-sm); color: var(--accent-black); font-weight: 500; }
.pv-flow .arrow { color: var(--heat); margin: 0 5px; display: inline-flex; align-items: center; }
.pv-flow .arrow svg { display: block; }
.pv-list { list-style: none; padding: 0; margin: 0; }
.pv-list li { font-size: 11.5px; color: var(--black-alpha-56); padding: 4px 0; display: flex; align-items: center; gap: 6px; }
.pv-list li::before { content: ''; width: 11px; height: 11px; flex-shrink: 0; background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23FA5D19' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 12l5 5L20 6'/%3E%3C/svg%3E") no-repeat center; background-size: contain; }
.pv-foot { margin-top: 14px; padding-top: 12px; border-top: 1px dashed var(--border-faint); font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); display: flex; justify-content: space-between; }
.pv-foot strong { color: var(--accent-black); font-weight: 500; }
</style>
</head>
<body>
<div id="page">
<div class="page-head">
<div>
<h1>新建项目</h1>
<div class="sub"><span class="mono">// 商品 → 脚本来源 → 配置 → 确认 · 4 步开始生成</span></div>
</div>
<div class="actions">
<a class="btn btn-ghost" href="projects.html">退出</a>
</div>
</div>
<div class="wizard">
<nav class="steps">
<div class="step done"><div class="num"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l5 5L20 6"/></svg></div><div><div class="label">选择商品</div><div class="desc">透真补水面膜</div></div></div>
<div class="step done"><div class="num"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l5 5L20 6"/></svg></div><div><div class="label">脚本来源</div><div class="desc">AI 全生 · 痛点种草</div></div></div>
<div class="step active"><div class="num">3</div><div><div class="label">项目配置</div><div class="desc">名称 · 时长 · 风格</div></div></div>
<div class="step"><div class="num">4</div><div><div class="label">确认与计费</div><div class="desc">预估 ¥3.20</div></div></div>
</nav>
<div>
<div class="wiz-pane collapsed">
<div class="wiz-pane-h">
<h3>第 1 步 · 选择商品</h3>
<span class="spacer"></span>
<button class="btn btn-ghost btn-sm">修改</button>
</div>
<div class="hstack" style="gap:12px;">
<div class="placeholder" style="width:44px;height:56px;"><span class="ph-frame">主图</span></div>
<div>
<div style="font-weight:600; font-size:13.5px;">透真玻尿酸补水面膜</div>
<div class="muted-2 mono" style="font-size:11.5px; margin-top:3px; letter-spacing:.02em;">美妆个护 · ¥39.9 · 3 张图 · 3 个卖点</div>
</div>
</div>
</div>
<div class="wiz-pane collapsed">
<div class="wiz-pane-h">
<h3>第 2 步 · 脚本来源</h3>
<span class="spacer"></span>
<button class="btn btn-ghost btn-sm">修改</button>
</div>
<div class="hstack">
<span class="pill info"><span class="dot"></span>AI 全生</span>
<span class="muted">主题:</span>
<span style="font-size:13px;">熬夜党的急救面膜 · 痛点种草向</span>
</div>
</div>
<div class="wiz-pane active">
<div class="wiz-step-h" style="margin-bottom:18px;">
<h2>第 3 步 · 项目配置</h2>
<p>这些设置会影响 LLM 生成脚本的方向,确认后会进入流水线第 1 步(脚本生成)。</p>
</div>
<div class="field">
<label class="field-label">项目名称<span class="req">*</span></label>
<input class="input" value="补水面膜 · 痛点种草 · v3">
</div>
<div class="field">
<label class="field-label">视频时长<span class="req">*</span></label>
<div class="opt-row cols-4">
<div class="opt-card"><h4>0-10 秒</h4><div class="sub">3-4 镜</div><div class="note">黄金完播</div><div class="metric">完播 <span class="val">52%</span></div></div>
<div class="opt-card selected"><h4>0-15 秒</h4><div class="sub">4-5 镜</div><div class="note">完播率最佳</div><div class="metric">完播 <span class="val">42%</span></div></div>
<div class="opt-card"><h4>0-30 秒</h4><div class="sub">6-8 镜</div><div class="note">卖点详解</div><div class="metric">完播 <span class="val">32%</span></div></div>
<div class="opt-card"><h4>0-60 秒</h4><div class="sub">10-12 镜</div><div class="note">故事化</div><div class="metric">完播 <span class="val">26%</span></div></div>
</div>
<div class="field-hint">数据来源:抖音同品类 TOP 视频均值 · 实际镜头数由 LLM 决定</div>
</div>
<div class="field">
<label class="field-label">脚本风格</label>
<div class="opt-row">
<div class="opt-card selected">
<h4>痛点种草</h4>
<div class="note">用户痛点切入,以「我懂你」的口吻引出产品。</div>
<span class="badge">最常用</span>
</div>
<div class="opt-card">
<h4>开箱测评</h4>
<div class="note">朋友式分享,从开箱到使用感受娓娓道来。</div>
</div>
<div class="opt-card">
<h4>对比展示</h4>
<div class="note">「用前 vs 用后 / 同类 vs 本品」直观呈现。</div>
</div>
</div>
</div>
<div class="field">
<label class="field-label">人物设定</label>
<div class="opt-row cols-6">
<div class="opt-card"><h4>都市白领女性</h4><div class="sub">25-30 岁</div><div class="metric"><span class="val">大盘消费力</span></div></div>
<div class="opt-card"><h4>闺蜜种草</h4><div class="sub">邻家女孩</div><div class="metric"><span class="val">复购最高</span></div></div>
<div class="opt-card"><h4>总裁亲选</h4><div class="sub">创始人 IP</div><div class="metric"><span class="val">30 万销额</span></div></div>
<div class="opt-card"><h4>专业测评师</h4><div class="sub">垂类达人</div><div class="metric"><span class="val">互动 +30%</span></div></div>
<div class="opt-card"><h4>实用宝妈</h4><div class="sub">家庭决策者</div><div class="metric"><span class="val">母婴/家清</span></div></div>
<div class="opt-card selected"><h4>学生党</h4><div class="sub">Z 世代 18-24</div><div class="metric"><span class="val">平价快消</span></div></div>
</div>
<div class="reco-bubble">
<span class="ic">
<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="M9 18h6"/><path d="M10 22h4"/><path d="M15.09 14c.18-.98.65-1.74 1.41-2.5A4.65 4.65 0 0 0 18 8 6 6 0 0 0 6 8c0 1 .23 2.23 1.5 3.5A4.61 4.61 0 0 1 8.91 14"/></svg>
</span>
<div class="txt">
<span>抖音同人设 TOP 视频更常用 <strong>0-10 秒</strong> + <strong>对比展示</strong></span>
<span class="meta">当前 0-15 秒 · 痛点种草 → 推荐换为学生党最优组合</span>
</div>
<button class="btn-apply">一键套用</button>
<button class="dismiss" aria-label="忽略">
<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 5l14 14M19 5L5 19"/></svg>
</button>
</div>
</div>
<div class="field">
<label class="field-label">关键卖点(可勾选要重点突出的)</label>
<div style="display:flex; gap:6px; flex-wrap:wrap;">
<span class="theme-pill active"><svg width="12" height="12" 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> 透明质酸 + B5</span>
<span class="theme-pill active"><svg width="12" height="12" 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> 30g 大容量精华</span>
<span class="theme-pill">+ 0 香精 0 酒精</span>
</div>
</div>
</div>
<div class="wiz-foot">
<button class="btn">← 上一步</button>
<div class="hstack">
<span class="muted-2 mono" style="font-size:11.5px; letter-spacing:.02em;">// 下一步:确认与计费</span>
<a class="btn btn-primary btn-lg" href="pipeline.html#stage-1" onclick="Shell.toast('开始生成项目', 'pipeline#stage-1')">下一步 <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>
</div>
</div>
</div>
<!-- ── Live preview ── -->
<aside class="wiz-preview">
<div class="pv-h">
<span>实时预估</span>
<span class="live">LIVE</span>
</div>
<div class="pv-title">补水面膜 · 痛点种草 · v3</div>
<div class="pv-metrics">
<div class="pv-metric"><div class="l">镜头</div><div class="v">4-5<small></small></div></div>
<div class="pv-metric accent"><div class="l">预估完播</div><div class="v">42<small>%</small></div></div>
<div class="pv-metric"><div class="l">预估转化</div><div class="v">1.8<small>%</small></div></div>
<div class="pv-metric"><div class="l">预估成本</div><div class="v">¥3.20</div></div>
</div>
<div class="pv-section">
<div class="lbl">人设 · 风格</div>
<ul class="pv-list">
<li>学生党 · Z 世代 18-24</li>
<li>痛点种草 · 完播率最佳</li>
</ul>
</div>
<div class="pv-section">
<div class="lbl">脚本走向</div>
<div class="pv-flow">
<span style="display:inline-flex; align-items:center;"><span class="node">痛点</span><span class="arrow"><svg width="12" height="12" 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></span></span>
<span style="display:inline-flex; align-items:center;"><span class="node">共鸣</span><span class="arrow"><svg width="12" height="12" 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></span></span>
<span style="display:inline-flex; align-items:center;"><span class="node">产品</span><span class="arrow"><svg width="12" height="12" 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></span></span>
<span style="display:inline-flex; align-items:center;"><span class="node">效果</span><span class="arrow"><svg width="12" height="12" 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></span></span>
<span class="node">引导</span>
</div>
</div>
<div class="pv-section">
<div class="lbl">突出卖点</div>
<ul class="pv-list">
<li>透明质酸 + B5</li>
<li>30g 大容量精华</li>
</ul>
</div>
<div class="pv-foot">
<span>v3 · Restraint</span>
<strong>就绪</strong>
</div>
</aside>
</div>
</div>
<script src="assets/shell.js"></script>
<script>Shell.render({ active: 'projects', crumbs: [{ label: '工作台', href: 'index.html' }, { label: '视频项目', href: 'projects.html' }, { label: '新建项目' }] });</script>
</body>
</html>

View File

@ -0,0 +1,523 @@
<!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>
/* ─── List view ─── */
.proj-name-cell { display: flex; align-items: center; gap: 12px; }
.proj-thumb { width: 40px; height: 52px; flex-shrink: 0; border-radius: var(--r-md); }
.proj-name { font-weight: 600; color: var(--accent-black); font-size: 13.5px; }
.proj-sub { font-size: 11.5px; color: var(--black-alpha-48); margin-top: 3px; font-family: var(--font-mono); letter-spacing: .02em; }
.row-action { display: flex; gap: 4px; visibility: hidden; }
table.t tbody tr:hover .row-action { visibility: visible; }
.row-action a { width: 28px; height: 28px; display: grid; place-items: center; color: var(--black-alpha-56); border-radius: var(--r-md); }
.row-action a:hover { background: var(--surface); color: var(--heat); border: 1px solid var(--border-faint); }
/* ─── View toggle ─── */
.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-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; }
/* ─── Grid view ─── */
.proj-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 16px; }
.proj-card { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); cursor: pointer; transition: background .15s; display: flex; flex-direction: column; }
.proj-card:hover { background: var(--background-lighter); border-color: var(--black-alpha-48); }
.proj-card .card-thumb { aspect-ratio: 9/16; max-height: 280px; border-radius: var(--r-md) var(--r-md) 0 0; }
.proj-card .card-body { padding: 14px; display: flex; flex-direction: column; gap: 10px; flex: 1; }
.proj-card .card-name { font-size: 13.5px; font-weight: 600; color: var(--accent-black); line-height: 1.4; }
.proj-card .card-sub { font-size: 11.5px; color: var(--black-alpha-48); font-family: var(--font-mono); letter-spacing: .02em; }
.proj-card .card-foot { display: flex; align-items: center; justify-content: space-between; padding-top: 10px; border-top: 1px solid var(--border-faint); margin-top: auto; }
.proj-card .card-time { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; }
/* ─── Empty state ─── */
.empty-state {
background: var(--surface);
border: 1px dashed var(--border-faint);
border-radius: var(--r-md);
padding: 80px 40px;
text-align: center;
color: var(--black-alpha-56);
display: none;
}
.empty-state.show { display: block; }
.empty-state .ic-empty {
width: 48px; height: 48px;
margin: 0 auto 14px;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
display: grid; place-items: center;
color: var(--black-alpha-48);
}
.empty-state h3 { font-size: 14px; font-weight: 600; color: var(--accent-black); margin-bottom: 6px; }
.empty-state p { font-size: 12.5px; color: var(--black-alpha-48); font-family: var(--font-mono); letter-spacing: .02em; }
/* ─── Result count ─── */
.result-meta {
font-family: var(--font-mono);
font-size: 11px;
color: var(--black-alpha-48);
margin-bottom: 14px;
letter-spacing: .04em;
}
.result-meta .count { color: var(--heat); font-weight: 600; }
</style>
</head>
<body>
<div id="page">
<div class="page-head">
<div>
<h1>视频项目</h1>
<div class="sub"><span class="mono">// 12 个 · 3 进行中 · 8 完成 · 1 失败</span></div>
</div>
<div class="actions">
<a class="btn btn-primary btn-lg" href="projects-new.html">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 5v14M5 12h14"/></svg>
新建项目
</a>
</div>
</div>
<div class="tabs" id="status-tabs">
<div class="tab active" data-filter="all">全部 <span class="count">12</span></div>
<div class="tab" data-filter="wip">进行中 <span class="count">3</span></div>
<div class="tab" data-filter="done">已完成 <span class="count">8</span></div>
<div class="tab" data-filter="fail">失败 <span class="count">1</span></div>
</div>
<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="search-input" placeholder="搜索项目名称、商品">
</div>
<button class="chip" onclick="Shell.toast('商品筛选', '/filter/product')">商品 <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg></button>
<button class="chip" onclick="Shell.toast('脚本来源筛选', '/filter/source')">脚本来源 <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg></button>
<button class="chip" onclick="Shell.toast('时间筛选', '/filter/date')">创建时间 <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg></button>
<span class="spacer"></span>
<div class="view-toggle">
<button id="view-grid" 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 id="view-list" 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="result-meta">// 显示 <span class="count">12</span> / 12 个项目</div>
<!-- ============= LIST VIEW ============= -->
<div id="list-view">
<table class="t">
<thead>
<tr>
<th style="width:32%">项目</th>
<th>商品</th>
<th>脚本来源</th>
<th style="width:200px">进度</th>
<th>状态</th>
<th style="width:120px">更新于</th>
<th style="width:60px"></th>
</tr>
</thead>
<tbody id="list-tbody">
<tr data-status="wip" data-name="补水面膜 痛点种草" onclick="location.href='pipeline.html#stage-3'">
<td>
<div class="proj-name-cell">
<div class="placeholder proj-thumb"><span class="ph-frame">9:16</span></div>
<div><div class="proj-name">补水面膜 · 痛点种草 · v3</div><div class="proj-sub">6 镜 · 0-15s</div></div>
</div>
</td>
<td>透真补水面膜</td>
<td><span class="muted">AI 全生</span></td>
<td>
<div class="hstack">
<div class="prog"><span class="done"></span><span class="done"></span><span class="cur"></span><span></span><span></span></div>
<span class="muted-2 mono" style="font-size:11px;">3/5</span>
</div>
</td>
<td><span class="pill info"><span class="dot"></span>故事板 待确认</span></td>
<td class="muted-2">12 分钟前</td>
<td>
<div class="row-action">
<a href="pipeline.html#stage-3" onclick="event.stopPropagation()" title="继续"><svg width="14" height="14" viewBox="0 0 16 16"><path d="M5 4l6 4-6 4z" fill="currentColor"/></svg></a>
<a onclick="event.stopPropagation();Shell.toast('更多操作')"><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></a>
</div>
</td>
</tr>
<tr data-status="wip" data-name="速食牛肉面 加班治愈" onclick="location.href='pipeline.html#stage-2'">
<td>
<div class="proj-name-cell">
<div class="placeholder proj-thumb"><span class="ph-frame">9:16</span></div>
<div><div class="proj-name">速食牛肉面 · 加班治愈</div><div class="proj-sub">4 镜 · 0-12s</div></div>
</div>
</td>
<td>滋啦速食 · 6 桶装</td>
<td><span class="muted">一句话主题</span></td>
<td>
<div class="hstack">
<div class="prog"><span class="done"></span><span class="cur"></span><span></span><span></span><span></span></div>
<span class="muted-2 mono" style="font-size:11px;">2/5</span>
</div>
</td>
<td><span class="pill info"><span class="dot"></span>资产生成中</span></td>
<td class="muted-2">37 分钟前</td>
<td></td>
</tr>
<tr data-status="wip" data-name="透真防晒 通勤对比" onclick="location.href='pipeline.html#stage-4'">
<td>
<div class="proj-name-cell">
<div class="placeholder proj-thumb"><span class="ph-frame">9:16</span></div>
<div><div class="proj-name">透真防晒 · 通勤对比</div><div class="proj-sub">6 镜 · 0-18s</div></div>
</div>
</td>
<td>透真清透防晒霜</td>
<td><span class="muted">AI 全生</span></td>
<td>
<div class="hstack">
<div class="prog"><span class="done"></span><span class="done"></span><span class="done"></span><span class="cur"></span><span></span></div>
<span class="muted-2 mono" style="font-size:11px;">4/5</span>
</div>
</td>
<td><span class="pill info"><span class="dot"></span>视频生成 4/6</span></td>
<td class="muted-2">2 小时前</td>
<td></td>
</tr>
<tr data-status="fail" data-name="咖啡冻干 早八剧情" onclick="location.href='pipeline.html#stage-3'">
<td>
<div class="proj-name-cell">
<div class="placeholder proj-thumb"><span class="ph-frame">9:16</span></div>
<div><div class="proj-name">咖啡冻干 · 早八剧情</div><div class="proj-sub">5 镜 · 0-15s</div></div>
</div>
</td>
<td>三顿半同款冻干</td>
<td><span class="muted">一句话主题</span></td>
<td>
<div class="hstack">
<div class="prog"><span class="done"></span><span class="done"></span><span class="fail"></span><span></span><span></span></div>
<span class="muted-2 mono" style="font-size:11px;">3/5</span>
</div>
</td>
<td><span class="pill err"><span class="dot"></span>故事板生成失败</span></td>
<td class="muted-2">昨天 18:42</td>
<td></td>
</tr>
<tr data-status="done" data-name="蓝牙耳机 开箱测评" onclick="location.href='pipeline.html#stage-5'">
<td>
<div class="proj-name-cell">
<div class="placeholder proj-thumb"><span class="ph-frame">9:16</span></div>
<div><div class="proj-name">蓝牙耳机 · 开箱测评</div><div class="proj-sub">5 镜 · 0-15s</div></div>
</div>
</td>
<td>南卡 Lite Pro</td>
<td><span class="muted">自带脚本</span></td>
<td>
<div class="hstack">
<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="muted-2 mono" style="font-size:11px;">5/5</span>
</div>
</td>
<td><span class="pill ok"><span class="dot"></span>已完成</span></td>
<td class="muted-2">5 月 7 日</td>
<td></td>
</tr>
<tr data-status="done" data-name="瑜伽裤 通勤穿搭" onclick="location.href='pipeline.html#stage-5'">
<td>
<div class="proj-name-cell">
<div class="placeholder proj-thumb"><span class="ph-frame">9:16</span></div>
<div><div class="proj-name">瑜伽裤 · 通勤穿搭</div><div class="proj-sub">5 镜 · 0-15s</div></div>
</div>
</td>
<td>露露同款瑜伽裤</td>
<td><span class="muted">AI 全生</span></td>
<td>
<div class="hstack">
<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="muted-2 mono" style="font-size:11px;">5/5</span>
</div>
</td>
<td><span class="pill ok"><span class="dot"></span>已完成</span></td>
<td class="muted-2">5 月 6 日</td>
<td></td>
</tr>
<tr data-status="done" data-name="空气炸锅 小户型" onclick="location.href='pipeline.html#stage-5'">
<td>
<div class="proj-name-cell">
<div class="placeholder proj-thumb"><span class="ph-frame">9:16</span></div>
<div><div class="proj-name">空气炸锅 · 小户型</div><div class="proj-sub">4 镜 · 0-12s</div></div>
</div>
</td>
<td>小熊 4L 空气炸锅</td>
<td><span class="muted">一句话主题</span></td>
<td>
<div class="hstack">
<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="muted-2 mono" style="font-size:11px;">5/5</span>
</div>
</td>
<td><span class="pill ok"><span class="dot"></span>已完成</span></td>
<td class="muted-2">5 月 4 日</td>
<td></td>
</tr>
<tr data-status="archived" data-name="补水面膜 痛点种草 v1" onclick="location.href='pipeline.html#stage-5'">
<td>
<div class="proj-name-cell">
<div class="placeholder proj-thumb"><span class="ph-frame">9:16</span></div>
<div><div class="proj-name">补水面膜 · 痛点种草 · v1</div><div class="proj-sub">6 镜 · 0-15s</div></div>
</div>
</td>
<td>透真补水面膜</td>
<td><span class="muted">AI 全生</span></td>
<td>
<div class="hstack">
<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="muted-2 mono" style="font-size:11px;">5/5</span>
</div>
</td>
<td><span class="pill neutral"><span class="dot"></span>已归档</span></td>
<td class="muted-2">4 月 28 日</td>
<td></td>
</tr>
</tbody>
</table>
</div>
<!-- ============= GRID VIEW ============= -->
<div id="grid-view" style="display:none;">
<div class="proj-grid" id="grid-body">
<div class="proj-card" data-status="wip" data-name="补水面膜 痛点种草" onclick="location.href='pipeline.html#stage-3'">
<div class="placeholder card-thumb"><span class="ph-frame">9:16 · 镜 3/6</span></div>
<div class="card-body">
<div>
<div class="card-name">补水面膜 · 痛点种草 · v3</div>
<div class="card-sub" style="margin-top:4px;">透真补水面膜 · 6 镜</div>
</div>
<div class="hstack">
<div class="prog"><span class="done"></span><span class="done"></span><span class="cur"></span><span></span><span></span></div>
<span class="muted-2 mono" style="font-size:10.5px;">3/5</span>
</div>
<div class="card-foot">
<span class="pill info"><span class="dot"></span>故事板 待确认</span>
<span class="card-time">12 分钟前</span>
</div>
</div>
</div>
<div class="proj-card" data-status="wip" data-name="速食牛肉面 加班治愈" onclick="location.href='pipeline.html#stage-2'">
<div class="placeholder card-thumb"><span class="ph-frame">9:16 · 镜 2/4</span></div>
<div class="card-body">
<div>
<div class="card-name">速食牛肉面 · 加班治愈</div>
<div class="card-sub" style="margin-top:4px;">滋啦速食 · 4 镜</div>
</div>
<div class="hstack">
<div class="prog"><span class="done"></span><span class="cur"></span><span></span><span></span><span></span></div>
<span class="muted-2 mono" style="font-size:10.5px;">2/5</span>
</div>
<div class="card-foot">
<span class="pill info"><span class="dot"></span>资产生成中</span>
<span class="card-time">37 分钟前</span>
</div>
</div>
</div>
<div class="proj-card" data-status="wip" data-name="透真防晒 通勤对比" onclick="location.href='pipeline.html#stage-4'">
<div class="placeholder card-thumb"><span class="ph-frame">9:16 · 镜 4/6</span></div>
<div class="card-body">
<div>
<div class="card-name">透真防晒 · 通勤对比</div>
<div class="card-sub" style="margin-top:4px;">透真清透防晒霜 · 6 镜</div>
</div>
<div class="hstack">
<div class="prog"><span class="done"></span><span class="done"></span><span class="done"></span><span class="cur"></span><span></span></div>
<span class="muted-2 mono" style="font-size:10.5px;">4/5</span>
</div>
<div class="card-foot">
<span class="pill info"><span class="dot"></span>视频 4/6</span>
<span class="card-time">2 小时前</span>
</div>
</div>
</div>
<div class="proj-card" data-status="fail" data-name="咖啡冻干 早八剧情" onclick="location.href='pipeline.html#stage-3'">
<div class="placeholder card-thumb"><span class="ph-frame">9:16 · 镜 3/5</span></div>
<div class="card-body">
<div>
<div class="card-name">咖啡冻干 · 早八剧情</div>
<div class="card-sub" style="margin-top:4px;">三顿半同款 · 5 镜</div>
</div>
<div class="hstack">
<div class="prog"><span class="done"></span><span class="done"></span><span class="fail"></span><span></span><span></span></div>
<span class="muted-2 mono" style="font-size:10.5px;">3/5</span>
</div>
<div class="card-foot">
<span class="pill err"><span class="dot"></span>故事板失败</span>
<span class="card-time">昨天 18:42</span>
</div>
</div>
</div>
<div class="proj-card" data-status="done" data-name="蓝牙耳机 开箱测评" onclick="location.href='pipeline.html#stage-5'">
<div class="placeholder card-thumb"><span class="ph-frame">9:16 · 5/5 ✓</span></div>
<div class="card-body">
<div>
<div class="card-name">蓝牙耳机 · 开箱测评</div>
<div class="card-sub" style="margin-top:4px;">南卡 Lite Pro · 5 镜</div>
</div>
<div class="hstack">
<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="muted-2 mono" style="font-size:10.5px;">5/5</span>
</div>
<div class="card-foot">
<span class="pill ok"><span class="dot"></span>已完成</span>
<span class="card-time">5 月 7 日</span>
</div>
</div>
</div>
<div class="proj-card" data-status="done" data-name="瑜伽裤 通勤穿搭" onclick="location.href='pipeline.html#stage-5'">
<div class="placeholder card-thumb"><span class="ph-frame">9:16 · 5/5 ✓</span></div>
<div class="card-body">
<div>
<div class="card-name">瑜伽裤 · 通勤穿搭</div>
<div class="card-sub" style="margin-top:4px;">露露同款 · 5 镜</div>
</div>
<div class="hstack">
<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="muted-2 mono" style="font-size:10.5px;">5/5</span>
</div>
<div class="card-foot">
<span class="pill ok"><span class="dot"></span>已完成</span>
<span class="card-time">5 月 6 日</span>
</div>
</div>
</div>
<div class="proj-card" data-status="done" data-name="空气炸锅 小户型" onclick="location.href='pipeline.html#stage-5'">
<div class="placeholder card-thumb"><span class="ph-frame">9:16 · 5/5 ✓</span></div>
<div class="card-body">
<div>
<div class="card-name">空气炸锅 · 小户型</div>
<div class="card-sub" style="margin-top:4px;">小熊 4L · 4 镜</div>
</div>
<div class="hstack">
<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="muted-2 mono" style="font-size:10.5px;">5/5</span>
</div>
<div class="card-foot">
<span class="pill ok"><span class="dot"></span>已完成</span>
<span class="card-time">5 月 4 日</span>
</div>
</div>
</div>
<div class="proj-card" data-status="archived" data-name="补水面膜 痛点种草 v1" onclick="location.href='pipeline.html#stage-5'">
<div class="placeholder card-thumb"><span class="ph-frame">9:16 · 5/5 ✓</span></div>
<div class="card-body">
<div>
<div class="card-name">补水面膜 · 痛点种草 · v1</div>
<div class="card-sub" style="margin-top:4px;">透真补水面膜 · 6 镜</div>
</div>
<div class="hstack">
<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="muted-2 mono" style="font-size:10.5px;">5/5</span>
</div>
<div class="card-foot">
<span class="pill neutral"><span class="dot"></span>已归档</span>
<span class="card-time">4 月 28 日</span>
</div>
</div>
</div>
</div>
</div>
<!-- Empty state -->
<div class="empty-state" id="empty">
<div class="ic-empty">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
</div>
<h3>没有匹配的项目</h3>
<p>// 试试切换 tab 或修改搜索词</p>
</div>
</div>
<script src="assets/shell.js"></script>
<script>
Shell.render({ active: 'projects', crumbs: [{ label: '工作台', href: 'index.html' }, { label: '视频项目' }] });
// ============== Tab + search filter + view toggle ==============
const state = { filter: 'all', view: 'list', search: '' };
const TOTAL = 8; // 实际渲染的样本数
function applyFilter() {
const isList = state.view === 'list';
document.getElementById('list-view').style.display = isList ? '' : 'none';
document.getElementById('grid-view').style.display = isList ? 'none' : '';
const items = document.querySelectorAll(isList ? '#list-tbody tr' : '.proj-card');
let visible = 0;
items.forEach(el => {
const status = el.dataset.status || '';
const name = (el.dataset.name || '').toLowerCase();
const matchFilter = state.filter === 'all' || status.split(' ').includes(state.filter);
const matchSearch = !state.search || name.includes(state.search.toLowerCase());
const show = matchFilter && matchSearch;
el.style.display = show ? '' : 'none';
if (show) visible++;
});
// empty state
const empty = document.getElementById('empty');
if (visible === 0) {
empty.classList.add('show');
document.getElementById('list-view').style.display = 'none';
document.getElementById('grid-view').style.display = 'none';
} else {
empty.classList.remove('show');
}
// result meta
document.getElementById('result-meta').innerHTML = `// 显示 <span class="count">${visible}</span> / ${TOTAL} 个项目`;
}
// Tab clicks
document.querySelectorAll('#status-tabs .tab').forEach(t => {
t.addEventListener('click', () => {
document.querySelectorAll('#status-tabs .tab').forEach(x => x.classList.remove('active'));
t.classList.add('active');
state.filter = t.dataset.filter;
applyFilter();
Shell.toast('筛选: ' + t.textContent.trim().split(' ')[0], 'filter=' + state.filter);
});
});
// View toggle
document.querySelectorAll('.view-toggle button').forEach(b => {
b.addEventListener('click', () => {
document.querySelectorAll('.view-toggle button').forEach(x => x.classList.remove('active'));
b.classList.add('active');
state.view = b.dataset.view;
applyFilter();
Shell.toast('视图切换: ' + (state.view === 'list' ? '列表' : '网格'), 'view=' + state.view);
});
});
// Search
document.getElementById('search-input').addEventListener('input', e => {
state.search = e.target.value.trim();
applyFilter();
});
applyFilter();
</script>
</body>
</html>