feat(workbench): 统一立绘详情页参考布局 · 三视图全 16:9 · 工作台批次追加
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 6s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 6s
详情页 (pipeline / library / model-photo) - 统一参考布局:大立绘+缩略 strip+查看大图,右栏 三视图+简介(标签 chip)+3 列属性表 - 底部仅留「下载」+「使用该资产」,去除收藏 / 关闭 三视图固定单张 16:9 - pipeline / library / model-photo / asset-factory / product-studio 全部同步 - 移除原 actor 3 列 3:4 拆图,改为单容器 16:9 图片工作台 (model-photo / platform-cover) - 立即生成 + 全部重跑 + 单张重跑 均追加新批次到下方,旧批次保留 - 批量按钮下沉到每批次下方,与图片网格左对齐 - hover 重跑/采用 icon 缩小至 26px,右下角横向,无遮罩层 - 立即生成后不再自动新增「编辑中」草稿卡 新建商品 drawer - 无 onSave 回调时默认跳转 product-detail - 卖点新增 「+ 添加卖点」按钮(输入框下方独立行,左对齐) product-detail - 视频项目卡片状态 pill 改为 4 态(已完成/视频生成 4/6/已归档/故事板失败) - 移除视频卡个体「通过/不通过/归档」状态切换 - 去掉冗余「通过」status 筛选;过滤逻辑兼容缺失按钮 sidebar (shell.js) - 图片生成补 badge 12,团队去 badge 清理 - 删除 v2/ 历史镜像目录(与 电商AI平台/ 重复,Dockerfile build context 不依赖) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2a3ae88dc5
commit
8a783ca36f
@ -1,952 +0,0 @@
|
||||
# 流·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 档之间只差 1–2 个色阶,**肉眼几乎看不出**。**用语义(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。0–24% 用 `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 标签 | 11–11.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.65–1.8**(V2 是 1.5–1.7),中文字间留呼吸
|
||||
|
||||
---
|
||||
|
||||
## 4. 圆角规则 · V2 统一 8 px
|
||||
|
||||
> **核心原则(V2 改写):统一 8 px / 状态徽标完全圆 / 极少数微元素降到 4–6 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 10–14 px`(紧凑组合)
|
||||
- 标题下方的正文/列表:`mb 22–28 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.5–2 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)
|
||||
|
||||
- 居中,460–480 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 档 · 0–24 用 #000,32+ 用 #262626)=== */
|
||||
--black-alpha-1: rgba(0, 0, 0, .01);
|
||||
--black-alpha-2: rgba(0, 0, 0, .02);
|
||||
--black-alpha-3: rgba(0, 0, 0, .03);
|
||||
--black-alpha-4: rgba(0, 0, 0, .04);
|
||||
--black-alpha-5: rgba(0, 0, 0, .05);
|
||||
--black-alpha-6: rgba(0, 0, 0, .06);
|
||||
--black-alpha-7: rgba(0, 0, 0, .07);
|
||||
--black-alpha-8: rgba(0, 0, 0, .08);
|
||||
--black-alpha-10: rgba(0, 0, 0, .10);
|
||||
--black-alpha-12: rgba(0, 0, 0, .12);
|
||||
--black-alpha-16: rgba(0, 0, 0, .16);
|
||||
--black-alpha-20: rgba(0, 0, 0, .20);
|
||||
--black-alpha-24: rgba(0, 0, 0, .24);
|
||||
--black-alpha-32: rgba(38, 38, 38, .32);
|
||||
--black-alpha-40: rgba(38, 38, 38, .40);
|
||||
--black-alpha-48: rgba(38, 38, 38, .48);
|
||||
--black-alpha-56: rgba(38, 38, 38, .56);
|
||||
--black-alpha-64: rgba(38, 38, 38, .64);
|
||||
--black-alpha-72: rgba(38, 38, 38, .72);
|
||||
--black-alpha-88: rgba(38, 38, 38, .88);
|
||||
|
||||
/* === Legacy aliases(V2 命名 → V2.1 token,组件 CSS 无需重写)=== */
|
||||
--bg: var(--background-base);
|
||||
--bg-soft: var(--background-lighter);
|
||||
--card: var(--surface);
|
||||
--ink: var(--accent-black);
|
||||
--green: var(--accent-forest);
|
||||
--red: var(--accent-crimson);
|
||||
--green-bg: rgba(66, 195, 102, .08);
|
||||
--green-bd: rgba(66, 195, 102, .20);
|
||||
--red-bg: rgba(235, 52, 36, .08);
|
||||
--red-bd: rgba(235, 52, 36, .20);
|
||||
--ink-alpha-4: var(--black-alpha-4);
|
||||
--ink-alpha-7: var(--black-alpha-7);
|
||||
--ink-alpha-12: var(--black-alpha-12);
|
||||
--ink-alpha-24: var(--black-alpha-24);
|
||||
--ink-alpha-32: var(--black-alpha-32);
|
||||
--ink-alpha-48: var(--black-alpha-48);
|
||||
--ink-alpha-56: var(--black-alpha-56);
|
||||
--ink-alpha-64: var(--black-alpha-64);
|
||||
--ink-alpha-72: var(--black-alpha-72);
|
||||
--ink-alpha-88: var(--black-alpha-88);
|
||||
|
||||
/* === 圆角 === */
|
||||
--r-sm: 4px;
|
||||
--r-md: 8px; /* 默认主圆角 */
|
||||
--r-pill: 999px;
|
||||
|
||||
/* === 字体 === */
|
||||
--font-sans: 'Inter Tight', 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||
--font-mono: 'JetBrains Mono', 'Geist Mono', monospace;
|
||||
|
||||
/* === 容器宽度 === */
|
||||
--container-max: 1480px;
|
||||
--sidebar-w: 248px;
|
||||
|
||||
/* === 过渡 === */
|
||||
--t-fast: 100ms ease;
|
||||
--t-base: 200ms ease;
|
||||
--t-slow: 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
/* === Selection · Firecrawl 签名细节 === */
|
||||
::selection {
|
||||
background: var(--heat-20);
|
||||
color: var(--heat);
|
||||
}
|
||||
|
||||
/* === Dark mode · 翻转底色 + black-alpha 改用 white-alpha === */
|
||||
.dark {
|
||||
--background-base: #0a0a0a;
|
||||
--background-lighter: #141414;
|
||||
--surface: #171717;
|
||||
--surface-raised: #1f1f1f;
|
||||
--border-faint: #2a2a2a;
|
||||
--border-muted: #333333;
|
||||
--border-loud: #404040;
|
||||
--accent-black: #f5f5f5;
|
||||
--black-alpha-1: rgba(255,255,255,.01);
|
||||
--black-alpha-2: rgba(255,255,255,.02);
|
||||
--black-alpha-3: rgba(255,255,255,.03);
|
||||
--black-alpha-4: rgba(255,255,255,.04);
|
||||
--black-alpha-5: rgba(255,255,255,.05);
|
||||
--black-alpha-6: rgba(255,255,255,.06);
|
||||
--black-alpha-7: rgba(255,255,255,.07);
|
||||
--black-alpha-8: rgba(255,255,255,.08);
|
||||
--black-alpha-10: rgba(255,255,255,.10);
|
||||
--black-alpha-12: rgba(255,255,255,.12);
|
||||
--black-alpha-16: rgba(255,255,255,.16);
|
||||
--black-alpha-20: rgba(255,255,255,.20);
|
||||
--black-alpha-24: rgba(255,255,255,.24);
|
||||
--black-alpha-32: rgba(255,255,255,.32);
|
||||
--black-alpha-40: rgba(255,255,255,.40);
|
||||
--black-alpha-48: rgba(255,255,255,.48);
|
||||
--black-alpha-56: rgba(255,255,255,.56);
|
||||
--black-alpha-64: rgba(255,255,255,.64);
|
||||
--black-alpha-72: rgba(255,255,255,.72);
|
||||
--black-alpha-88: rgba(255,255,255,.88);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14. V1 → V2 迁移检查清单(给后续改代码用)
|
||||
|
||||
- [ ] 全局替换 `border-radius: 0` → `border-radius: 8px`(卡片 / stats / shortcut / modal / toast / thumb)
|
||||
- [ ] 替换 V1 ink-2/3/4 token 为 ink-alpha-56/48/12
|
||||
- [ ] 替换 `--orange-tint` `--orange-soft` 为 `--heat-12` `--heat-20`
|
||||
- [ ] 主 CTA hover 移除 `#D04E1F`,改用 4 层橙阴影变化
|
||||
- [ ] 所有 `.btn` `.input` 加 `inside-border` 类
|
||||
- [ ] 主工作区容器加 `border-left + border-right` + 4 个 SVG 准星
|
||||
- [ ] 字体 Inter → Inter Tight
|
||||
- [ ] 所有 icon 转 SVG line icon,stroke 1.5
|
||||
- [ ] Pill 按 L1/L2/L3 三级规范化高度/字号/圆点尺寸
|
||||
- [ ] 每个交互组件补齐 hover / active / focused / disabled 状态
|
||||
|
||||
---
|
||||
|
||||
## 15. 参考与来源
|
||||
|
||||
- **视觉灵感(实测):** [Firecrawl Playground](https://www.firecrawl.dev/playground?endpoint=parse) · 详见 [firecrawl_playground_spec.md](_design_src/firecrawl_playground_spec.md)
|
||||
- **结构灵感:** Linear / Stripe Dashboard
|
||||
- **图纸感来源:** 印刷套版准星 + 老 Unix 终端
|
||||
- **V1 文档:** [DESIGN_SPEC.md](DESIGN_SPEC.md)(保留作为历史)
|
||||
170
v2/account.html
170
v2/account.html
@ -1,170 +0,0 @@
|
||||
<!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>
|
||||
@ -1,900 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>图片生成 · 流·Studio</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="assets/restraint.css">
|
||||
<style>
|
||||
#page-content { padding: 24px 28px 60px; }
|
||||
|
||||
/* ─── 三 Hero 卡片网格(模特上身图 / 平台套图 / 图片优化 · 等比)─── */
|
||||
.factory-hero {
|
||||
display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 16px; margin-bottom: 56px;
|
||||
}
|
||||
@media (max-width: 1400px) { .factory-hero { grid-template-columns: 1fr 1fr; } }
|
||||
@media (max-width: 1000px) { .factory-hero { grid-template-columns: 1fr; } }
|
||||
|
||||
.factory-card {
|
||||
position: relative;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-md);
|
||||
padding: 28px 30px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 卡片内 · 文上图下 单列(3 卡并排时保持视觉一致)*/
|
||||
.factory-body {
|
||||
display: flex; flex-direction: column;
|
||||
gap: 18px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.factory-text { display: flex; flex-direction: column; min-width: 0; }
|
||||
.factory-tag {
|
||||
align-self: flex-start;
|
||||
font-family: var(--font-mono); font-size: 10.5px;
|
||||
color: var(--black-alpha-48); letter-spacing: .06em;
|
||||
padding: 2px 8px;
|
||||
border: 1px solid var(--border-faint);
|
||||
background: var(--background-lighter);
|
||||
border-radius: var(--r-sm);
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.factory-title {
|
||||
font-size: 22px; font-weight: 600;
|
||||
letter-spacing: -.018em; line-height: 1.25;
|
||||
color: var(--accent-black);
|
||||
}
|
||||
.factory-desc {
|
||||
margin-top: 8px;
|
||||
font-size: 13.5px; color: var(--black-alpha-64); line-height: 1.55;
|
||||
}
|
||||
|
||||
/* feature 列表 */
|
||||
.factory-features {
|
||||
list-style: none; padding: 0;
|
||||
margin: 22px 0 0;
|
||||
display: flex; flex-direction: column; gap: 11px;
|
||||
}
|
||||
.factory-features li {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
font-size: 13px; color: var(--black-alpha-72); font-weight: 500;
|
||||
}
|
||||
.factory-features .ff-ic {
|
||||
width: 26px; height: 26px;
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
background: var(--heat-12);
|
||||
color: var(--heat);
|
||||
border-radius: var(--r-md);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.factory-features .ff-ic svg { width: 14px; height: 14px; }
|
||||
|
||||
/* 平台 chip 行 */
|
||||
.platform-row {
|
||||
display: flex; flex-wrap: wrap; gap: 8px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.platform-chip {
|
||||
display: inline-flex; align-items: center; gap: 7px;
|
||||
height: 30px; padding: 0 12px 0 8px;
|
||||
border: 1px solid var(--border-faint);
|
||||
background: var(--surface);
|
||||
border-radius: var(--r-pill);
|
||||
transition: background var(--t-base);
|
||||
cursor: pointer;
|
||||
}
|
||||
.platform-chip:hover { background: var(--black-alpha-4); }
|
||||
.platform-chip .code {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
width: 20px; height: 20px;
|
||||
background: var(--accent-black); color: var(--accent-white);
|
||||
font-family: var(--font-mono); font-size: 8.5px; font-weight: 600;
|
||||
border-radius: var(--r-pill); letter-spacing: .04em;
|
||||
}
|
||||
.platform-chip .nm {
|
||||
font-size: 12px; color: var(--accent-black); font-weight: 500;
|
||||
}
|
||||
|
||||
/* CTA 行:主按钮 + 价格 mono */
|
||||
.factory-cta {
|
||||
margin-top: auto; padding-top: 24px;
|
||||
display: flex; align-items: center; gap: 14px;
|
||||
}
|
||||
.factory-cta .cost {
|
||||
font-family: var(--font-mono); font-size: 10.5px;
|
||||
color: var(--black-alpha-48); letter-spacing: .04em;
|
||||
}
|
||||
|
||||
/* 视觉占位 + feature 列表已隐藏 — CTA 卡片只保留标题 + 描述 + 按钮 */
|
||||
.factory-visual { display: none; }
|
||||
.factory-features { display: none; }
|
||||
.model-visual { grid-template-columns: repeat(4, 1fr); }
|
||||
.model-visual .main { aspect-ratio: 3 / 4; grid-column: span 1; }
|
||||
.model-visual .stack { display: contents; }
|
||||
.model-visual .stack .placeholder { aspect-ratio: 3 / 4; }
|
||||
.kit-visual { grid-template-columns: repeat(4, 1fr); }
|
||||
.kit-visual .placeholder { aspect-ratio: 1 / 1; }
|
||||
.tri-visual { grid-template-columns: repeat(3, 1fr); }
|
||||
.tri-visual .placeholder { aspect-ratio: 1 / 1; }
|
||||
|
||||
/* ─── 任务中心 · section header ─── */
|
||||
.section-h { display: flex; align-items: center; gap: 12px; margin-top: 24px; margin-bottom: 14px; }
|
||||
.section-h h2 { font-size: 18px; font-weight: 600; letter-spacing: -.01em; }
|
||||
.section-h .sub-mono { font-family: var(--font-mono); font-size: 11.5px; color: var(--black-alpha-48); letter-spacing: .02em; }
|
||||
|
||||
/* ─── 视图切换 (复用 projects.html · 图标 + 文字) ─── */
|
||||
.view-toggle { display: inline-flex; border: 1px solid var(--border-faint); border-radius: var(--r-md); overflow: hidden; }
|
||||
.view-toggle button { padding: 0 14px; background: var(--surface); color: var(--black-alpha-56); font-size: 13px; border: 0; border-right: 1px solid var(--border-faint); border-radius: 0; height: 36px; cursor: pointer; font-family: inherit; display: flex; align-items: center; gap: 6px; transition: background var(--t-base), color var(--t-base); }
|
||||
.view-toggle button:last-child { border-right: 0; }
|
||||
.view-toggle button:hover { background: var(--background-lighter); color: var(--accent-black); }
|
||||
.view-toggle button.active { background: var(--heat-12); color: var(--heat); font-weight: 600; }
|
||||
.view-toggle button svg { width: 13px; height: 13px; }
|
||||
|
||||
/* ─── 网格视图 · 卡片(原 history-grid) ─── */
|
||||
.history-grid {
|
||||
display: grid; grid-template-columns: repeat(4, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
@media (max-width: 1280px) { .history-grid { grid-template-columns: repeat(3, 1fr); } }
|
||||
@media (max-width: 960px) { .history-grid { grid-template-columns: repeat(2, 1fr); } }
|
||||
.history-grid[hidden] { display: none; }
|
||||
|
||||
.history-card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-md);
|
||||
padding: 12px;
|
||||
display: grid; grid-template-columns: 78px 1fr; gap: 14px;
|
||||
align-items: center;
|
||||
transition: background var(--t-base);
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
.history-card:hover { background: var(--black-alpha-4); }
|
||||
.history-card:hover .card-del-btn { opacity: 1; }
|
||||
.history-card .placeholder { width: 78px; height: 78px; }
|
||||
|
||||
/* ─── 列表视图 · 表格 ─── */
|
||||
#task-list-view[hidden] { display: none; }
|
||||
.task-name-cell { display: flex; align-items: center; gap: 12px; }
|
||||
.task-thumb { width: 40px; height: 40px; flex-shrink: 0; border-radius: var(--r-sm); }
|
||||
.task-name { font-weight: 600; color: var(--accent-black); font-size: 13.5px; }
|
||||
.task-sub { font-size: 11.5px; color: var(--black-alpha-48); margin-top: 3px; font-family: var(--font-mono); letter-spacing: .02em; }
|
||||
table.t .task-list-prog { display: flex; align-items: center; gap: 8px; min-width: 120px; }
|
||||
table.t .task-list-prog .bar { flex: 1; height: 4px; background: var(--black-alpha-7); border-radius: 2px; overflow: hidden; }
|
||||
table.t .task-list-prog .bar span { display: block; height: 100%; background: var(--heat); border-radius: 2px; animation: hp-pulse 1.4s ease-in-out infinite; }
|
||||
table.t .task-list-prog .pct { font-family: var(--font-mono); font-size: 10.5px; color: var(--heat); letter-spacing: .02em; white-space: nowrap; }
|
||||
|
||||
/* ─── 行末 ⋯ 删除气泡 (复用 projects.html) ─── */
|
||||
.row-action { display: flex; gap: 4px; justify-content: flex-end; }
|
||||
table.t tbody tr .row-more { opacity: 0; transition: opacity .15s; }
|
||||
table.t tbody tr:hover .row-more { opacity: 1; }
|
||||
.row-more {
|
||||
position: relative; display: inline-flex;
|
||||
cursor: pointer; align-items: center;
|
||||
color: var(--black-alpha-56);
|
||||
padding: 4px;
|
||||
}
|
||||
.row-more:hover { color: var(--accent-black); }
|
||||
.row-more-tip {
|
||||
position: absolute; top: calc(100% + 6px); right: 0;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-md);
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,.08);
|
||||
padding: 4px; min-width: 110px;
|
||||
opacity: 0; pointer-events: none;
|
||||
transform: translateY(-2px);
|
||||
transition: opacity .15s, transform .15s;
|
||||
z-index: 12;
|
||||
}
|
||||
.row-more-tip::before {
|
||||
content: ''; position: absolute;
|
||||
top: -8px; left: 0; right: 0; height: 8px;
|
||||
}
|
||||
.row-more:hover .row-more-tip,
|
||||
.row-more-tip:hover { opacity: 1; pointer-events: auto; transform: translateY(0); }
|
||||
.row-more-tip .mi {
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
width: 100%; padding: 6px 10px;
|
||||
background: transparent; border: 0;
|
||||
border-radius: var(--r-sm); cursor: pointer;
|
||||
font-size: 12.5px; color: var(--accent-black);
|
||||
font-family: inherit; text-align: left;
|
||||
transition: background var(--t-base), color var(--t-base);
|
||||
}
|
||||
.row-more-tip .mi:hover {
|
||||
background: var(--crimson-bg, #fdebea);
|
||||
color: var(--accent-crimson, #c43d3d);
|
||||
}
|
||||
.row-more-tip .mi svg { width: 13px; height: 13px; }
|
||||
|
||||
/* ─── 删除确认 modal · 复用 ─── */
|
||||
.modal-bg.show { display: flex; }
|
||||
.mono-acc { font-family: var(--font-mono); color: var(--heat); font-weight: 600; }
|
||||
|
||||
.history-body { min-width: 0; display: flex; flex-direction: column; gap: 4px; }
|
||||
.history-name {
|
||||
font-size: 13.5px; font-weight: 600; color: var(--accent-black);
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.history-type {
|
||||
font-size: 11.5px; color: var(--black-alpha-48);
|
||||
font-family: var(--font-mono); letter-spacing: .02em;
|
||||
}
|
||||
.history-foot {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
margin-top: 4px; gap: 6px; min-width: 0;
|
||||
}
|
||||
.history-foot .mono {
|
||||
font-family: var(--font-mono); font-size: 10.5px;
|
||||
color: var(--black-alpha-48); letter-spacing: .02em;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.history-foot .pill { padding: 2px 8px; font-size: 10.5px; }
|
||||
.history-foot .pill .dot { width: 5px; height: 5px; }
|
||||
|
||||
/* 进度条(生成中状态) */
|
||||
.history-prog {
|
||||
margin-top: 6px;
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
}
|
||||
.history-prog .bar {
|
||||
flex: 1; height: 4px;
|
||||
background: var(--black-alpha-7);
|
||||
border-radius: 2px; overflow: hidden;
|
||||
}
|
||||
.history-prog .bar span {
|
||||
display: block; height: 100%;
|
||||
background: var(--heat); border-radius: 2px;
|
||||
animation: hp-pulse 1.4s ease-in-out infinite;
|
||||
}
|
||||
@keyframes hp-pulse {
|
||||
0%, 100% { opacity: 1; transform: scaleY(1); }
|
||||
50% { opacity: .55; transform: scaleY(.7); }
|
||||
}
|
||||
.history-prog .pct {
|
||||
font-family: var(--font-mono); font-size: 10px;
|
||||
color: var(--heat); letter-spacing: .02em; white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="page">
|
||||
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1>图片生成</h1>
|
||||
<div class="sub">
|
||||
<span class="mono">// 一键生成</span>
|
||||
<span>·</span>
|
||||
<span>电商视觉素材,提升内容制作效率</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 双 Hero 卡片 -->
|
||||
<div class="factory-hero">
|
||||
|
||||
<!-- 卡片 A · 模特上身图 -->
|
||||
<div class="factory-card with-corners">
|
||||
<span class="corner-tr" aria-hidden></span>
|
||||
<span class="corner-bl" aria-hidden></span>
|
||||
|
||||
<div class="factory-body">
|
||||
<div class="factory-text">
|
||||
<span class="factory-tag">[ MODEL · TRY-ON ]</span>
|
||||
<div class="factory-title">模特上身图</div>
|
||||
<div class="factory-desc">选择模特,AI 生成商品模特上身效果图</div>
|
||||
|
||||
<ul class="factory-features">
|
||||
<li>
|
||||
<span class="ff-ic">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="9" r="3"/><circle cx="17" cy="9" r="2.5"/><path d="M3 19c0-3 2.7-5 6-5s6 2 6 5M14 19c.5-2.4 2.4-4 5-4 .8 0 1.5.2 2 .5"/></svg>
|
||||
</span>
|
||||
支持多模特选择
|
||||
</li>
|
||||
<li>
|
||||
<span class="ff-ic">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="8" height="8"/><rect x="13" y="3" width="8" height="8"/><rect x="3" y="13" width="8" height="8"/><rect x="13" y="13" width="8" height="8"/></svg>
|
||||
</span>
|
||||
一次生成 4 张
|
||||
</li>
|
||||
<li>
|
||||
<span class="ff-ic">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="m3 7 9-4 9 4-9 4-9-4z"/><path d="m3 12 9 4 9-4M3 17l9 4 9-4"/></svg>
|
||||
</span>
|
||||
支持多商品并行
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="factory-cta">
|
||||
<a class="btn btn-primary btn-lg" href="model-photo.html">
|
||||
开始生成
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
|
||||
</a>
|
||||
<span class="cost">[ ≈ ¥0.30 / 张 ]</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="factory-visual model-visual">
|
||||
<div class="placeholder main"><span class="ph-frame">Ava · 9:16</span></div>
|
||||
<div class="stack">
|
||||
<div class="placeholder"><span class="ph-frame">变体 01</span></div>
|
||||
<div class="placeholder"><span class="ph-frame">变体 02</span></div>
|
||||
<div class="placeholder"><span class="ph-frame">变体 03</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 卡片 B · 平台套图 -->
|
||||
<div class="factory-card with-corners">
|
||||
<span class="corner-tr" aria-hidden></span>
|
||||
<span class="corner-bl" aria-hidden></span>
|
||||
|
||||
<div class="factory-body">
|
||||
<div class="factory-text">
|
||||
<span class="factory-tag">[ PLATFORM · KIT ]</span>
|
||||
<div class="factory-title">平台套图</div>
|
||||
<div class="factory-desc">选择平台模板,AI 生成电商平台套图</div>
|
||||
|
||||
<ul class="factory-features">
|
||||
<li>
|
||||
<span class="ff-ic">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21V9M9 21V5M15 21v-8M21 21V11"/></svg>
|
||||
</span>
|
||||
覆盖主流电商平台
|
||||
</li>
|
||||
<li>
|
||||
<span class="ff-ic">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="8" height="8"/><rect x="13" y="3" width="8" height="8"/><rect x="3" y="13" width="8" height="8"/><rect x="13" y="13" width="8" height="8"/></svg>
|
||||
</span>
|
||||
一键生成 4 张套图
|
||||
</li>
|
||||
<li>
|
||||
<span class="ff-ic">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l1.7 4.6L18 9l-4.3 1.4L12 15l-1.7-4.6L6 9l4.3-1.4L12 3z"/></svg>
|
||||
</span>
|
||||
智能排版设计
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="platform-row">
|
||||
<div class="platform-chip"><span class="code">DY</span><span class="nm">抖音</span></div>
|
||||
<div class="platform-chip"><span class="code">TB</span><span class="nm">淘宝</span></div>
|
||||
<div class="platform-chip"><span class="code">XHS</span><span class="nm">小红书</span></div>
|
||||
<div class="platform-chip"><span class="code">PDD</span><span class="nm">拼多多</span></div>
|
||||
<div class="platform-chip"><span class="code">AMZ</span><span class="nm">亚马逊</span></div>
|
||||
</div>
|
||||
|
||||
<div class="factory-cta">
|
||||
<a class="btn btn-primary btn-lg" href="platform-cover.html">
|
||||
开始生成
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
|
||||
</a>
|
||||
<span class="cost">[ ≈ ¥0.50 / 张 ]</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="factory-visual kit-visual">
|
||||
<div class="placeholder"><span class="ph-frame">套图 / TB</span></div>
|
||||
<div class="placeholder"><span class="ph-frame">套图 / DY</span></div>
|
||||
<div class="placeholder"><span class="ph-frame">套图 / XHS</span></div>
|
||||
<div class="placeholder"><span class="ph-frame">套图 / PDD</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 卡片 C · 图片优化(生成人物 / 商品三视图)-->
|
||||
<div class="factory-card with-corners">
|
||||
<span class="corner-tr" aria-hidden></span>
|
||||
<span class="corner-bl" aria-hidden></span>
|
||||
|
||||
<div class="factory-body">
|
||||
<div class="factory-text">
|
||||
<span class="factory-tag">[ TRI-VIEW · OPTIMIZE ]</span>
|
||||
<div class="factory-title">图片优化</div>
|
||||
<div class="factory-desc">为人物 / 商品生成三视图,保证多镜头一致性</div>
|
||||
|
||||
<ul class="factory-features">
|
||||
<li>
|
||||
<span class="ff-ic">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="9" r="3"/><path d="M3 19c0-3 2.7-5 6-5s6 2 6 5"/></svg>
|
||||
</span>
|
||||
人物 · 商品 全支持
|
||||
</li>
|
||||
<li>
|
||||
<span class="ff-ic">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="6" height="18"/><rect x="9" y="3" width="6" height="18"/><rect x="15" y="3" width="6" height="18"/></svg>
|
||||
</span>
|
||||
正面 / 侧面 / 背面 一次输出
|
||||
</li>
|
||||
<li>
|
||||
<span class="ff-ic">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/></svg>
|
||||
</span>
|
||||
多镜头一致性保证
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="factory-cta">
|
||||
<a class="btn btn-primary btn-lg" href="model-photo.html?mode=tri">
|
||||
开始生成
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
|
||||
</a>
|
||||
<span class="cost">[ ≈ ¥0.40 / 组 ]</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="factory-visual tri-visual">
|
||||
<div class="placeholder"><span class="ph-frame">正面</span></div>
|
||||
<div class="placeholder"><span class="ph-frame">侧面</span></div>
|
||||
<div class="placeholder"><span class="ph-frame">背面</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ============= 任务中心 · 参考 projects.html 布局 ============= -->
|
||||
<div class="section-h">
|
||||
<h2>任务中心</h2>
|
||||
<span class="sub-mono">// <span id="tc-sub-total">0</span> 个 · <span id="tc-sub-gen">0</span> 生成中 · <span id="tc-sub-ok">0</span> 已完成 · <span id="tc-sub-err">0</span> 失败</span>
|
||||
</div>
|
||||
|
||||
<!-- 状态 tabs (复用 .tabs) -->
|
||||
<div class="tabs" id="tc-tabs">
|
||||
<div class="tab active" data-filter="all">全部 <span class="count">0</span></div>
|
||||
<div class="tab" data-filter="gen">生成中 <span class="count">0</span></div>
|
||||
<div class="tab" data-filter="ok">已完成 <span class="count">0</span></div>
|
||||
<div class="tab" data-filter="err">失败 <span class="count">0</span></div>
|
||||
</div>
|
||||
|
||||
<!-- toolbar: search + 类型 chip + clear + view-toggle -->
|
||||
<div class="toolbar">
|
||||
<div class="search-inline">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
|
||||
<input class="input" id="tc-search" placeholder="搜索任务名">
|
||||
</div>
|
||||
<div class="chip-wrap" data-key="type">
|
||||
<button class="chip" type="button"><span class="chip-label">任务类型</span> <svg class="caret" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg></button>
|
||||
<div class="chip-menu"></div>
|
||||
</div>
|
||||
<button class="clear-filters" id="tc-clear" type="button" hidden>
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 4l8 8M12 4l-8 8"/></svg>
|
||||
清空筛选
|
||||
</button>
|
||||
<span class="spacer"></span>
|
||||
<div class="view-toggle" id="tc-view-toggle">
|
||||
<button type="button" data-view="grid">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2" y="2" width="5" height="5"/><rect x="9" y="2" width="5" height="5"/><rect x="2" y="9" width="5" height="5"/><rect x="9" y="9" width="5" height="5"/></svg>
|
||||
网格
|
||||
</button>
|
||||
<button type="button" class="active" data-view="list">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 4h12M2 8h12M2 12h12"/></svg>
|
||||
列表
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="result-meta" id="tc-result-meta">// 显示 <span class="count">0</span> / 0 个任务</div>
|
||||
|
||||
<!-- ============= LIST VIEW (默认) ============= -->
|
||||
<div id="task-list-view">
|
||||
<table class="t">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:32%">任务</th>
|
||||
<th>类型</th>
|
||||
<th style="width:140px">进度</th>
|
||||
<th>状态</th>
|
||||
<th style="width:120px">创建于</th>
|
||||
<th style="width:48px"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="task-list-tbody"><!-- JS 从卡片同步生成 --></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- ============= GRID VIEW ============= -->
|
||||
<div class="history-grid" id="task-grid" hidden>
|
||||
|
||||
<div class="task-card history-card" data-status="ok" data-type="model" data-name="补水面膜 × Ava">
|
||||
<button class="card-del-btn" type="button" title="删除任务" onclick="event.stopPropagation();" data-action="delete-task"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
|
||||
<div class="placeholder"><span class="ph-frame">Ava · 1:1</span></div>
|
||||
<div class="history-body">
|
||||
<div class="history-name">补水面膜 × Ava</div>
|
||||
<div class="history-type">模特上身图 · 4 张</div>
|
||||
<div class="history-foot">
|
||||
<span class="mono">// 05.19 · 14:30</span>
|
||||
<span class="pill ok"><span class="dot"></span>已完成</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="task-card history-card" data-status="ok" data-type="platform" data-name="精华液 × 平台套图">
|
||||
<button class="card-del-btn" type="button" title="删除任务" onclick="event.stopPropagation();"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
|
||||
<div class="placeholder"><span class="ph-frame">TB / XHS · 1:1</span></div>
|
||||
<div class="history-body">
|
||||
<div class="history-name">精华液 × 平台套图</div>
|
||||
<div class="history-type">平台套图 · 4 张</div>
|
||||
<div class="history-foot">
|
||||
<span class="mono">// 05.19 · 14:25</span>
|
||||
<span class="pill ok"><span class="dot"></span>已完成</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="task-card history-card" data-status="gen" data-type="model" data-name="防晒霜 × Luna">
|
||||
<button class="card-del-btn" type="button" title="删除任务" onclick="event.stopPropagation();"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
|
||||
<div class="placeholder"><span class="ph-frame">Luna · 1:1</span></div>
|
||||
<div class="history-body">
|
||||
<div class="history-name">防晒霜 × Luna</div>
|
||||
<div class="history-type">模特上身图 · 4 张</div>
|
||||
<div class="history-foot">
|
||||
<span class="mono">// 05.19 · 14:20</span>
|
||||
<span class="pill info"><span class="dot"></span>生成中</span>
|
||||
</div>
|
||||
<div class="history-prog">
|
||||
<div class="bar"><span style="width:65%"></span></div>
|
||||
<span class="pct">65%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="task-card history-card" data-status="err" data-type="platform" data-name="面霜 × 平台套图">
|
||||
<button class="card-del-btn" type="button" title="删除任务" onclick="event.stopPropagation();"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
|
||||
<div class="placeholder"><span class="ph-frame">DY / PDD · 1:1</span></div>
|
||||
<div class="history-body">
|
||||
<div class="history-name">面霜 × 平台套图</div>
|
||||
<div class="history-type">平台套图 · 4 张</div>
|
||||
<div class="history-foot">
|
||||
<span class="mono">// 05.19 · 14:15</span>
|
||||
<span class="pill err"><span class="dot"></span>失败</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="task-card history-card" data-status="ok" data-type="model" data-name="口红 × Mia">
|
||||
<button class="card-del-btn" type="button" title="删除任务" onclick="event.stopPropagation();"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
|
||||
<div class="placeholder"><span class="ph-frame">Mia · 1:1</span></div>
|
||||
<div class="history-body">
|
||||
<div class="history-name">口红 × Mia</div>
|
||||
<div class="history-type">模特上身图 · 4 张</div>
|
||||
<div class="history-foot">
|
||||
<span class="mono">// 05.18 · 21:08</span>
|
||||
<span class="pill ok"><span class="dot"></span>已完成</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="task-card history-card" data-status="ok" data-type="platform" data-name="眼霜 × 平台套图">
|
||||
<button class="card-del-btn" type="button" title="删除任务" onclick="event.stopPropagation();"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
|
||||
<div class="placeholder"><span class="ph-frame">TB / DY · 1:1</span></div>
|
||||
<div class="history-body">
|
||||
<div class="history-name">眼霜 × 平台套图</div>
|
||||
<div class="history-type">平台套图 · 8 张</div>
|
||||
<div class="history-foot">
|
||||
<span class="mono">// 05.18 · 18:42</span>
|
||||
<span class="pill ok"><span class="dot"></span>已完成</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="task-card history-card" data-status="ok" data-type="model" data-name="瑜伽裤 × Zoe">
|
||||
<button class="card-del-btn" type="button" title="删除任务" onclick="event.stopPropagation();"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
|
||||
<div class="placeholder"><span class="ph-frame">Zoe · 1:1</span></div>
|
||||
<div class="history-body">
|
||||
<div class="history-name">瑜伽裤 × Zoe</div>
|
||||
<div class="history-type">模特上身图 · 12 张</div>
|
||||
<div class="history-foot">
|
||||
<span class="mono">// 05.18 · 11:20</span>
|
||||
<span class="pill ok"><span class="dot"></span>已完成</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="task-card history-card" data-status="ok" data-type="platform" data-name="咖啡粉 × 平台套图">
|
||||
<button class="card-del-btn" type="button" title="删除任务" onclick="event.stopPropagation();"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
|
||||
<div class="placeholder"><span class="ph-frame">XHS / AMZ · 1:1</span></div>
|
||||
<div class="history-body">
|
||||
<div class="history-name">咖啡粉 × 平台套图</div>
|
||||
<div class="history-type">平台套图 · 4 张</div>
|
||||
<div class="history-foot">
|
||||
<span class="mono">// 05.17 · 16:50</span>
|
||||
<span class="pill ok"><span class="dot"></span>已完成</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ===== 删除确认 modal (复用 projects.html 风格) ===== -->
|
||||
<div class="modal-bg" id="tc-del-bg">
|
||||
<div class="modal" role="dialog">
|
||||
<span class="corner-tr" aria-hidden></span>
|
||||
<span class="corner-bl" aria-hidden></span>
|
||||
<div class="modal-h">
|
||||
<div class="ic-m" style="background:var(--crimson-bg,#fdebea);color:var(--accent-crimson,#c43d3d)">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg>
|
||||
</div>
|
||||
<div class="ti">确认删除任务<span>// CONFIRM DELETE</span></div>
|
||||
</div>
|
||||
<div class="modal-b" id="tc-del-body">即将删除任务记录。</div>
|
||||
<div class="modal-f">
|
||||
<button class="btn" type="button" id="tc-del-cancel">取消</button>
|
||||
<button class="btn" type="button" id="tc-del-ok" style="background:var(--accent-crimson);color:var(--accent-white);border-color:var(--accent-crimson)">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/></svg>
|
||||
确认删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="assets/shell.js"></script>
|
||||
<script src="assets/new-product-drawer.js"></script>
|
||||
<script>
|
||||
Shell.render({
|
||||
active: 'asset-factory',
|
||||
crumbs: [{ label: '工作台', href: 'index.html' }, { label: '图片生成' }]
|
||||
});
|
||||
|
||||
/* ============================================================
|
||||
任务中心 · projects.html 风格 (tabs + toolbar + 双视图)
|
||||
============================================================ */
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const TYPE_LABEL = { model: '模特上身图', platform: '平台套图' };
|
||||
const STATUS_LABEL = { ok: '已完成', gen: '生成中', err: '失败' };
|
||||
const STATUS_PILL = { ok: 'ok', gen: 'info', err: 'err' };
|
||||
|
||||
const taskGrid = document.getElementById('task-grid');
|
||||
const listTbody = document.getElementById('task-list-tbody');
|
||||
const gridView = document.getElementById('task-grid');
|
||||
const listView = document.getElementById('task-list-view');
|
||||
const cards = [...taskGrid.querySelectorAll('.task-card')];
|
||||
|
||||
const state = { filter: 'all', type: 'all', search: '', view: 'list' };
|
||||
|
||||
function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
|
||||
|
||||
// 任务行点击 → 跳转到对应工作台,携带商品名(任务名一般是「商品 × 模特/平台」格式)
|
||||
function goToWorkbench(type, name) {
|
||||
const productName = (name || '').split(/\s[×x]\s/)[0].trim();
|
||||
const q = '?t=' + Date.now() + (productName ? '&product=' + encodeURIComponent(productName) : '');
|
||||
const url = (type === 'model') ? 'model-photo.html' + q
|
||||
: (type === 'platform') ? 'platform-cover.html' + q
|
||||
: 'projects-new.html' + q;
|
||||
location.href = url;
|
||||
}
|
||||
|
||||
/* ---------- 1. 从卡片生成 list 表行 (单数据源) ---------- */
|
||||
function rowFor(card) {
|
||||
const name = card.dataset.name;
|
||||
const type = card.dataset.type;
|
||||
const status = card.dataset.status;
|
||||
const subText = (card.querySelector('.history-type')?.textContent || '').trim();
|
||||
const timeText = (card.querySelector('.history-foot .mono')?.textContent || '').replace(/^\/\/\s*/, '');
|
||||
const pct = card.querySelector('.history-prog .pct')?.textContent || '';
|
||||
const pillClass = STATUS_PILL[status] || 'info';
|
||||
const pillLabel = STATUS_LABEL[status] || status;
|
||||
|
||||
const tr = document.createElement('tr');
|
||||
tr.dataset.taskRow = '1';
|
||||
tr.dataset.name = name;
|
||||
tr.dataset.type = type;
|
||||
tr.dataset.status = status;
|
||||
tr.addEventListener('click', () => goToWorkbench(type, name));
|
||||
tr.innerHTML = `
|
||||
<td>
|
||||
<div class="task-name-cell">
|
||||
<div class="placeholder task-thumb"><span class="ph-frame">${esc(name.split(' ')[0] || '')}</span></div>
|
||||
<div>
|
||||
<div class="task-name">${esc(name)}</div>
|
||||
<div class="task-sub">${esc(subText)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td><span class="muted">${esc(TYPE_LABEL[type] || type)}</span></td>
|
||||
<td>${status === 'gen'
|
||||
? `<div class="task-list-prog"><div class="bar"><span style="width:${pct || '60%'}"></span></div><span class="pct">${esc(pct || '60%')}</span></div>`
|
||||
: (status === 'ok' ? '<span class="muted-2 mono" style="font-size:11px;">已完成</span>' : '<span class="muted-2 mono" style="font-size:11px;">—</span>')}</td>
|
||||
<td><span class="pill ${pillClass}"><span class="dot"></span>${esc(pillLabel)}</span></td>
|
||||
<td class="muted-2">${esc(timeText)}</td>
|
||||
<td>
|
||||
<div class="row-action">
|
||||
<span class="row-more"><svg width="14" height="14" viewBox="0 0 16 16"><circle cx="3" cy="8" r="1.2" fill="currentColor"/><circle cx="8" cy="8" r="1.2" fill="currentColor"/><circle cx="13" cy="8" r="1.2" fill="currentColor"/></svg>
|
||||
<div class="row-more-tip"><button class="mi mi-del-task" type="button"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg>删除任务</button></div>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
`;
|
||||
// 阻止 row-more 区域冒泡触发整行点击
|
||||
tr.querySelector('.row-more').addEventListener('click', e => e.stopPropagation());
|
||||
return tr;
|
||||
}
|
||||
|
||||
cards.forEach(card => {
|
||||
const tr = rowFor(card);
|
||||
listTbody.appendChild(tr);
|
||||
card._listRow = tr;
|
||||
tr._card = card;
|
||||
});
|
||||
|
||||
/* ---------- 2. 构建类型 chip 菜单 ---------- */
|
||||
const checkSvg = '<svg class="mi-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg>';
|
||||
const typeMenu = document.querySelector('.chip-wrap[data-key="type"] .chip-menu');
|
||||
const typeOptions = [...new Set(cards.map(c => c.dataset.type))];
|
||||
typeMenu.innerHTML = `<div class="mi selected" data-value="all">${checkSvg}<span>全部任务类型</span></div><div class="mi-sep"></div>`
|
||||
+ typeOptions.map(v => `<div class="mi" data-value="${esc(v)}">${checkSvg}<span>${esc(TYPE_LABEL[v] || v)}</span></div>`).join('');
|
||||
|
||||
function syncTypeChip() {
|
||||
const wrap = document.querySelector('.chip-wrap[data-key="type"]');
|
||||
const label = wrap.querySelector('.chip-label');
|
||||
const chip = wrap.querySelector('.chip');
|
||||
if (state.type === 'all') {
|
||||
label.textContent = '任务类型';
|
||||
chip.classList.remove('active');
|
||||
} else {
|
||||
label.textContent = TYPE_LABEL[state.type] || state.type;
|
||||
chip.classList.add('active');
|
||||
}
|
||||
wrap.querySelectorAll('.mi').forEach(mi => mi.classList.toggle('selected', mi.dataset.value === state.type));
|
||||
}
|
||||
|
||||
/* ---------- 3. applyFilter ---------- */
|
||||
function applyFilter() {
|
||||
const q = state.search.toLowerCase();
|
||||
let visible = 0;
|
||||
cards.forEach(card => {
|
||||
const okStatus = state.filter === 'all' || card.dataset.status === state.filter;
|
||||
const okType = state.type === 'all' || card.dataset.type === state.type;
|
||||
const okSearch = !q || (card.dataset.name || '').toLowerCase().includes(q);
|
||||
const show = okStatus && okType && okSearch;
|
||||
card.style.display = show ? '' : 'none';
|
||||
if (card._listRow) card._listRow.style.display = show ? '' : 'none';
|
||||
if (show) visible++;
|
||||
});
|
||||
|
||||
// 计数
|
||||
const counts = { all: cards.length, gen: 0, ok: 0, err: 0 };
|
||||
cards.forEach(c => { if (counts[c.dataset.status] !== undefined) counts[c.dataset.status]++; });
|
||||
document.querySelectorAll('#tc-tabs .tab').forEach(t => {
|
||||
const f = t.dataset.filter;
|
||||
t.querySelector('.count').textContent = f === 'all' ? counts.all : counts[f];
|
||||
});
|
||||
document.getElementById('tc-sub-total').textContent = counts.all;
|
||||
document.getElementById('tc-sub-gen').textContent = counts.gen;
|
||||
document.getElementById('tc-sub-ok').textContent = counts.ok;
|
||||
document.getElementById('tc-sub-err').textContent = counts.err;
|
||||
|
||||
document.getElementById('tc-result-meta').innerHTML = `// 显示 <span class="count">${visible}</span> / ${cards.length} 个任务`;
|
||||
document.getElementById('tc-clear').hidden = !(state.search || state.type !== 'all');
|
||||
}
|
||||
|
||||
/* ---------- 4. 事件绑定 ---------- */
|
||||
// status tabs
|
||||
document.querySelectorAll('#tc-tabs .tab').forEach(t => {
|
||||
t.addEventListener('click', () => {
|
||||
document.querySelectorAll('#tc-tabs .tab').forEach(x => x.classList.remove('active'));
|
||||
t.classList.add('active');
|
||||
state.filter = t.dataset.filter;
|
||||
applyFilter();
|
||||
});
|
||||
});
|
||||
// search
|
||||
document.getElementById('tc-search').addEventListener('input', e => {
|
||||
state.search = e.target.value.trim();
|
||||
applyFilter();
|
||||
});
|
||||
// type chip
|
||||
const typeWrap = document.querySelector('.chip-wrap[data-key="type"]');
|
||||
typeWrap.querySelector('.chip').addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
const isOpen = typeWrap.classList.contains('open');
|
||||
document.querySelectorAll('.chip-wrap.open').forEach(w => w.classList.remove('open'));
|
||||
if (!isOpen) typeWrap.classList.add('open');
|
||||
});
|
||||
typeMenu.addEventListener('click', e => {
|
||||
const mi = e.target.closest('.mi');
|
||||
if (!mi) return;
|
||||
e.stopPropagation();
|
||||
state.type = mi.dataset.value;
|
||||
typeWrap.classList.remove('open');
|
||||
syncTypeChip();
|
||||
applyFilter();
|
||||
});
|
||||
document.addEventListener('click', () => {
|
||||
document.querySelectorAll('.chip-wrap.open').forEach(w => w.classList.remove('open'));
|
||||
});
|
||||
// clear filters
|
||||
document.getElementById('tc-clear').addEventListener('click', () => {
|
||||
state.search = '';
|
||||
state.type = 'all';
|
||||
document.getElementById('tc-search').value = '';
|
||||
syncTypeChip();
|
||||
applyFilter();
|
||||
Shell.toast('已清空筛选');
|
||||
});
|
||||
// view toggle
|
||||
document.querySelectorAll('#tc-view-toggle button').forEach(b => {
|
||||
b.addEventListener('click', () => {
|
||||
document.querySelectorAll('#tc-view-toggle button').forEach(x => x.classList.remove('active'));
|
||||
b.classList.add('active');
|
||||
state.view = b.dataset.view;
|
||||
if (state.view === 'list') { listView.hidden = false; gridView.hidden = true; }
|
||||
else { listView.hidden = true; gridView.hidden = false; }
|
||||
});
|
||||
});
|
||||
|
||||
/* ---------- 5. 删除 modal + 配对删除 (复用 projects.html 模式) ---------- */
|
||||
const delBg = document.getElementById('tc-del-bg');
|
||||
const delBody = document.getElementById('tc-del-body');
|
||||
const delCancel = document.getElementById('tc-del-cancel');
|
||||
const delOk = document.getElementById('tc-del-ok');
|
||||
let _delTarget = null;
|
||||
|
||||
function openDelConfirm(target) {
|
||||
_delTarget = target;
|
||||
const name = target.dataset.name || '该任务';
|
||||
delBody.innerHTML = '即将删除任务 <span class="mono-acc">' + esc(name) + '</span>。任务记录将清除,已入库的素材不受影响。';
|
||||
delBg.classList.add('show');
|
||||
}
|
||||
function closeDelConfirm() { delBg.classList.remove('show'); _delTarget = null; }
|
||||
delCancel.addEventListener('click', closeDelConfirm);
|
||||
delBg.addEventListener('click', e => { if (e.target === delBg) closeDelConfirm(); });
|
||||
delOk.addEventListener('click', () => {
|
||||
if (!_delTarget) return;
|
||||
const card = _delTarget._card || _delTarget; // 可能传 row (with ._card) 或 card (with ._listRow)
|
||||
const row = card._listRow;
|
||||
const name = card.dataset.name;
|
||||
card.remove();
|
||||
if (row) row.remove();
|
||||
// 同步 cards 数组
|
||||
const idx = cards.indexOf(card);
|
||||
if (idx >= 0) cards.splice(idx, 1);
|
||||
closeDelConfirm();
|
||||
Shell.toast('已删除', name);
|
||||
applyFilter();
|
||||
});
|
||||
|
||||
// 绑定网格卡 删除按钮
|
||||
document.querySelectorAll('.task-card .card-del-btn').forEach(btn => {
|
||||
btn.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
const card = btn.closest('.task-card');
|
||||
if (card) openDelConfirm(card);
|
||||
});
|
||||
});
|
||||
|
||||
// 绑定网格卡片点击 → 跳工作台
|
||||
cards.forEach(card => {
|
||||
card.style.cursor = 'pointer';
|
||||
card.addEventListener('click', e => {
|
||||
if (e.target.closest('.card-del-btn')) return;
|
||||
goToWorkbench(card.dataset.type, card.dataset.name);
|
||||
});
|
||||
});
|
||||
// 绑定列表行 删除按钮 (事件委托, 因为是动态生成)
|
||||
listTbody.addEventListener('click', e => {
|
||||
const btn = e.target.closest('.mi-del-task');
|
||||
if (!btn) return;
|
||||
e.stopPropagation();
|
||||
const tr = btn.closest('tr');
|
||||
if (tr) openDelConfirm(tr);
|
||||
});
|
||||
|
||||
/* ---------- 6. 初始化 ---------- */
|
||||
applyFilter();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,578 +0,0 @@
|
||||
/* ============================================================
|
||||
新建商品 · 共享 Drawer 模块
|
||||
----------------------------------------------------------
|
||||
在任意页面只需 <script src="assets/new-product-drawer.js"> 引入,
|
||||
然后调用 NewProductDrawer.open({ onSave: fn }) 即可在当前页之上
|
||||
弹出右侧 Drawer。点击遮罩 / X / 取消 / ESC 关闭后,用户停在原页面。
|
||||
|
||||
提供:
|
||||
window.NewProductDrawer.open(opts?)
|
||||
window.NewProductDrawer.close()
|
||||
opts 字段:
|
||||
onSave(product) — 保存时回调,product = { id, name, cat, target,
|
||||
points: string[], images: { id, dataUrl, name }[] }
|
||||
============================================================ */
|
||||
(function () {
|
||||
'use strict';
|
||||
if (window.NewProductDrawer) return; // idempotent
|
||||
|
||||
const DRAWER_ID = 'npd-drawer';
|
||||
const DRAWER_BG_ID = 'npd-drawer-bg';
|
||||
|
||||
/* ---------- 注入样式(独立 namespace 以免与 products.html 冲突) ---------- */
|
||||
|
||||
const CSS = `
|
||||
/* drawer base (相同尺寸/动画) */
|
||||
#${DRAWER_BG_ID} {
|
||||
position: fixed; inset: 0;
|
||||
background: rgba(21, 20, 15, .32);
|
||||
display: none; z-index: 1100;
|
||||
}
|
||||
#${DRAWER_BG_ID}.show { display: block; }
|
||||
#${DRAWER_ID} {
|
||||
position: fixed; right: 0; top: 0; bottom: 0;
|
||||
width: 820px; max-width: 100vw;
|
||||
background: var(--surface);
|
||||
border-left: 1px solid var(--border-faint);
|
||||
z-index: 1101;
|
||||
transform: translateX(100%);
|
||||
transition: transform .25s cubic-bezier(.32, .72, 0, 1);
|
||||
display: flex; flex-direction: column;
|
||||
box-shadow: -4px 0 24px rgba(21, 20, 15, .04);
|
||||
}
|
||||
#${DRAWER_ID}.show { transform: translateX(0); }
|
||||
#${DRAWER_ID} .drawer-h {
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid var(--border-faint);
|
||||
display: flex; align-items: center;
|
||||
}
|
||||
#${DRAWER_ID} .drawer-h h3 { font-size: 16px; font-weight: 600; color: var(--accent-black); }
|
||||
#${DRAWER_ID} .drawer-h .x {
|
||||
margin-left: auto; width: 32px; height: 32px;
|
||||
border-radius: var(--r-md);
|
||||
display: grid; place-items: center;
|
||||
color: var(--black-alpha-56); cursor: pointer;
|
||||
background: transparent; border: 0;
|
||||
transition: background var(--t-base);
|
||||
}
|
||||
#${DRAWER_ID} .drawer-h .x:hover { background: var(--black-alpha-4); color: var(--accent-black); }
|
||||
#${DRAWER_ID} .drawer-b { padding: 24px 28px; overflow-y: auto; flex: 1; overscroll-behavior: contain; }
|
||||
#${DRAWER_ID} .drawer-f {
|
||||
padding: 14px 24px;
|
||||
border-top: 1px solid var(--border-faint);
|
||||
display: flex; gap: 10px; align-items: center;
|
||||
background: var(--surface);
|
||||
}
|
||||
#${DRAWER_ID} .drawer-f .btn-guide {
|
||||
margin-right: auto;
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
font-size: 13px; color: var(--black-alpha-56);
|
||||
background: transparent; border: 0; cursor: pointer;
|
||||
padding: 8px 10px; border-radius: var(--r-md);
|
||||
font-family: inherit;
|
||||
transition: background var(--t-base), color var(--t-base);
|
||||
}
|
||||
#${DRAWER_ID} .drawer-f .btn-guide:hover { color: var(--accent-black); background: var(--black-alpha-4); }
|
||||
#${DRAWER_ID} .drawer-f .btn-guide svg { width: 14px; height: 14px; }
|
||||
|
||||
/* form-card */
|
||||
#${DRAWER_ID} .form-h {
|
||||
font-size: 15px; font-weight: 600; color: var(--accent-black);
|
||||
margin-bottom: 18px; padding-bottom: 12px;
|
||||
border-bottom: 1px solid var(--border-faint);
|
||||
}
|
||||
#${DRAWER_ID} .field { margin-bottom: 16px; }
|
||||
#${DRAWER_ID} .field:last-child { margin-bottom: 0; }
|
||||
#${DRAWER_ID} .field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-bottom: 16px; }
|
||||
#${DRAWER_ID} .field-label {
|
||||
display: block; font-size: 13px; font-weight: 500;
|
||||
color: var(--accent-black); margin-bottom: 6px;
|
||||
}
|
||||
#${DRAWER_ID} .field-label .req { color: var(--heat); margin-left: 2px; }
|
||||
#${DRAWER_ID} .field-label .opt {
|
||||
color: var(--black-alpha-48); font-weight: 400; font-size: 12px; margin-left: 6px;
|
||||
}
|
||||
#${DRAWER_ID} .input,
|
||||
#${DRAWER_ID} .select {
|
||||
width: 100%; height: 38px;
|
||||
background: var(--background-lighter);
|
||||
border: 1px solid var(--black-alpha-12);
|
||||
border-radius: var(--r-md);
|
||||
padding: 0 14px;
|
||||
font-size: 13.5px; color: var(--accent-black);
|
||||
outline: none; font-family: inherit;
|
||||
transition: border-color var(--t-base);
|
||||
}
|
||||
#${DRAWER_ID} .input:focus,
|
||||
#${DRAWER_ID} .select:focus {
|
||||
border-color: var(--heat-40);
|
||||
box-shadow: inset 0 0 0 1px var(--heat-40);
|
||||
}
|
||||
|
||||
/* upload */
|
||||
#${DRAWER_ID} .pf-upload-row {
|
||||
display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
|
||||
gap: 16px; align-items: stretch;
|
||||
}
|
||||
#${DRAWER_ID} .pf-upload-zone {
|
||||
border: 1.5px dashed var(--black-alpha-24);
|
||||
border-radius: var(--r-md);
|
||||
padding: 28px 20px;
|
||||
background: var(--background-lighter);
|
||||
cursor: pointer; text-align: center;
|
||||
transition: border-color var(--t-base), background var(--t-base);
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
min-height: 180px;
|
||||
}
|
||||
#${DRAWER_ID} .pf-upload-zone:hover { border-color: var(--heat); background: var(--heat-8); }
|
||||
#${DRAWER_ID} .pf-upload-zone .uz-ic {
|
||||
width: 44px; height: 44px;
|
||||
margin: 0 auto 10px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--heat-20);
|
||||
border-radius: var(--r-md);
|
||||
color: var(--heat);
|
||||
display: grid; place-items: center;
|
||||
}
|
||||
#${DRAWER_ID} .pf-upload-zone .uz-ic svg { width: 20px; height: 20px; }
|
||||
#${DRAWER_ID} .pf-upload-zone .uz-t { font-size: 14px; color: var(--accent-black); font-weight: 500; }
|
||||
#${DRAWER_ID} .pf-upload-zone .uz-t strong { color: var(--heat); font-weight: 600; }
|
||||
#${DRAWER_ID} .pf-upload-zone .uz-d {
|
||||
margin-top: 8px;
|
||||
font-family: var(--font-mono); font-size: 11.5px;
|
||||
color: var(--black-alpha-48); letter-spacing: .02em;
|
||||
}
|
||||
#${DRAWER_ID} .pf-example {
|
||||
background: var(--background-lighter);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-md);
|
||||
padding: 16px;
|
||||
display: flex; flex-direction: column; gap: 10px;
|
||||
}
|
||||
#${DRAWER_ID} .pf-example .ex-h { font-size: 13px; font-weight: 600; color: var(--accent-black); }
|
||||
#${DRAWER_ID} .pf-example .ex-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
|
||||
#${DRAWER_ID} .pf-example .ex-grid .ex-thumb {
|
||||
aspect-ratio: 1;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-sm);
|
||||
overflow: hidden; position: relative;
|
||||
display: grid; place-items: center;
|
||||
color: var(--black-alpha-32);
|
||||
}
|
||||
#${DRAWER_ID} .pf-example .ex-grid .ex-thumb svg { width: 22px; height: 22px; }
|
||||
#${DRAWER_ID} .pf-example .ex-grid .ex-thumb::after {
|
||||
content: ''; position: absolute; inset: 0;
|
||||
background: repeating-linear-gradient(135deg, transparent 0 6px, rgba(0,0,0,.03) 6px 7px);
|
||||
pointer-events: none;
|
||||
}
|
||||
#${DRAWER_ID} .pf-example .ex-d { font-size: 12px; color: var(--black-alpha-56); line-height: 1.5; }
|
||||
#${DRAWER_ID} .pf-grid {
|
||||
display: grid; grid-template-columns: repeat(5, 1fr);
|
||||
gap: 8px; margin-top: 12px;
|
||||
}
|
||||
#${DRAWER_ID} .pf-grid:empty { display: none; }
|
||||
#${DRAWER_ID} .pf-thumb {
|
||||
aspect-ratio: 1;
|
||||
background: var(--background-lighter);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-md);
|
||||
position: relative; overflow: hidden; cursor: pointer;
|
||||
}
|
||||
#${DRAWER_ID} .pf-thumb img { width: 100%; height: 100%; object-fit: cover; }
|
||||
#${DRAWER_ID} .pf-thumb .pf-x {
|
||||
position: absolute; top: 4px; right: 4px;
|
||||
width: 22px; height: 22px;
|
||||
background: rgba(0,0,0,.7); color: var(--accent-white);
|
||||
border: 0; border-radius: 50%; cursor: pointer;
|
||||
display: grid; place-items: center;
|
||||
opacity: 0; transition: opacity var(--t-base);
|
||||
}
|
||||
#${DRAWER_ID} .pf-thumb:hover .pf-x { opacity: 1; }
|
||||
#${DRAWER_ID} .pf-thumb .pf-x svg { width: 11px; height: 11px; }
|
||||
|
||||
/* bullet-list */
|
||||
#${DRAWER_ID} .bullet-list { list-style: none; padding: 0; margin: 0; }
|
||||
#${DRAWER_ID} .bullet-list .bl-item,
|
||||
#${DRAWER_ID} .bullet-list .bl-add {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding: 8px 12px;
|
||||
background: var(--background-lighter);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-md);
|
||||
margin-bottom: 6px;
|
||||
font-size: 13.5px;
|
||||
}
|
||||
#${DRAWER_ID} .bullet-list .bl-add { background: transparent; border-style: dashed; }
|
||||
#${DRAWER_ID} .bullet-list .num {
|
||||
width: 22px; height: 22px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-sm);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px; color: var(--heat); font-weight: 700;
|
||||
display: grid; place-items: center; flex-shrink: 0;
|
||||
}
|
||||
#${DRAWER_ID} .bullet-list .bl-text { flex: 1; color: var(--accent-black); }
|
||||
#${DRAWER_ID} .bullet-list .bl-input {
|
||||
flex: 1; background: transparent; border: 0; outline: none;
|
||||
font-size: 13.5px; color: var(--accent-black); font-family: inherit;
|
||||
}
|
||||
#${DRAWER_ID} .bullet-list .bl-x {
|
||||
width: 22px; height: 22px;
|
||||
color: var(--black-alpha-48);
|
||||
cursor: pointer; display: grid; place-items: center;
|
||||
border-radius: var(--r-sm);
|
||||
transition: color var(--t-base), background var(--t-base);
|
||||
}
|
||||
#${DRAWER_ID} .bullet-list .bl-x:hover { color: var(--accent-crimson); background: var(--crimson-bg); }
|
||||
#${DRAWER_ID} .bullet-list .bl-x svg { width: 11px; height: 11px; }
|
||||
|
||||
@media (max-width: 900px) {
|
||||
#${DRAWER_ID} .pf-upload-row { grid-template-columns: 1fr; }
|
||||
}
|
||||
`;
|
||||
|
||||
const HTML = `
|
||||
<div id="${DRAWER_BG_ID}"></div>
|
||||
<aside id="${DRAWER_ID}" role="dialog" aria-label="新建商品" aria-hidden="true">
|
||||
<div class="drawer-h">
|
||||
<h3>新建商品</h3>
|
||||
<button class="x" type="button" data-act="close" aria-label="关闭">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 6l12 12M6 18L18 6"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="drawer-b">
|
||||
<div class="form-card">
|
||||
<div class="form-h">基础信息</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field-label">商品名称<span class="req">*</span></label>
|
||||
<input class="input" data-f="name" placeholder="请输入商品名称(必填)" maxlength="100">
|
||||
</div>
|
||||
|
||||
<div class="field-row">
|
||||
<div>
|
||||
<label class="field-label">品类<span class="req">*</span></label>
|
||||
<select class="select" data-f="cat">
|
||||
<option>美妆个护</option>
|
||||
<option>服饰内衣</option>
|
||||
<option>食品饮料</option>
|
||||
<option>家居家电</option>
|
||||
<option>数码 3C</option>
|
||||
<option>个护清洁</option>
|
||||
<option>运动户外</option>
|
||||
<option>母婴亲子</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="field-label">目标人群<span class="opt">(选填)</span></label>
|
||||
<input class="input" data-f="target" placeholder="例: 22-32 岁女性、敏感肌、办公室通勤">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field-label">商品主图<span class="req">*</span></label>
|
||||
<input type="file" data-f="file" accept="image/*" multiple hidden>
|
||||
<div class="pf-upload-row">
|
||||
<div class="pf-upload-zone" data-act="upload-zone">
|
||||
<div class="uz-ic">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M17 8l-5-5-5 5M12 3v12"/></svg>
|
||||
</div>
|
||||
<div class="uz-t">点击上传或<strong>拖拽图片</strong>到此处</div>
|
||||
<div class="uz-d">// 支持 JPG、PNG 格式,建议尺寸 800×800 以上,大小不超过 10MB</div>
|
||||
</div>
|
||||
<div class="pf-example">
|
||||
<div class="ex-h">示例图</div>
|
||||
<div class="ex-grid">
|
||||
<div class="ex-thumb"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M7 4h10l1 4v12H6V8l1-4z"/><path d="M9 4v3M15 4v3M9 11h6M9 14h6"/></svg></div>
|
||||
<div class="ex-thumb"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="5" width="12" height="15" rx="2"/><path d="M9 9h6M9 12h6M9 15h4"/></svg></div>
|
||||
<div class="ex-thumb"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3h8l1 5v12H7V8l1-5z"/><circle cx="12" cy="13" r="2.5"/></svg></div>
|
||||
</div>
|
||||
<div class="ex-d">优质的商品图有助于生成更好的素材效果</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-grid" data-f="grid"></div>
|
||||
</div>
|
||||
|
||||
<div class="field" style="margin-bottom: 0;">
|
||||
<label class="field-label">核心卖点<span class="req">*</span></label>
|
||||
<ul class="bullet-list" data-f="bullets">
|
||||
<li class="bl-add"><span class="num">+</span><input class="bl-input" placeholder="添加新卖点 · 回车确认"></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="drawer-f">
|
||||
<button class="btn-guide" type="button" data-act="guide">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M9.5 9a2.5 2.5 0 015 0c0 1.5-2.5 2-2.5 4M12 17h.01"/></svg>
|
||||
使用指南
|
||||
</button>
|
||||
<button class="btn" type="button" data-act="cancel">取消</button>
|
||||
<button class="btn btn-primary" type="button" data-act="save">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l5 5L20 6"/></svg>
|
||||
创建商品
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
`;
|
||||
|
||||
/* ---------- DOM refs (populated by ensureInjected) ---------- */
|
||||
let injected = false;
|
||||
let bg, drawer, $f, $grid, $bullets, $blInput;
|
||||
let currentOpts = {};
|
||||
const PF_MAX = 5;
|
||||
let pfFiles = []; // { id, dataUrl, name }
|
||||
const blXSvg = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M4 4l8 8M12 4l-8 8"/></svg>';
|
||||
|
||||
function esc(s) { return String(s == null ? '' : s).replace(/[<>&"]/g, c => ({ '<':'<','>':'>','&':'&','"':'"' })[c]); }
|
||||
function uid() { return Date.now().toString(36) + Math.random().toString(36).slice(2, 5); }
|
||||
function toast(msg, sub) {
|
||||
if (typeof Shell !== 'undefined' && Shell && Shell.toast) Shell.toast(msg, sub);
|
||||
}
|
||||
|
||||
function ensureInjected() {
|
||||
if (injected) return;
|
||||
// style
|
||||
const styleEl = document.createElement('style');
|
||||
styleEl.textContent = CSS;
|
||||
document.head.appendChild(styleEl);
|
||||
// html
|
||||
const wrap = document.createElement('div');
|
||||
wrap.innerHTML = HTML;
|
||||
while (wrap.firstChild) document.body.appendChild(wrap.firstChild);
|
||||
|
||||
bg = document.getElementById(DRAWER_BG_ID);
|
||||
drawer = document.getElementById(DRAWER_ID);
|
||||
$f = {
|
||||
name: drawer.querySelector('[data-f="name"]'),
|
||||
cat: drawer.querySelector('[data-f="cat"]'),
|
||||
target: drawer.querySelector('[data-f="target"]'),
|
||||
file: drawer.querySelector('[data-f="file"]'),
|
||||
};
|
||||
$grid = drawer.querySelector('[data-f="grid"]');
|
||||
$bullets = drawer.querySelector('[data-f="bullets"]');
|
||||
$blInput = $bullets.querySelector('.bl-add .bl-input');
|
||||
|
||||
bindEvents();
|
||||
injected = true;
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
// 关闭交互
|
||||
bg.addEventListener('click', close);
|
||||
drawer.addEventListener('click', e => {
|
||||
const a = e.target.closest('[data-act]');
|
||||
if (!a) return;
|
||||
const act = a.dataset.act;
|
||||
if (act === 'close') return close();
|
||||
if (act === 'cancel') return close();
|
||||
if (act === 'guide') return toast('使用指南', '点击查看完整填写指南');
|
||||
if (act === 'save') return save();
|
||||
if (act === 'upload-zone') return openFilePicker();
|
||||
});
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'Escape' && drawer.classList.contains('show')) close();
|
||||
});
|
||||
|
||||
// 上传
|
||||
$f.file.addEventListener('change', e => { addFiles(e.target.files); e.target.value = ''; });
|
||||
const zone = drawer.querySelector('[data-act="upload-zone"]');
|
||||
zone.addEventListener('dragover', e => { e.preventDefault(); zone.style.borderColor = 'var(--heat)'; });
|
||||
zone.addEventListener('dragleave', () => { zone.style.borderColor = ''; });
|
||||
zone.addEventListener('drop', e => {
|
||||
e.preventDefault(); zone.style.borderColor = '';
|
||||
if (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length) addFiles(e.dataTransfer.files);
|
||||
});
|
||||
|
||||
// 卖点 bullet-list
|
||||
$blInput.addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter') { e.preventDefault(); blAdd($blInput.value); $blInput.value = ''; }
|
||||
});
|
||||
}
|
||||
|
||||
function openFilePicker() { if (pfFiles.length < PF_MAX) $f.file.click(); }
|
||||
|
||||
function pfRender() {
|
||||
$grid.innerHTML = pfFiles.map(u => `
|
||||
<div class="pf-thumb" data-id="${u.id}">
|
||||
<img src="${u.dataUrl}" alt="${esc(u.name)}">
|
||||
<button class="pf-x" type="button" title="删除">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 4l8 8M12 4l-8 8"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
`).join('');
|
||||
$grid.querySelectorAll('.pf-thumb .pf-x').forEach(b => {
|
||||
b.onclick = e => {
|
||||
e.stopPropagation();
|
||||
const id = b.closest('.pf-thumb').dataset.id;
|
||||
const i = pfFiles.findIndex(f => f.id === id);
|
||||
if (i >= 0) { pfFiles.splice(i, 1); pfRender(); }
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function addFiles(fileList) {
|
||||
const room = PF_MAX - pfFiles.length;
|
||||
if (room <= 0) { toast('已达上限', PF_MAX + ' / ' + PF_MAX + ' 张'); return; }
|
||||
const incoming = [...fileList].filter(f => f.type.startsWith('image/')).slice(0, room);
|
||||
let done = 0;
|
||||
incoming.forEach(f => {
|
||||
const r = new FileReader();
|
||||
r.onload = e => {
|
||||
pfFiles.push({ id: uid(), dataUrl: e.target.result, name: f.name });
|
||||
if (++done === incoming.length) {
|
||||
pfRender();
|
||||
toast('已上传', '+ ' + done + ' 张 · 共 ' + pfFiles.length + ' / ' + PF_MAX);
|
||||
}
|
||||
};
|
||||
r.readAsDataURL(f);
|
||||
});
|
||||
}
|
||||
|
||||
function blRenumber() {
|
||||
[...$bullets.querySelectorAll('.bl-item')].forEach((li, i) => {
|
||||
li.querySelector('.num').textContent = i + 1;
|
||||
});
|
||||
}
|
||||
function blAdd(text) {
|
||||
const t = (text || '').trim();
|
||||
if (!t) return;
|
||||
const li = document.createElement('li');
|
||||
li.className = 'bl-item';
|
||||
li.innerHTML = '<span class="num">0</span><span class="bl-text">' + esc(t) + '</span><span class="bl-x" title="删除">' + blXSvg + '</span>';
|
||||
$bullets.querySelector('.bl-add').before(li);
|
||||
li.querySelector('.bl-x').addEventListener('click', () => {
|
||||
li.style.transition = 'opacity .15s, transform .15s';
|
||||
li.style.opacity = 0;
|
||||
li.style.transform = 'translateX(-8px)';
|
||||
setTimeout(() => { li.remove(); blRenumber(); }, 150);
|
||||
});
|
||||
blRenumber();
|
||||
}
|
||||
function getBullets() {
|
||||
return [...$bullets.querySelectorAll('.bl-item .bl-text')].map(t => t.textContent.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
/* ---------- API ---------- */
|
||||
|
||||
function resetForm() {
|
||||
$f.name.value = '';
|
||||
$f.cat.value = $f.cat.options[0].value;
|
||||
$f.target.value = '';
|
||||
pfFiles = [];
|
||||
pfRender();
|
||||
[...$bullets.querySelectorAll('.bl-item')].forEach(li => li.remove());
|
||||
$blInput.value = '';
|
||||
}
|
||||
|
||||
function lockBody() {
|
||||
// 优先用 Shell 的引用计数实现(避免多 overlay 互相解锁)
|
||||
if (typeof Shell !== 'undefined' && Shell && typeof Shell.lockScroll === 'function') {
|
||||
Shell.lockScroll();
|
||||
return;
|
||||
}
|
||||
// 兜底: Shell 未加载时本地锁
|
||||
const docEl = document.documentElement;
|
||||
const sbw = window.innerWidth - docEl.clientWidth;
|
||||
drawer._lockSnap = {
|
||||
bodyOverflow: document.body.style.overflow,
|
||||
bodyPaddingRight: document.body.style.paddingRight,
|
||||
htmlOverflow: docEl.style.overflow,
|
||||
};
|
||||
document.body.style.overflow = 'hidden';
|
||||
docEl.style.overflow = 'hidden';
|
||||
if (sbw > 0) document.body.style.paddingRight = sbw + 'px';
|
||||
}
|
||||
function unlockBody() {
|
||||
if (typeof Shell !== 'undefined' && Shell && typeof Shell.unlockScroll === 'function') {
|
||||
Shell.unlockScroll();
|
||||
return;
|
||||
}
|
||||
const s = drawer._lockSnap;
|
||||
if (s) {
|
||||
document.body.style.overflow = s.bodyOverflow;
|
||||
document.body.style.paddingRight = s.bodyPaddingRight;
|
||||
document.documentElement.style.overflow = s.htmlOverflow;
|
||||
drawer._lockSnap = null;
|
||||
}
|
||||
}
|
||||
|
||||
function open(opts) {
|
||||
ensureInjected();
|
||||
if (drawer.classList.contains('show')) return; // 已开则不重复锁
|
||||
currentOpts = opts || {};
|
||||
resetForm();
|
||||
bg.classList.add('show');
|
||||
drawer.classList.add('show');
|
||||
drawer.setAttribute('aria-hidden', 'false');
|
||||
lockBody();
|
||||
setTimeout(() => $f.name.focus(), 280);
|
||||
}
|
||||
|
||||
function close() {
|
||||
if (!injected) return;
|
||||
if (!drawer.classList.contains('show')) return; // 已关则不重复解锁
|
||||
bg.classList.remove('show');
|
||||
drawer.classList.remove('show');
|
||||
drawer.setAttribute('aria-hidden', 'true');
|
||||
unlockBody();
|
||||
if (typeof currentOpts.onClose === 'function') currentOpts.onClose();
|
||||
}
|
||||
|
||||
function save() {
|
||||
const name = ($f.name.value || '').trim();
|
||||
const cat = $f.cat.value;
|
||||
const target = ($f.target.value || '').trim();
|
||||
const points = getBullets();
|
||||
const images = pfFiles.slice();
|
||||
|
||||
if (!name) {
|
||||
toast('请填写商品名称');
|
||||
$f.name.focus();
|
||||
return;
|
||||
}
|
||||
if (images.length === 0) {
|
||||
toast('请上传商品主图', '至少 1 张');
|
||||
return;
|
||||
}
|
||||
if (points.length === 0) {
|
||||
toast('请添加核心卖点', '至少 1 条');
|
||||
$blInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
const product = {
|
||||
id: 'np-' + uid(),
|
||||
name, cat, target,
|
||||
points,
|
||||
images,
|
||||
imgs: images.length,
|
||||
};
|
||||
toast('商品已创建', '+ ' + name);
|
||||
if (typeof currentOpts.onSave === 'function') currentOpts.onSave(product);
|
||||
close();
|
||||
}
|
||||
|
||||
window.NewProductDrawer = { open, close };
|
||||
|
||||
/* ---------- sessionStorage 自动打开钩子 ---------- */
|
||||
// 任何页面只要在跳转前 sessionStorage.setItem('npd-auto-open','1') 即可,
|
||||
// 落地页加载完模块后,会自动 open() 一次并清掉 flag。
|
||||
// 用于:product-create.html 重定向后让落地页弹出 drawer,而不是用户重新点击。
|
||||
function checkAutoOpen() {
|
||||
try {
|
||||
if (sessionStorage.getItem('npd-auto-open') === '1') {
|
||||
sessionStorage.removeItem('npd-auto-open');
|
||||
// 延后一拍,确保宿主页面自己的 init 已经跑完
|
||||
setTimeout(() => open(), 50);
|
||||
}
|
||||
} catch (e) { /* sessionStorage 不可用就静默放弃 */ }
|
||||
}
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', checkAutoOpen);
|
||||
} else {
|
||||
checkAutoOpen();
|
||||
}
|
||||
})();
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,602 +0,0 @@
|
||||
/* ============================================================
|
||||
流·Studio · Shell renderer V2.1
|
||||
渲染 sidebar / topbar / 网格背景装饰 / Toast / Modal helpers
|
||||
每个页面调用 Shell.render({ active, crumbs, balance, topActions })
|
||||
|
||||
V2.1 变化:
|
||||
- sidebar 搜索 ⌘K → "Ctrl K" Inter Bold 平铺(无 kbd 边框)
|
||||
============================================================ */
|
||||
|
||||
const NAV = [
|
||||
{
|
||||
id: 'dashboard', label: '工作台', href: 'index.html',
|
||||
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 12 12 3l9 9"/><path d="M5 10v10h14V10"/></svg>'
|
||||
},
|
||||
{
|
||||
id: 'products', label: '商品库', href: 'products.html', badge: '7',
|
||||
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="m3 7 9-4 9 4-9 4-9-4z"/><path d="m3 12 9 4 9-4M3 17l9 4 9-4"/></svg>'
|
||||
},
|
||||
{
|
||||
id: 'projects', label: '视频项目', href: 'projects.html', badge: '8',
|
||||
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="4" width="18" height="16"/><path d="M7 4v16M16 4v16M3 9h18M3 15h18"/></svg>'
|
||||
},
|
||||
{
|
||||
id: 'asset-factory', label: '图片生成', href: 'asset-factory.html',
|
||||
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l1.7 4.6L18 9l-4.3 1.4L12 15l-1.7-4.6L6 9l4.3-1.4L12 3z"/><path d="M19 16l.85 2.3L22 19l-2.15.7L19 22l-.85-2.3L16 19l2.15-.7L19 16z"/></svg>'
|
||||
},
|
||||
{
|
||||
id: 'library', label: '资产库', href: 'library.html',
|
||||
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="4" width="6" height="16"/><rect x="11" y="4" width="4" height="16"/><rect x="17" y="6" width="4" height="14"/></svg>'
|
||||
},
|
||||
{
|
||||
id: 'team', label: '团队', href: 'team.html', badge: '5',
|
||||
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="9" cy="8" r="3"/><circle cx="17" cy="9" r="2.5"/><path d="M3 19c0-3 2.7-5 6-5s6 2 6 5M14 19c.5-2.4 2.4-4 5-4 .8 0 1.5.2 2 .5"/></svg>'
|
||||
},
|
||||
{
|
||||
id: 'account', label: '账户', href: 'account.html',
|
||||
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="6" width="18" height="13" rx="2"/><path d="M3 10h18M16 14h2"/></svg>'
|
||||
},
|
||||
{
|
||||
id: 'settings', label: '设置', href: 'settings.html',
|
||||
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8 2 2 0 0 1-2.8 2.8 1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5 2 2 0 0 1-4 0 1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3 2 2 0 0 1-2.8-2.8 1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1 2 2 0 0 1 0-4 1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8 2 2 0 0 1 2.8-2.8 1.7 1.7 0 0 0 1.8.3 1.7 1.7 0 0 0 1-1.5 2 2 0 0 1 4 0 1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3 2 2 0 0 1 2.8 2.8 1.7 1.7 0 0 0-.3 1.8 1.7 1.7 0 0 0 1.5 1 2 2 0 0 1 0 4 1.7 1.7 0 0 0-1.5 1.1Z"/></svg>'
|
||||
}
|
||||
];
|
||||
|
||||
window.Shell = {
|
||||
render({ active = '', crumbs = [], balance = '¥327.40', topActions = '' } = {}) {
|
||||
const navHtml = NAV.map(n => `
|
||||
<a href="${n.href}" class="${active === n.id ? 'active' : ''}">
|
||||
${n.icon}
|
||||
<span>${n.label}</span>
|
||||
${n.badge ? `<span class="pill-mini">${n.badge}</span>` : ''}
|
||||
</a>
|
||||
`).join('');
|
||||
|
||||
const sidebar = `
|
||||
<aside class="sidebar">
|
||||
<div class="brand">
|
||||
<div class="flame"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2c1 3 4 5 4 9a4 4 0 0 1-4 4 4 4 0 0 1-4-4 5 5 0 0 1 1.5-3.5C10.5 6 11.5 4 12 2zm-1 13c0 2 1 3 1 5 1-1 3-2 3-5 0-1.5-1-2-2-3-1 1-2 2-2 3z"/></svg></div>
|
||||
<div><div class="name">流·Studio</div></div>
|
||||
</div>
|
||||
<div class="search-box" onclick="document.getElementById('global-search').focus()">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
|
||||
<input id="global-search" placeholder="搜索"/>
|
||||
<span class="kbd">Ctrl K</span>
|
||||
</div>
|
||||
<div class="nav-section">主要</div>
|
||||
<nav>${navHtml}</nav>
|
||||
<div class="aside-foot">
|
||||
<div class="user" onclick="Shell.toast('账户菜单', 'li@shop.com')">
|
||||
<div class="av">李</div>
|
||||
<div class="em">小李的店</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
`;
|
||||
|
||||
const crumbHtml = crumbs.length ? `
|
||||
<div class="crumbs">
|
||||
${crumbs.map((c, i) => {
|
||||
const last = i === crumbs.length - 1;
|
||||
const sep = i > 0 ? '<span class="sep">/</span>' : '';
|
||||
if (last) return `${sep}<span class="here">${c.label}</span>`;
|
||||
return `${sep}<a href="${c.href || '#'}">${c.label}</a>`;
|
||||
}).join('')}
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
const topbar = `
|
||||
<header class="topbar">
|
||||
${crumbHtml}
|
||||
<div class="right">
|
||||
<span class="balance-chip" onclick="location.href='account.html'">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="9"/><path d="M12 7v10M9 10h4a1.5 1.5 0 0 1 0 3h-3a1.5 1.5 0 0 0 0 3h4"/></svg>
|
||||
余额 <strong>${balance}</strong>
|
||||
</span>
|
||||
<button class="queue-chip" onclick="Shell.toast('任务队列', '3 个进行中')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
|
||||
任务队列
|
||||
<span class="count">3</span>
|
||||
</button>
|
||||
<button class="icon-btn" onclick="Shell.toast('通知中心', '12 条未读')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9M10 21a2 2 0 0 0 4 0"/></svg>
|
||||
<span class="count-noti">12</span>
|
||||
</button>
|
||||
<div class="topbar-avatar" onclick="Shell.toast('账户菜单', '李 · li@shop.com')" title="账户">
|
||||
<span>李</span>
|
||||
</div>
|
||||
${topActions}
|
||||
</div>
|
||||
</header>
|
||||
`;
|
||||
|
||||
const decorations = `
|
||||
<div class="grid-bg"></div>
|
||||
<span class="sq-mark" style="top:238px;left:478px"></span>
|
||||
<span class="sq-mark" style="top:478px;left:1198px"></span>
|
||||
<span class="sq-mark" style="bottom:300px;left:238px"></span>
|
||||
<span class="sq-mark" style="top:718px;right:240px"></span>
|
||||
`;
|
||||
|
||||
const toastHtml = `
|
||||
<div class="toast" id="__toast">
|
||||
<div class="ic-t"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><polyline points="20 6 9 17 4 12"/></svg></div>
|
||||
<div class="txt" id="__toast-txt">操作成功<span class="mono">[ 200 OK ]</span></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// ─── 全局 Lightbox · 任意页面可用 Shell._openLightbox(src, name) ──
|
||||
const lightboxHtml = `
|
||||
<div class="np-lightbox" id="np-lightbox" onclick="Shell._closeLightbox()">
|
||||
<button class="lb-x" type="button" onclick="event.stopPropagation();Shell._closeLightbox()">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
||||
</button>
|
||||
<img id="np-lightbox-img" alt="">
|
||||
<span class="lb-name" id="np-lightbox-name"></span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// [DEPRECATED · 弹窗已废弃,创建商品直接进 product-create.html]
|
||||
const _deprecatedModalHtml = `
|
||||
<div class="modal-bg" id="new-product-bg" onclick="if(event.target===this)Shell.closeModal('new-product-bg')">
|
||||
<div class="modal new-product-modal">
|
||||
<span class="corner-tr"></span>
|
||||
<span class="corner-bl"></span>
|
||||
|
||||
<div class="np-header">
|
||||
<div class="np-title-ic">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="m3 7 9-4 9 4-9 4-9-4z"/><path d="m3 12 9 4 9-4M3 17l9 4 9-4"/></svg>
|
||||
</div>
|
||||
<h2>新建商品</h2>
|
||||
<span class="np-mode-pill">[ UPLOAD MODE ]</span>
|
||||
<button class="np-x" type="button" onclick="Shell.closeModal('new-product-bg')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="np-body">
|
||||
<div class="np-body-grid">
|
||||
|
||||
<!-- 左栏: 图片 -->
|
||||
<div class="np-left">
|
||||
<!-- AI CTA: 独立全宽区域,放在 商品图册 上方 -->
|
||||
<a class="np-ai-cta primary" id="np-ai-cta" title="不上传也能用 AI 生成图">
|
||||
<span class="ai-icon">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l1.5 4.5L18 8l-4.5 1.5L12 14l-1.5-4.5L6 8l4.5-1.5L12 2zM19 14l.9 2.7 2.6.8-2.6.8L19 21l-.9-2.7-2.6-.8 2.6-.8L19 14zM5 14l.7 2.1L7.8 17l-2.1.7L5 20l-.7-2.3-2.1-.7 2.1-.7L5 14z"/></svg>
|
||||
</span>
|
||||
<span class="ai-label" id="np-ai-label">没有图? 让 AI 帮我生成</span>
|
||||
<span class="ai-arrow">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<div class="field" style="margin-bottom: 12px;">
|
||||
<label class="field-label">商品图册<span class="req">*</span></label>
|
||||
<input type="file" id="np-file" accept="image/*" multiple hidden>
|
||||
|
||||
<!-- 图片案例 (静态示例) -->
|
||||
<div class="np-section-h">
|
||||
<span class="check-ic"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 8 7 12 13 4"/></svg></span>
|
||||
图片案例
|
||||
</div>
|
||||
<div class="np-section-sub">上传您的商品的多角度白底图和使用图</div>
|
||||
<div class="np-examples">
|
||||
<div class="ex"><span>白底主图</span><span class="ex-badge"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M2 21h4V9H2v12zM23 10c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg></span></div>
|
||||
<div class="ex"><span>多角度</span><span class="ex-badge"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M2 21h4V9H2v12zM23 10c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg></span></div>
|
||||
<div class="ex"><span>细节图</span><span class="ex-badge"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M2 21h4V9H2v12zM23 10c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg></span></div>
|
||||
<div class="ex"><span>使用图</span><span class="ex-badge"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M2 21h4V9H2v12zM23 10c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg></span></div>
|
||||
<div class="ex"><span>包装图</span><span class="ex-badge"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M2 21h4V9H2v12zM23 10c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg></span></div>
|
||||
</div>
|
||||
|
||||
<!-- 我的上传 (动态 5 槽) -->
|
||||
<div class="np-section-h">
|
||||
我的上传
|
||||
<span class="counter">( <span class="num" id="np-upload-count">0</span> / 5 )</span>
|
||||
</div>
|
||||
<div class="upload-grid" id="np-grid"></div>
|
||||
|
||||
<!-- Dropzone (放在 grid 之后, 满 5 张时禁用) -->
|
||||
<div class="upload-zone" id="np-zone" style="margin-top: 10px;">
|
||||
<span class="uz-ic">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M17 8l-5-5-5 5M12 3v12"/></svg>
|
||||
</span>
|
||||
<span id="np-zone-text">点击或拖拽上传图片</span>
|
||||
<span class="uz-hint">// 支持多选 · 最多 5 张 · JPG / PNG / WEBP</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右栏: 文案 -->
|
||||
<div class="np-right">
|
||||
<div class="field">
|
||||
<label class="field-label">商品名称<span class="req">*</span></label>
|
||||
<input class="input" id="np-name" placeholder="例: 透真玻尿酸补水面膜">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field-label">品类</label>
|
||||
<select class="select" id="np-cat">
|
||||
<option>美妆个护</option>
|
||||
<option>服饰内衣</option>
|
||||
<option>食品饮料</option>
|
||||
<option>家居家电</option>
|
||||
<option>数码 3C</option>
|
||||
<option>个护清洁</option>
|
||||
<option>运动户外</option>
|
||||
<option>母婴亲子</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field-label">核心卖点<span class="req">*</span></label>
|
||||
<ul class="bullet-list" id="np-bullets" data-bl>
|
||||
<li class="bl-add"><span class="num">+</span><input class="bl-input" placeholder="添加新卖点 · 回车确认"></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="field" style="margin-bottom:0;">
|
||||
<label class="field-label">目标人群</label>
|
||||
<input class="input" id="np-target" placeholder="例: 22-32 岁女性、敏感肌、办公室通勤">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="np-footer">
|
||||
<span class="np-meta">// 上传模式 · <span class="accent">不消耗 token</span> · 一次性创建</span>
|
||||
<button class="btn" type="button" onclick="Shell.closeModal('new-product-bg')">取消</button>
|
||||
<button class="btn btn-primary" type="button" id="np-create" disabled style="opacity:.5;cursor:not-allowed;">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l5 5L20 6"/></svg>
|
||||
创建商品
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lightbox · 缩略图全屏预览 -->
|
||||
<div class="np-lightbox" id="np-lightbox" onclick="Shell._closeLightbox()">
|
||||
<button class="lb-x" type="button" onclick="event.stopPropagation();Shell._closeLightbox()">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
||||
</button>
|
||||
<img id="np-lightbox-img" alt="">
|
||||
<span class="lb-name" id="np-lightbox-name"></span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 装订线 SVG 准星 · V2.1 签名元素(圆弧内凹的"+")
|
||||
const cornerSvg = `<path d="M10.5 4C10.5 7.31371 7.81371 10 4.5 10H0.5V11H4.5C7.81371 11 10.5 13.6863 10.5 17V21H11.5V17C11.5 13.6863 14.1863 11 17.5 11H21.5V10H17.5C14.1863 10 11.5 7.31371 11.5 4V0H10.5V4Z" fill="currentColor"/>`;
|
||||
const cornerMarks = `
|
||||
<span class="corner-mark tl"><svg viewBox="0 0 22 21" fill="none">${cornerSvg}</svg></span>
|
||||
<span class="corner-mark tr"><svg viewBox="0 0 22 21" fill="none">${cornerSvg}</svg></span>
|
||||
<span class="corner-mark bl"><svg viewBox="0 0 22 21" fill="none">${cornerSvg}</svg></span>
|
||||
<span class="corner-mark br"><svg viewBox="0 0 22 21" fill="none">${cornerSvg}</svg></span>
|
||||
`;
|
||||
|
||||
const app = document.createElement('div');
|
||||
app.className = 'app';
|
||||
app.innerHTML = sidebar + `<main>${decorations}${topbar}<div class="content" id="page-content">${cornerMarks}</div></main>`;
|
||||
|
||||
const src = document.getElementById('page');
|
||||
document.body.prepend(app);
|
||||
if (src) {
|
||||
// 把页面 body 内容追加到 .content,保留 4 个 corner-mark SVG
|
||||
document.getElementById('page-content').insertAdjacentHTML('beforeend', src.innerHTML);
|
||||
src.remove();
|
||||
}
|
||||
document.body.insertAdjacentHTML('beforeend', toastHtml);
|
||||
document.body.insertAdjacentHTML('beforeend', lightboxHtml);
|
||||
|
||||
document.addEventListener('keydown', e => {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
||||
e.preventDefault();
|
||||
document.getElementById('global-search')?.focus();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// [DEPRECATED] 新建商品弹窗 · 已废弃,创建商品改为直跳 product-create.html
|
||||
_bindNewProductModal_DEPRECATED() {
|
||||
const modal = document.getElementById('new-product-bg');
|
||||
if (!modal) return;
|
||||
|
||||
// 上传图册的内存状态 (供 collect 和 AI CTA 用)
|
||||
const uploads = []; // { id, dataUrl, name, type, size }
|
||||
|
||||
// 收集表单状态(供 AI CTA 和创建按钮用)
|
||||
const collect = () => {
|
||||
const get = (id) => document.getElementById(id)?.value?.trim() || '';
|
||||
return {
|
||||
name: get('np-name'),
|
||||
cat: get('np-cat'),
|
||||
target: get('np-target'),
|
||||
bullets: [...document.querySelectorAll('#np-bullets .bl-item .bl-text')].map(el => el.textContent),
|
||||
uploadedCount: uploads.length,
|
||||
uploads: uploads.map(u => ({ name: u.name, type: u.type })),
|
||||
};
|
||||
};
|
||||
|
||||
const MAX_UPLOADS = 5;
|
||||
|
||||
// 创建按钮: 商品名 + 至少 1 张图
|
||||
const nameInput = document.getElementById('np-name');
|
||||
const createBtn = document.getElementById('np-create');
|
||||
const updateCreateBtn = () => {
|
||||
const nameOk = (nameInput.value || '').trim().length > 0;
|
||||
const uploadOk = uploads.length >= 1;
|
||||
const ok = nameOk && uploadOk;
|
||||
createBtn.disabled = !ok;
|
||||
createBtn.style.opacity = ok ? '' : '.5';
|
||||
createBtn.style.cursor = ok ? '' : 'not-allowed';
|
||||
// 提示用户缺什么
|
||||
if (!nameOk) createBtn.title = '请填写商品名称';
|
||||
else if (!uploadOk) createBtn.title = '请至少上传 1 张商品图';
|
||||
else createBtn.title = '';
|
||||
};
|
||||
nameInput.addEventListener('input', updateCreateBtn);
|
||||
createBtn.addEventListener('click', () => {
|
||||
if (createBtn.disabled) return;
|
||||
const s = collect();
|
||||
Shell.toast('商品已创建', `+ ${s.name}`);
|
||||
Shell.closeModal('new-product-bg');
|
||||
});
|
||||
|
||||
// 核心卖点: bullet-list
|
||||
const list = document.getElementById('np-bullets');
|
||||
const addInput = list.querySelector('.bl-add .bl-input');
|
||||
const xSvg = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M4 4l8 8M12 4l-8 8"/></svg>';
|
||||
const renumber = () => {
|
||||
[...list.querySelectorAll('.bl-item')].forEach((li, i) => {
|
||||
li.querySelector('.num').textContent = i + 1;
|
||||
});
|
||||
};
|
||||
const bindX = (x) => {
|
||||
x.addEventListener('click', () => {
|
||||
const li = x.closest('li');
|
||||
li.style.transition = 'opacity .15s, transform .15s';
|
||||
li.style.opacity = 0;
|
||||
li.style.transform = 'translateX(-8px)';
|
||||
setTimeout(() => { li.remove(); renumber(); }, 150);
|
||||
});
|
||||
};
|
||||
const addBullet = (text) => {
|
||||
const t = (text || '').trim();
|
||||
if (!t) return;
|
||||
const li = document.createElement('li');
|
||||
li.className = 'bl-item';
|
||||
li.innerHTML = `<span class="num">0</span><span class="bl-text">${t.replace(/[<>&]/g, c => ({ '<':'<','>':'>','&':'&' })[c])}</span><span class="bl-x" title="删除">${xSvg}</span>`;
|
||||
list.querySelector('.bl-add').before(li);
|
||||
bindX(li.querySelector('.bl-x'));
|
||||
renumber();
|
||||
};
|
||||
addInput?.addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
addBullet(addInput.value);
|
||||
addInput.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
// 上传组件: 批量上传(MAX 5) + 5 固定槽 + 预览/删除 + AI CTA 联动
|
||||
const zone = document.getElementById('np-zone');
|
||||
const zoneText = document.getElementById('np-zone-text');
|
||||
const grid = document.getElementById('np-grid');
|
||||
const fileInput = document.getElementById('np-file');
|
||||
const aiCtaEl = document.getElementById('np-ai-cta');
|
||||
const aiLabel = document.getElementById('np-ai-label');
|
||||
const uploadCount = document.getElementById('np-upload-count');
|
||||
|
||||
const syncAiCta = () => {
|
||||
if (uploads.length === 0) {
|
||||
aiCtaEl.classList.add('primary');
|
||||
aiLabel.textContent = '没有图? 让 AI 帮我生成';
|
||||
} else {
|
||||
aiCtaEl.classList.remove('primary');
|
||||
aiLabel.textContent = '用 AI 加工 / 生成模特上身图';
|
||||
}
|
||||
};
|
||||
|
||||
const syncZone = () => {
|
||||
const full = uploads.length >= MAX_UPLOADS;
|
||||
zone.classList.toggle('full', full);
|
||||
zoneText.textContent = full ? `已达上限 (${MAX_UPLOADS} / ${MAX_UPLOADS})` : '点击或拖拽上传图片';
|
||||
};
|
||||
|
||||
const syncCounter = () => {
|
||||
uploadCount.textContent = uploads.length;
|
||||
};
|
||||
|
||||
const thumbXSvg = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 4l8 8M12 4l-8 8"/></svg>';
|
||||
const esc = (s) => s.replace(/[<>&"]/g, c => ({ '<':'<','>':'>','&':'&','"':'"' })[c]);
|
||||
|
||||
const renderGrid = () => {
|
||||
const filledHtml = uploads.map(u => `
|
||||
<div class="up-thumb" data-id="${u.id}">
|
||||
<img src="${u.dataUrl}" alt="${esc(u.name)}">
|
||||
<button class="slot-x" type="button" title="删除">${thumbXSvg}</button>
|
||||
</div>
|
||||
`).join('');
|
||||
const emptyCount = MAX_UPLOADS - uploads.length;
|
||||
const emptyHtml = Array.from({ length: emptyCount }, () => `<div class="up-slot-empty" data-action="add">无预览</div>`).join('');
|
||||
grid.innerHTML = filledHtml + emptyHtml;
|
||||
|
||||
// 已填充槽: 点击预览 + X 删除
|
||||
grid.querySelectorAll('.up-thumb').forEach(thumb => {
|
||||
const id = thumb.dataset.id;
|
||||
thumb.querySelector('.slot-x').addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const idx = uploads.findIndex(u => u.id === id);
|
||||
if (idx >= 0) {
|
||||
const removed = uploads.splice(idx, 1)[0];
|
||||
Shell.toast('已删除', removed.name);
|
||||
refreshAll();
|
||||
}
|
||||
});
|
||||
thumb.addEventListener('click', () => {
|
||||
const u = uploads.find(x => x.id === id);
|
||||
if (u) Shell._openLightbox(u.dataUrl, u.name);
|
||||
});
|
||||
});
|
||||
// 空槽: 点击触发上传
|
||||
grid.querySelectorAll('[data-action="add"]').forEach(slot => {
|
||||
slot.addEventListener('click', () => fileInput.click());
|
||||
});
|
||||
};
|
||||
|
||||
const refreshAll = () => {
|
||||
renderGrid();
|
||||
syncAiCta();
|
||||
syncZone();
|
||||
syncCounter();
|
||||
updateCreateBtn();
|
||||
};
|
||||
|
||||
const addFiles = (fileList) => {
|
||||
const remaining = MAX_UPLOADS - uploads.length;
|
||||
if (remaining <= 0) {
|
||||
Shell.toast('已达上传上限', `${MAX_UPLOADS} / ${MAX_UPLOADS} 张`);
|
||||
return;
|
||||
}
|
||||
const incoming = [...fileList].filter(f => f.type.startsWith('image/'));
|
||||
if (!incoming.length) return;
|
||||
const accepted = incoming.slice(0, remaining);
|
||||
const overflow = incoming.length - accepted.length;
|
||||
let added = 0;
|
||||
accepted.forEach(f => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
uploads.push({
|
||||
id: Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
|
||||
dataUrl: e.target.result,
|
||||
name: f.name,
|
||||
type: f.type,
|
||||
size: f.size,
|
||||
});
|
||||
added++;
|
||||
if (added === accepted.length) {
|
||||
refreshAll();
|
||||
const msg = overflow > 0
|
||||
? `+ ${added} 张 · 超出 ${overflow} 张已忽略`
|
||||
: `+ ${added} 张 · 共 ${uploads.length} / ${MAX_UPLOADS}`;
|
||||
Shell.toast('图片已上传', msg);
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(f);
|
||||
});
|
||||
};
|
||||
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
addFiles(e.target.files);
|
||||
e.target.value = '';
|
||||
});
|
||||
zone.addEventListener('click', () => {
|
||||
if (uploads.length < MAX_UPLOADS) fileInput.click();
|
||||
});
|
||||
zone.addEventListener('dragover', e => {
|
||||
e.preventDefault();
|
||||
if (uploads.length < MAX_UPLOADS) zone.style.borderColor = 'var(--heat)';
|
||||
});
|
||||
zone.addEventListener('dragleave', () => { zone.style.borderColor = ''; });
|
||||
zone.addEventListener('drop', e => {
|
||||
e.preventDefault();
|
||||
zone.style.borderColor = '';
|
||||
if (e.dataTransfer?.files?.length) addFiles(e.dataTransfer.files);
|
||||
});
|
||||
|
||||
refreshAll();
|
||||
|
||||
// AI CTA: 把已填表单 + 上传图册存到 sessionStorage,跳到 AI 工作台
|
||||
const aiCta = document.getElementById('np-ai-cta');
|
||||
aiCta.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const s = collect();
|
||||
sessionStorage.setItem('pending-product', JSON.stringify(s));
|
||||
Shell.toast('正在跳转 AI 工作台', `带入 ${s.uploadedCount} 张图`);
|
||||
setTimeout(() => { location.href = 'product-create.html'; }, 350);
|
||||
});
|
||||
|
||||
updateCreateBtn();
|
||||
},
|
||||
|
||||
_openLightbox(src, name) {
|
||||
const lb = document.getElementById('np-lightbox');
|
||||
const img = document.getElementById('np-lightbox-img');
|
||||
const nm = document.getElementById('np-lightbox-name');
|
||||
if (!lb || !img || lb.classList.contains('show')) return;
|
||||
img.src = src;
|
||||
if (nm) nm.textContent = name || '';
|
||||
lb.classList.add('show');
|
||||
this.lockScroll();
|
||||
},
|
||||
|
||||
_closeLightbox() {
|
||||
const lb = document.getElementById('np-lightbox');
|
||||
if (!lb || !lb.classList.contains('show')) return;
|
||||
lb.classList.remove('show');
|
||||
this.unlockScroll();
|
||||
},
|
||||
|
||||
toast(text, mono) {
|
||||
const t = document.getElementById('__toast');
|
||||
const txt = document.getElementById('__toast-txt');
|
||||
if (!t || !txt) return;
|
||||
txt.innerHTML = text + (mono ? `<span class="mono">[ ${mono} ]</span>` : '');
|
||||
t.classList.add('show');
|
||||
clearTimeout(this._tt);
|
||||
this._tt = setTimeout(() => t.classList.remove('show'), 2400);
|
||||
},
|
||||
|
||||
/* ─── Body scroll lock (引用计数 · 多 overlay 叠加安全) ─── */
|
||||
_scrollLockCount: 0,
|
||||
_scrollLockSnapshot: null,
|
||||
lockScroll() {
|
||||
if (++this._scrollLockCount > 1) return;
|
||||
const docEl = document.documentElement;
|
||||
const sbw = window.innerWidth - docEl.clientWidth;
|
||||
this._scrollLockSnapshot = {
|
||||
bodyOverflow: document.body.style.overflow,
|
||||
bodyPaddingRight: document.body.style.paddingRight,
|
||||
htmlOverflow: docEl.style.overflow,
|
||||
};
|
||||
document.body.style.overflow = 'hidden';
|
||||
docEl.style.overflow = 'hidden';
|
||||
if (sbw > 0) document.body.style.paddingRight = sbw + 'px';
|
||||
},
|
||||
unlockScroll() {
|
||||
if (--this._scrollLockCount > 0) return;
|
||||
this._scrollLockCount = 0;
|
||||
const s = this._scrollLockSnapshot;
|
||||
if (s) {
|
||||
document.body.style.overflow = s.bodyOverflow;
|
||||
document.body.style.paddingRight = s.bodyPaddingRight;
|
||||
document.documentElement.style.overflow = s.htmlOverflow;
|
||||
this._scrollLockSnapshot = null;
|
||||
}
|
||||
},
|
||||
|
||||
openModal(id) {
|
||||
const el = document.getElementById(id);
|
||||
if (!el || el.classList.contains('show')) return;
|
||||
el.classList.add('show');
|
||||
this.lockScroll();
|
||||
},
|
||||
closeModal(id) {
|
||||
const el = document.getElementById(id);
|
||||
if (!el || !el.classList.contains('show')) return;
|
||||
el.classList.remove('show');
|
||||
this.unlockScroll();
|
||||
},
|
||||
openDrawer(id) {
|
||||
const el = document.getElementById(id);
|
||||
const bg = document.getElementById(id + '-bg');
|
||||
if (!el || el.classList.contains('show')) return;
|
||||
el.classList.add('show');
|
||||
if (bg) bg.classList.add('show');
|
||||
this.lockScroll();
|
||||
},
|
||||
closeDrawer(id) {
|
||||
const el = document.getElementById(id);
|
||||
const bg = document.getElementById(id + '-bg');
|
||||
if (!el || !el.classList.contains('show')) return;
|
||||
el.classList.remove('show');
|
||||
if (bg) bg.classList.remove('show');
|
||||
this.unlockScroll();
|
||||
}
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
176
v2/index.html
176
v2/index.html
@ -1,176 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>工作台 · 流·Studio</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="assets/restraint.css">
|
||||
<style>
|
||||
.dash-grid { display: grid; grid-template-columns: 1.7fr 1fr; gap: 24px; align-items: start; }
|
||||
.recent-row { display: grid; grid-template-columns: 54px 1fr 110px 130px 60px; align-items: center; gap: 16px; padding: 14px 18px; border-bottom: 1px solid var(--border-faint); cursor: pointer; }
|
||||
.recent-row .prog, .recent-row .pill, .recent-row .btn { justify-self: start; }
|
||||
.recent-row:last-child { border-bottom: 0; }
|
||||
.recent-row:hover { background: var(--background-lighter); }
|
||||
.recent-row .thumb { width: 54px; height: 70px; border-radius: var(--r-md); }
|
||||
.recent-meta .name { font-weight: 600; font-size: 13.5px; color: var(--accent-black); }
|
||||
.recent-meta .sub { font-size: 12px; color: var(--black-alpha-48); margin-top: 3px; font-family: var(--font-mono); letter-spacing: .01em; }
|
||||
.shortcuts { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||||
.shortcut { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 16px; display: flex; align-items: flex-start; gap: 12px; cursor: pointer; transition: background var(--t-base); }
|
||||
.shortcut:hover { background: var(--black-alpha-4); }
|
||||
.shortcut .ic { width: 32px; height: 32px; background: var(--heat-12); color: var(--heat); display: grid; place-items: center; border-radius: var(--r-md); flex-shrink: 0; }
|
||||
.shortcut .ic svg { width: 16px; height: 16px; }
|
||||
.shortcut .t { font-size: 13px; font-weight: 600; }
|
||||
.shortcut .d { font-size: 11.5px; color: var(--black-alpha-48); margin-top: 3px; font-family: var(--font-mono); letter-spacing: .01em; }
|
||||
.tip { background: var(--surface); border: 1px dashed var(--border-faint); padding: 14px 16px; font-size: 12.5px; color: var(--black-alpha-56); line-height: 1.6; border-radius: var(--r-md); }
|
||||
.tip strong { color: var(--accent-black); font-weight: 600; display: block; margin-bottom: 4px; }
|
||||
.tip .mono { font-family: var(--font-mono); color: var(--heat); background: var(--heat-12); padding: 1px 5px; border-radius: var(--r-sm); font-size: 11.5px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="page">
|
||||
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1>欢迎回来,小李</h1>
|
||||
<div class="sub">
|
||||
<span class="mono">// 05.14 · 周三</span>
|
||||
<span>·</span>
|
||||
<span>你有 <b style="color:var(--accent-black)">3 个项目</b> 正在进行中</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<a class="btn" href="javascript:void(0)" onclick="event.preventDefault(); window.NewProductDrawer && NewProductDrawer.open();">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
|
||||
新建商品
|
||||
</a>
|
||||
<a class="btn btn-primary btn-lg" href="projects-new.html">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
|
||||
新建项目
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats with-corners">
|
||||
<span class="corner-tr" aria-hidden></span><span class="corner-bl" aria-hidden></span>
|
||||
<a class="stat" href="projects.html">
|
||||
<div class="lbl">总项目 <span class="badge">ALL</span></div>
|
||||
<div class="v">8</div>
|
||||
<div class="delta up"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V5M5 12l7-7 7 7"/></svg> 本月 +3</div>
|
||||
</a>
|
||||
<a class="stat" href="projects.html">
|
||||
<div class="lbl">进行中 <span class="badge">WIP</span></div>
|
||||
<div class="v">3</div>
|
||||
<div class="delta">2 个待审核</div>
|
||||
</a>
|
||||
<a class="stat" href="projects.html">
|
||||
<div class="lbl">本月成片 <span class="badge">DONE</span></div>
|
||||
<div class="v">3</div>
|
||||
<div class="delta up"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V5M5 12l7-7 7 7"/></svg> 较上月 +33%</div>
|
||||
</a>
|
||||
<a class="stat" href="account.html">
|
||||
<div class="lbl">余额 <span class="badge">¥</span></div>
|
||||
<div class="v">¥327<small>.40</small></div>
|
||||
<div class="bar"><span style="width:33%"></span></div>
|
||||
<div class="sub">已用 ¥162.60 / ¥500</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="dash-grid">
|
||||
<div>
|
||||
<div class="section-h">
|
||||
<h2>最近项目</h2>
|
||||
<a class="more" href="projects.html">[ ALL · 8 ] <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle;"><path d="M5 12h14M12 5l7 7-7 7"/></svg></a>
|
||||
</div>
|
||||
<div class="card-hard">
|
||||
<a class="recent-row" href="pipeline.html#stage-3">
|
||||
<div class="placeholder thumb"><span class="ph-frame">9:16</span></div>
|
||||
<div class="recent-meta">
|
||||
<div class="name">补水面膜 · 痛点种草</div>
|
||||
<div class="sub">补水面膜 / AI 全生 / 6 镜</div>
|
||||
</div>
|
||||
<div class="prog"><span class="done"></span><span class="done"></span><span class="cur"></span><span></span><span></span></div>
|
||||
<span class="pill info"><span class="dot"></span>故事板 待确认</span>
|
||||
<span class="btn btn-sm">继续</span>
|
||||
</a>
|
||||
<a class="recent-row" href="pipeline.html#stage-5">
|
||||
<div class="placeholder thumb"><span class="ph-frame">9:16</span></div>
|
||||
<div class="recent-meta">
|
||||
<div class="name">蓝牙耳机 · 开箱测评</div>
|
||||
<div class="sub">南卡 Lite Pro / 自带脚本 / 5 镜</div>
|
||||
</div>
|
||||
<div class="prog"><span class="done"></span><span class="done"></span><span class="done"></span><span class="done"></span><span class="done"></span></div>
|
||||
<span class="pill ok"><span class="dot"></span>已完成</span>
|
||||
<span class="btn btn-sm">打开</span>
|
||||
</a>
|
||||
<a class="recent-row" href="pipeline.html#stage-2">
|
||||
<div class="placeholder thumb"><span class="ph-frame">9:16</span></div>
|
||||
<div class="recent-meta">
|
||||
<div class="name">速食牛肉面 · 一句话主题</div>
|
||||
<div class="sub">滋啦速食 / 一句话 / 4 镜</div>
|
||||
</div>
|
||||
<div class="prog"><span class="done"></span><span class="cur"></span><span></span><span></span><span></span></div>
|
||||
<span class="pill info"><span class="dot"></span>资产生成中</span>
|
||||
<span class="btn btn-sm">继续</span>
|
||||
</a>
|
||||
<a class="recent-row" href="pipeline.html#stage-4">
|
||||
<div class="placeholder thumb"><span class="ph-frame">9:16</span></div>
|
||||
<div class="recent-meta">
|
||||
<div class="name">防晒霜 · 对比展示</div>
|
||||
<div class="sub">透真防晒 / AI 全生 / 6 镜</div>
|
||||
</div>
|
||||
<div class="prog"><span class="done"></span><span class="done"></span><span class="done"></span><span class="cur"></span><span></span></div>
|
||||
<span class="pill info"><span class="dot"></span>视频生成 4/6</span>
|
||||
<span class="btn btn-sm">继续</span>
|
||||
</a>
|
||||
<a class="recent-row" href="pipeline.html#stage-3">
|
||||
<div class="placeholder thumb"><span class="ph-frame">9:16</span></div>
|
||||
<div class="recent-meta">
|
||||
<div class="name">咖啡冻干粉 · 剧情带货</div>
|
||||
<div class="sub">三顿半同款 / 一句话 / 5 镜</div>
|
||||
</div>
|
||||
<div class="prog"><span class="done"></span><span class="done"></span><span class="fail"></span><span></span><span></span></div>
|
||||
<span class="pill err"><span class="dot"></span>故事板失败</span>
|
||||
<span class="btn btn-sm">查看</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;flex-direction:column;gap:24px">
|
||||
<div>
|
||||
<div class="section-h"><h2>快捷入口</h2><span class="more">[ /shortcuts ]</span></div>
|
||||
<div class="shortcuts">
|
||||
<a class="shortcut" href="products.html">
|
||||
<div class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="m3 7 9-4 9 4-9 4-9-4z"/><path d="m3 12 9 4 9-4"/></svg></div>
|
||||
<div><div class="t">商品库</div><div class="d">7 SKU</div></div>
|
||||
</a>
|
||||
<a class="shortcut" href="library.html">
|
||||
<div class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="6" height="16"/><rect x="11" y="4" width="4" height="16"/><rect x="17" y="6" width="4" height="14"/></svg></div>
|
||||
<div><div class="t">资产库</div><div class="d">人 8 · 景 14 · 片 8</div></div>
|
||||
</a>
|
||||
<a class="shortcut" href="account.html">
|
||||
<div class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 7v10M9 10h4a1.5 1.5 0 0 1 0 3h-3a1.5 1.5 0 0 0 0 3h4"/></svg></div>
|
||||
<div><div class="t">充值</div><div class="d">¥327.40</div></div>
|
||||
</a>
|
||||
<a class="shortcut" href="projects.html">
|
||||
<div class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16"/><path d="M7 4v16M16 4v16M3 9h18M3 15h18"/></svg></div>
|
||||
<div><div class="t">所有项目</div><div class="d">8 个</div></div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="section-h"><h2>提示</h2><span class="more">[ FAQ ]</span></div>
|
||||
<div class="tip">
|
||||
<strong>扣费规则</strong>
|
||||
生成失败、超时、用户重跑 — 均不扣费。仅在你点 <span class="mono">[ 确认通过 ]</span> 时按 token 实际结算。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="assets/shell.js"></script>
|
||||
<script src="assets/new-product-drawer.js"></script>
|
||||
<script>Shell.render({ active: 'dashboard', crumbs: [{ label: '工作台' }] });</script>
|
||||
</body>
|
||||
</html>
|
||||
1805
v2/library.html
1805
v2/library.html
File diff suppressed because it is too large
Load Diff
1961
v2/model-photo.html
1961
v2/model-photo.html
File diff suppressed because it is too large
Load Diff
1737
v2/pipeline.html
1737
v2/pipeline.html
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,14 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>新建商品 · 跳转中...</title>
|
||||
<script>
|
||||
// 已废弃 · 新建商品改为 products.html 上的居中弹窗(Shell.openNewProduct)
|
||||
// 直接访问此 URL 时,跳回商品库并自动打开弹窗
|
||||
sessionStorage.setItem('auto-open-new-product', '1');
|
||||
location.replace('products.html');
|
||||
</script>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
@ -1,413 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>新建商品 · 流·Studio</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="assets/restraint.css">
|
||||
<style>
|
||||
/* ─── 主表单 ─── */
|
||||
.form-grid {
|
||||
display: grid; grid-template-columns: 1.05fr 1fr; gap: 24px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.form-card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-md);
|
||||
padding: 24px;
|
||||
}
|
||||
.form-card .card-h {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.form-card .card-h h3 {
|
||||
font-size: 14px; font-weight: 600; color: var(--accent-black);
|
||||
}
|
||||
.form-card .card-h .req-tag {
|
||||
font-family: var(--font-mono); font-size: 10px;
|
||||
padding: 2px 7px;
|
||||
background: var(--crimson-bg); color: var(--accent-crimson);
|
||||
border-radius: var(--r-sm); letter-spacing: .04em;
|
||||
border: 1px solid var(--red-bd);
|
||||
}
|
||||
.form-card .card-h .opt-tag {
|
||||
font-family: var(--font-mono); font-size: 10px;
|
||||
padding: 2px 7px;
|
||||
background: var(--background-lighter); color: var(--black-alpha-56);
|
||||
border-radius: var(--r-sm); letter-spacing: .04em;
|
||||
border: 1px solid var(--border-faint);
|
||||
}
|
||||
.form-card .card-sub {
|
||||
font-family: var(--font-mono); font-size: 11.5px;
|
||||
color: var(--black-alpha-48); margin: -10px 0 14px; letter-spacing: .02em;
|
||||
}
|
||||
|
||||
/* 原图槽位 */
|
||||
.photo-grid {
|
||||
display: grid; grid-template-columns: repeat(5, 1fr); gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.photo-slot {
|
||||
aspect-ratio: 1;
|
||||
border-radius: var(--r-md);
|
||||
border: 1px dashed var(--border-faint);
|
||||
background: var(--background-lighter);
|
||||
display: grid; place-items: center;
|
||||
color: var(--black-alpha-32);
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
font-size: 10px; font-family: var(--font-mono); letter-spacing: .04em;
|
||||
transition: all var(--t-base);
|
||||
}
|
||||
.photo-slot:hover { border-color: var(--heat); color: var(--heat); background: var(--heat-8); }
|
||||
.photo-slot.filled {
|
||||
border-style: solid;
|
||||
background-size: cover; background-position: center;
|
||||
cursor: default; color: transparent;
|
||||
}
|
||||
.photo-slot.filled:hover { border-color: var(--heat-40); }
|
||||
.photo-slot .slot-label {
|
||||
position: absolute; top: 5px; left: 5px;
|
||||
font-family: var(--font-mono); font-size: 9.5px; font-weight: 600;
|
||||
padding: 2px 6px;
|
||||
background: rgba(255,255,255,.92); color: var(--black-alpha-72);
|
||||
border-radius: var(--r-sm); letter-spacing: .04em;
|
||||
}
|
||||
.photo-slot .slot-main {
|
||||
position: absolute; top: 5px; right: 5px;
|
||||
font-family: var(--font-mono); font-size: 9.5px; font-weight: 600;
|
||||
padding: 2px 6px; background: var(--heat); color: #fff;
|
||||
border-radius: var(--r-sm); letter-spacing: .04em;
|
||||
}
|
||||
.photo-slot .slot-x {
|
||||
position: absolute; top: 4px; right: 4px;
|
||||
width: 18px; height: 18px;
|
||||
border-radius: 999px;
|
||||
background: rgba(21,20,15,.7); color: #fff;
|
||||
display: none; place-items: center; cursor: pointer; border: 0;
|
||||
}
|
||||
.photo-slot.filled:hover .slot-x { display: grid; }
|
||||
.photo-slot.filled:hover .slot-main { display: none; }
|
||||
.photo-slot .slot-x svg { width: 9px; height: 9px; }
|
||||
.photo-slot .plus { width: 22px; height: 22px; border: 1px solid currentColor; border-radius: var(--r-sm); display: grid; place-items: center; margin-bottom: 4px; }
|
||||
.photo-slot .plus svg { width: 12px; height: 12px; }
|
||||
|
||||
.upload-tip {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
margin-top: 14px; padding: 10px 12px;
|
||||
background: var(--heat-8); border: 1px dashed var(--heat-40);
|
||||
border-radius: var(--r-md);
|
||||
font-size: 12px; color: var(--accent-black); line-height: 1.5;
|
||||
}
|
||||
.upload-tip svg { width: 14px; height: 14px; color: var(--heat); flex-shrink: 0; }
|
||||
.upload-tip strong { color: var(--heat); font-weight: 600; }
|
||||
|
||||
/* AI 提示 banner(选填字段说明) */
|
||||
.ai-tip {
|
||||
margin-top: -6px; margin-bottom: 16px;
|
||||
padding: 10px 12px;
|
||||
background: var(--background-lighter);
|
||||
border-radius: var(--r-md);
|
||||
border: 1px dashed var(--border-faint);
|
||||
display: flex; align-items: flex-start; gap: 8px;
|
||||
font-size: 12px; color: var(--black-alpha-72); line-height: 1.55;
|
||||
}
|
||||
.ai-tip svg { width: 13px; height: 13px; color: var(--heat); flex-shrink: 0; margin-top: 2px; }
|
||||
.ai-tip strong { color: var(--accent-black); font-weight: 600; }
|
||||
|
||||
/* 卖点 bullet 输入 */
|
||||
.sell-list { list-style: none; margin: 0; padding: 0; }
|
||||
.sell-list li {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
padding: 8px 10px;
|
||||
background: var(--background-lighter);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-md);
|
||||
margin-bottom: 6px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.sell-list li.add { background: var(--surface); border-style: dashed; }
|
||||
.sell-list .num {
|
||||
width: 18px; height: 18px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-sm);
|
||||
font-size: 10.5px; font-family: var(--font-mono);
|
||||
color: var(--black-alpha-56);
|
||||
display: grid; place-items: center; flex-shrink: 0;
|
||||
}
|
||||
.sell-list li.add .num { background: transparent; color: var(--heat); border-color: var(--heat-40); }
|
||||
.sell-list .txt { flex: 1; min-width: 0; }
|
||||
.sell-list .bl-input {
|
||||
flex: 1; border: 0; background: transparent;
|
||||
font-size: 13px; color: var(--accent-black); padding: 0;
|
||||
font-family: inherit;
|
||||
}
|
||||
.sell-list .bl-input::placeholder { color: var(--black-alpha-48); }
|
||||
.sell-list .bl-x {
|
||||
width: 20px; height: 20px;
|
||||
display: grid; place-items: center;
|
||||
color: var(--black-alpha-48); cursor: pointer;
|
||||
background: transparent; border: 0; opacity: 0;
|
||||
transition: opacity var(--t-base);
|
||||
}
|
||||
.sell-list li:hover .bl-x { opacity: 1; }
|
||||
.sell-list .bl-x:hover { color: var(--accent-crimson); }
|
||||
.sell-list .bl-x svg { width: 11px; height: 11px; }
|
||||
|
||||
/* 底部操作行 */
|
||||
.form-foot {
|
||||
position: sticky; bottom: 0;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-md);
|
||||
padding: 14px 22px;
|
||||
display: flex; align-items: center; gap: 14px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.form-foot .req-info {
|
||||
font-family: var(--font-mono); font-size: 11.5px;
|
||||
color: var(--black-alpha-48); letter-spacing: .02em;
|
||||
}
|
||||
.form-foot .req-info .ok { color: var(--accent-forest); }
|
||||
.form-foot .req-info .miss { color: var(--accent-crimson); }
|
||||
.form-foot .actions {
|
||||
margin-left: auto;
|
||||
display: flex; gap: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="page">
|
||||
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1>新建商品</h1>
|
||||
<div class="sub"><span class="mono">// 上传原图 + 填写基本信息</span> · 保存后可在工作台逐步丰富素材</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============ 表单 ============ -->
|
||||
<div class="form-grid">
|
||||
|
||||
<!-- 左:原图 -->
|
||||
<div class="form-card">
|
||||
<div class="card-h">
|
||||
<h3>商品原图</h3>
|
||||
<span class="req-tag">必填</span>
|
||||
</div>
|
||||
<div class="card-sub">// 1-5 张 · 这是后续所有 AI 生成的源材料</div>
|
||||
<input type="file" id="photo-input" accept="image/*" multiple hidden>
|
||||
<div class="photo-grid" id="photo-grid"></div>
|
||||
<div class="upload-tip">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="9"/><path d="M12 8v4M12 16h.01"/></svg>
|
||||
<span>建议上传 <strong>正面 / 侧面 / 细节 / 包装</strong> 4 张,后续在工作台生成的<strong>白底三视图</strong>更准确。</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右:基本信息 -->
|
||||
<div class="form-card">
|
||||
<div class="card-h">
|
||||
<h3>基本信息</h3>
|
||||
<span class="req-tag">必填</span>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field-label">商品名称<span class="req">*</span></label>
|
||||
<input class="input" id="p-name" placeholder="例: 透真玻尿酸补水面膜">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field-label">品类<span class="req">*</span></label>
|
||||
<select class="select" id="p-cat">
|
||||
<option value="">— 选择品类 —</option>
|
||||
<option>美妆个护</option>
|
||||
<option>服饰内衣</option>
|
||||
<option>食品饮料</option>
|
||||
<option>家居家电</option>
|
||||
<option>数码 3C</option>
|
||||
<option>个护清洁</option>
|
||||
<option>运动户外</option>
|
||||
<option>母婴亲子</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field" style="margin-bottom:0;">
|
||||
<label class="field-label">价格(元)</label>
|
||||
<input class="input" id="p-price" type="number" placeholder="选填 · 仅用于素材生成参考">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="form-card" style="margin-bottom: 24px;">
|
||||
<div class="card-h">
|
||||
<h3>卖点 & 人群</h3>
|
||||
<span class="opt-tag">选填 · 推荐</span>
|
||||
</div>
|
||||
<div class="ai-tip">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l1.5 4.5L18 8l-4.5 1.5L12 14l-1.5-4.5L6 8l4.5-1.5L12 2z"/></svg>
|
||||
<span>填上这两项,后续 AI 生脚本(<strong>痛点种草 / 剧情带货</strong> 等模板)质量明显更高 —— 系统会用卖点构造钩子,用人群定语气。现在不填也可以,做视频项目时仍可补。</span>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field-label">核心卖点</label>
|
||||
<div class="field-hint" style="margin: 4px 0 8px;">3-5 条要点,回车添加</div>
|
||||
<ul class="sell-list" id="sell-list">
|
||||
<li class="add"><span class="num">+</span><input class="bl-input" id="sell-input" placeholder="例: 玻尿酸双效保湿,4 小时持久水润"></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="field" style="margin-bottom:0;">
|
||||
<label class="field-label">目标人群</label>
|
||||
<input class="input" id="p-target" placeholder="例: 22-32 岁女性、敏感肌、办公室通勤">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============ 底部操作 ============ -->
|
||||
<div class="form-foot">
|
||||
<span class="req-info" id="req-info">// 必填检查:<span class="miss">商品名 / 品类 / ≥1 张图</span> 未完成</span>
|
||||
<div class="actions">
|
||||
<a class="btn" href="products.html">取消</a>
|
||||
<button class="btn btn-primary" id="save-btn" disabled>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l5 5L20 6"/></svg>
|
||||
保存并进入工作台
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<script src="assets/shell.js"></script>
|
||||
<script>
|
||||
Shell.render({
|
||||
active: 'products',
|
||||
crumbs: [
|
||||
{ label: '工作台', href: 'index.html' },
|
||||
{ label: '商品库', href: 'products.html' },
|
||||
{ label: '新建' }
|
||||
]
|
||||
});
|
||||
|
||||
const MAX = 5;
|
||||
const photos = []; // { id, dataUrl }
|
||||
const SLOT_LABELS = ['主图', '细节 02', '细节 03', '细节 04', '细节 05'];
|
||||
|
||||
const $ = id => document.getElementById(id);
|
||||
|
||||
// 渲染槽位
|
||||
function renderPhotos() {
|
||||
const grid = $('photo-grid');
|
||||
grid.innerHTML = '';
|
||||
for (let i = 0; i < MAX; i++) {
|
||||
const slot = document.createElement('div');
|
||||
const p = photos[i];
|
||||
if (p) {
|
||||
slot.className = 'photo-slot filled';
|
||||
slot.style.backgroundImage = `url("${p.dataUrl}")`;
|
||||
slot.innerHTML = `
|
||||
<span class="slot-label">${SLOT_LABELS[i]}</span>
|
||||
${i === 0 ? '<span class="slot-main">MAIN</span>' : ''}
|
||||
<button class="slot-x" type="button" data-i="${i}" aria-label="移除">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 4l8 8M12 4l-8 8"/></svg>
|
||||
</button>
|
||||
`;
|
||||
} else if (i === photos.length) {
|
||||
slot.className = 'photo-slot empty';
|
||||
slot.innerHTML = `<div class="plus"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 5v14M5 12h14"/></svg></div><span>添加</span>`;
|
||||
slot.addEventListener('click', () => $('photo-input').click());
|
||||
} else {
|
||||
slot.className = 'photo-slot';
|
||||
slot.style.opacity = '.6';
|
||||
}
|
||||
grid.appendChild(slot);
|
||||
}
|
||||
// 绑定删除
|
||||
grid.querySelectorAll('.slot-x').forEach(b => {
|
||||
b.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
const i = +b.dataset.i;
|
||||
photos.splice(i, 1);
|
||||
renderPhotos();
|
||||
syncSave();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 文件上传
|
||||
$('photo-input').addEventListener('change', e => {
|
||||
const files = [...e.target.files].filter(f => f.type.startsWith('image/'));
|
||||
const remain = MAX - photos.length;
|
||||
files.slice(0, remain).forEach(f => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = ev => {
|
||||
photos.push({
|
||||
id: Date.now().toString(36) + Math.random().toString(36).slice(2, 5),
|
||||
dataUrl: ev.target.result,
|
||||
});
|
||||
renderPhotos();
|
||||
syncSave();
|
||||
};
|
||||
reader.readAsDataURL(f);
|
||||
});
|
||||
e.target.value = '';
|
||||
});
|
||||
|
||||
// 卖点
|
||||
const sellList = $('sell-list');
|
||||
const sellInput = $('sell-input');
|
||||
sellInput.addEventListener('keydown', e => {
|
||||
if (e.key !== 'Enter') return;
|
||||
e.preventDefault();
|
||||
const t = sellInput.value.trim();
|
||||
if (!t) return;
|
||||
const li = document.createElement('li');
|
||||
li.innerHTML = `<span class="num"></span><span class="txt"></span><button class="bl-x" type="button" aria-label="删除"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M4 4l8 8M12 4l-8 8"/></svg></button>`;
|
||||
li.querySelector('.txt').textContent = t;
|
||||
sellList.querySelector('.add').before(li);
|
||||
sellInput.value = '';
|
||||
renumberSell();
|
||||
li.querySelector('.bl-x').addEventListener('click', () => {
|
||||
li.remove();
|
||||
renumberSell();
|
||||
});
|
||||
});
|
||||
function renumberSell() {
|
||||
sellList.querySelectorAll('li:not(.add) .num').forEach((n, i) => n.textContent = i + 1);
|
||||
}
|
||||
|
||||
// 必填检查
|
||||
function syncSave() {
|
||||
const hasName = $('p-name').value.trim().length > 0;
|
||||
const hasCat = $('p-cat').value.length > 0;
|
||||
const hasPhoto = photos.length > 0;
|
||||
const ok = hasName && hasCat && hasPhoto;
|
||||
$('save-btn').disabled = !ok;
|
||||
|
||||
const missing = [];
|
||||
if (!hasName) missing.push('商品名');
|
||||
if (!hasCat) missing.push('品类');
|
||||
if (!hasPhoto) missing.push('≥1 张图');
|
||||
|
||||
const info = $('req-info');
|
||||
if (ok) {
|
||||
info.innerHTML = '// 必填检查:<span class="ok">已全部完成 ✓</span> · 可进入工作台';
|
||||
} else {
|
||||
info.innerHTML = `// 必填检查:<span class="miss">${missing.join(' / ')}</span> 未完成`;
|
||||
}
|
||||
}
|
||||
$('p-name').addEventListener('input', syncSave);
|
||||
$('p-cat').addEventListener('change', syncSave);
|
||||
|
||||
// 保存 → 跳工作台
|
||||
$('save-btn').addEventListener('click', () => {
|
||||
if ($('save-btn').disabled) return;
|
||||
Shell.toast('商品已建档', `进入工作台`);
|
||||
setTimeout(() => {
|
||||
// 真实场景下会带商品 ID;demo 直接跳
|
||||
location.href = 'product-studio.html?from=new&onboard=1';
|
||||
}, 500);
|
||||
});
|
||||
|
||||
renderPhotos();
|
||||
syncSave();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,54 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>新建商品 · 流·Studio</title>
|
||||
<meta http-equiv="Cache-Control" content="no-store, must-revalidate">
|
||||
<meta http-equiv="Pragma" content="no-cache">
|
||||
<meta http-equiv="Expires" content="0">
|
||||
<!-- 双保险:JS 没跑也能跳走 (1.5s 后强制回 products.html) -->
|
||||
<meta http-equiv="refresh" content="1.5; url=products.html?npd=1">
|
||||
<style>
|
||||
html,body{background:#f9f9f9;margin:0;height:100%;font-family:'Inter',system-ui,sans-serif;}
|
||||
.stub-msg{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:10px;color:#999;font-size:13px;}
|
||||
.stub-msg .ttl{font-size:14px;color:#262626;font-weight:500;}
|
||||
.stub-msg a{color:#fa5d19;text-decoration:underline;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="stub-msg">
|
||||
<div class="ttl">正在打开「新建商品」…</div>
|
||||
<div>如未自动跳转,<a href="products.html?npd=1">点击这里</a></div>
|
||||
</div>
|
||||
<script>
|
||||
/* ============================================================
|
||||
新建商品 · 重定向 stub
|
||||
----------------------------------------------------------
|
||||
旧版本是一个 3883 行的全屏 drawer 页(legacy 备份在 product-create.legacy.html)。
|
||||
现在「新建商品」改为 drawer 形态在原页面弹出,直接访问此 URL 没有意义。
|
||||
|
||||
行为:
|
||||
1. 设置 sessionStorage 标志,通知落地页自动打开 drawer
|
||||
2. 优先回退到 referrer(用户来源页),否则去 products.html
|
||||
3. cache-bust 参数 ?npd=1 强制浏览器重新拉新版宿主页
|
||||
============================================================ */
|
||||
(function () {
|
||||
try { sessionStorage.setItem('npd-auto-open', '1'); } catch (e) {}
|
||||
try { sessionStorage.setItem('auto-open-new-product', '1'); } catch (e) {}
|
||||
|
||||
const ref = document.referrer || '';
|
||||
const here = location.origin + location.pathname;
|
||||
|
||||
// 同源 referrer 且不是自身才回跳, 否则去 products.html
|
||||
let target = 'products.html';
|
||||
if (ref && ref.indexOf(location.origin) === 0 && ref.split('#')[0].split('?')[0] !== here) {
|
||||
target = ref;
|
||||
}
|
||||
// 加 cache-bust query · 强制浏览器拉新版宿主页,避免再次命中缓存旧版
|
||||
const sep = target.indexOf('?') >= 0 ? '&' : '?';
|
||||
target = target + sep + 'npd=' + Date.now().toString(36);
|
||||
location.replace(target);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1787
v2/products.html
1787
v2/products.html
File diff suppressed because it is too large
Load Diff
1231
v2/projects-new.html
1231
v2/projects-new.html
File diff suppressed because it is too large
Load Diff
1063
v2/projects.html
1063
v2/projects.html
File diff suppressed because it is too large
Load Diff
524
v2/settings.html
524
v2/settings.html
@ -1,524 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>设置 · 流·Studio</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="assets/restraint.css">
|
||||
<style>
|
||||
/* ─── 设置布局:左 nav + 右 panel ─── */
|
||||
.settings-grid { display: grid; grid-template-columns: 220px minmax(0, 1fr); gap: 24px; align-items: start; }
|
||||
|
||||
.settings-nav { position: sticky; top: 16px; }
|
||||
.settings-nav .nav-h { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .06em; text-transform: uppercase; padding: 0 12px 8px; }
|
||||
.settings-nav a { display: flex; align-items: center; gap: 10px; padding: 10px 12px; font-size: 13px; color: var(--accent-black); border-radius: var(--r-md); border: 1px solid transparent; cursor: pointer; text-decoration: none; transition: background var(--t-base), border-color var(--t-base); }
|
||||
.settings-nav a:hover { background: var(--background-lighter); }
|
||||
.settings-nav a.active { background: var(--heat-12); color: var(--heat); border-color: var(--heat-20); font-weight: 600; }
|
||||
.settings-nav a svg { width: 16px; height: 16px; stroke-width: 1.5; }
|
||||
|
||||
/* ─── pane ─── */
|
||||
.pane { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 24px; margin-bottom: 16px; }
|
||||
.pane h3 { font-size: 14px; font-weight: 600; margin-bottom: 4px; }
|
||||
.pane .pane-desc { font-size: 12px; color: var(--black-alpha-48); font-family: var(--font-mono); letter-spacing: .02em; margin-bottom: 18px; }
|
||||
.pane.danger { border-color: rgba(180,30,30,.25); background: rgba(180,30,30,.03); }
|
||||
.pane.danger h3 { color: var(--accent-crimson); }
|
||||
|
||||
/* ─── form row ─── */
|
||||
.form-row { display: grid; grid-template-columns: 160px minmax(0, 1fr); gap: 16px; padding: 14px 0; border-bottom: 1px solid var(--border-faint); align-items: center; }
|
||||
.form-row:last-child { border-bottom: 0; }
|
||||
.form-row .lbl { font-size: 12.5px; color: var(--black-alpha-56); }
|
||||
.form-row .lbl .req { color: var(--accent-crimson); margin-left: 2px; }
|
||||
.form-row .lbl-sub { font-size: 11px; color: var(--black-alpha-48); font-family: var(--font-mono); margin-top: 2px; letter-spacing: .02em; }
|
||||
.form-row .val { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
||||
.form-row .val .input, .form-row .val .select { width: 100%; max-width: 380px; }
|
||||
.form-row .val .static { font-size: 13px; color: var(--accent-black); font-variant-numeric: tabular-nums; }
|
||||
.form-row .val .static.mono { font-family: var(--font-mono); font-size: 12.5px; color: var(--black-alpha-56); }
|
||||
.form-row .val .role-tag { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: var(--r-pill); font-size: 11px; font-weight: 500; background: var(--heat-12); color: var(--heat); }
|
||||
.form-row .val .role-tag .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--heat); }
|
||||
|
||||
/* ─── 头像上传 ─── */
|
||||
.avatar-edit { display: flex; align-items: center; gap: 16px; }
|
||||
.avatar-edit .av-big { width: 64px; height: 64px; border-radius: 50%; background: var(--background-lighter); border: 1px solid var(--border-faint); display: grid; place-items: center; font-size: 24px; font-weight: 600; color: var(--accent-black); }
|
||||
.avatar-edit .av-actions { display: flex; gap: 8px; }
|
||||
|
||||
/* ─── toggle switch ─── */
|
||||
.switch { position: relative; width: 36px; height: 20px; flex: 0 0 36px; }
|
||||
.switch input { opacity: 0; width: 0; height: 0; }
|
||||
.switch .slider { position: absolute; inset: 0; background: var(--black-alpha-24); border-radius: 20px; cursor: pointer; transition: background var(--t-base); }
|
||||
.switch .slider::before { content: ''; position: absolute; left: 2px; top: 2px; width: 16px; height: 16px; background: var(--accent-white); border-radius: 50%; transition: transform var(--t-base); }
|
||||
.switch input:checked + .slider { background: var(--heat); }
|
||||
.switch input:checked + .slider::before { transform: translateX(16px); }
|
||||
|
||||
/* ─── 偏好选项卡 ─── */
|
||||
.pref-choices { display: grid; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); gap: 8px; max-width: 540px; }
|
||||
.pref-choice { padding: 10px 12px; border: 1px solid var(--border-faint); border-radius: var(--r-md); cursor: pointer; transition: all var(--t-base); }
|
||||
.pref-choice:hover { background: var(--background-lighter); }
|
||||
.pref-choice.selected { border-color: var(--heat); background: var(--heat-12); }
|
||||
.pref-choice .t { font-size: 12.5px; font-weight: 600; color: var(--accent-black); }
|
||||
.pref-choice .d { font-size: 11px; color: var(--black-alpha-48); margin-top: 2px; font-family: var(--font-mono); letter-spacing: .02em; }
|
||||
.pref-choice.selected .t { color: var(--heat); }
|
||||
|
||||
/* ─── 时长档 ─── */
|
||||
.duration-row { display: flex; gap: 8px; }
|
||||
.dur-chip { padding: 6px 14px; border: 1px solid var(--border-faint); border-radius: var(--r-md); font-size: 13px; cursor: pointer; font-family: var(--font-mono); font-variant-numeric: tabular-nums; transition: all var(--t-base); background: var(--surface); }
|
||||
.dur-chip:hover { background: var(--background-lighter); }
|
||||
.dur-chip.selected { border-color: var(--heat); background: var(--heat-12); color: var(--heat); font-weight: 600; }
|
||||
|
||||
/* ─── 设备列表 ─── */
|
||||
.device-row { display: flex; align-items: center; gap: 12px; padding: 12px 0; border-bottom: 1px solid var(--border-faint); }
|
||||
.device-row:last-child { border-bottom: 0; }
|
||||
.device-row .ic { width: 36px; height: 36px; border-radius: var(--r-md); background: var(--background-lighter); display: grid; place-items: center; color: var(--black-alpha-56); }
|
||||
.device-row .ic svg { width: 18px; height: 18px; }
|
||||
.device-row .nm { font-size: 13px; font-weight: 500; }
|
||||
.device-row .meta { font-size: 11.5px; color: var(--black-alpha-48); font-family: var(--font-mono); margin-top: 2px; letter-spacing: .02em; }
|
||||
.device-row .tag-cur { font-family: var(--font-mono); font-size: 10.5px; padding: 1px 6px; background: var(--accent-forest); color: var(--accent-white); border-radius: var(--r-sm); margin-left: 8px; letter-spacing: .04em; font-weight: 600; }
|
||||
.device-row .spacer { margin-left: auto; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="page">
|
||||
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1>设置</h1>
|
||||
<div class="sub"><span class="mono">// 个人信息 · 偏好 · 通知 · 安全</span></div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn" id="save-cancel" disabled style="opacity:.5; cursor:not-allowed;">取消</button>
|
||||
<button class="btn btn-primary" id="save-btn" disabled style="opacity:.5; cursor:not-allowed;">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l5 5L20 6"/></svg>
|
||||
保存所有变更
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-grid">
|
||||
<!-- 左侧 nav -->
|
||||
<aside class="settings-nav">
|
||||
<div class="nav-h">个人</div>
|
||||
<a href="#sec-profile" class="active" data-jump="sec-profile">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="4"/><path d="M4 21c0-4.4 3.6-8 8-8s8 3.6 8 8"/></svg>
|
||||
个人信息
|
||||
</a>
|
||||
<a href="#sec-security" data-jump="sec-security">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
||||
安全
|
||||
</a>
|
||||
<a href="#sec-notify" data-jump="sec-notify">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9M10 21a2 2 0 0 0 4 0"/></svg>
|
||||
通知
|
||||
</a>
|
||||
<div class="nav-h" style="margin-top: 16px;">偏好</div>
|
||||
<a href="#sec-pref" data-jump="sec-pref">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="m3 7 9-4 9 4-9 4-9-4z"/><path d="m3 12 9 4 9-4"/></svg>
|
||||
创作默认
|
||||
</a>
|
||||
<a href="#sec-display" data-jump="sec-display">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M2 10h20"/></svg>
|
||||
显示
|
||||
</a>
|
||||
<div class="nav-h" style="margin-top: 16px;">账号</div>
|
||||
<a href="#sec-danger" data-jump="sec-danger" style="color: var(--accent-crimson);">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><path d="M12 9v4M12 17h.01"/></svg>
|
||||
危险操作
|
||||
</a>
|
||||
</aside>
|
||||
|
||||
<!-- 右侧内容 -->
|
||||
<main>
|
||||
<!-- ─── 个人信息 ─── -->
|
||||
<section class="pane" id="sec-profile">
|
||||
<h3>个人信息</h3>
|
||||
<div class="pane-desc">// 头像、姓名、联系方式 · 邮箱用于接收通知</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="lbl">头像</div>
|
||||
<div class="val">
|
||||
<div class="avatar-edit">
|
||||
<div class="av-big">李</div>
|
||||
<div class="av-actions">
|
||||
<button class="btn btn-sm" onclick="Shell.toast('上传头像', '占位 · 选择本地图片')">上传新头像</button>
|
||||
<button class="btn btn-ghost btn-sm" onclick="Shell.toast('恢复默认头像', '已重置')">恢复默认</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="lbl">显示名称<span class="req">*</span></div>
|
||||
<div class="val"><input class="input" id="prof-name" value="小李" data-track></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="lbl">登录邮箱</div>
|
||||
<div class="val">
|
||||
<input class="input" id="prof-email" value="li@shop.com" data-track>
|
||||
<button class="btn btn-ghost btn-sm" onclick="Shell.toast('已发送验证邮件', 'li@shop.com')">验证</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="lbl">手机号</div>
|
||||
<div class="val">
|
||||
<input class="input" id="prof-phone" value="138****8000" data-track>
|
||||
<button class="btn btn-ghost btn-sm" onclick="Shell.toast('已发送短信验证码', '+86 138****8000')">更换</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="lbl">所属团队<div class="lbl-sub">// 一人一团队</div></div>
|
||||
<div class="val">
|
||||
<span class="static">小李的店</span>
|
||||
<span class="role-tag"><span class="dot"></span>超管 · 创建者</span>
|
||||
<a href="team.html" style="font-size: 12px; color: var(--heat); text-decoration: none; margin-left: auto;">管理团队 →</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="lbl">用户 ID<div class="lbl-sub">// 不可改</div></div>
|
||||
<div class="val"><span class="static mono">USR-2026-A8F2-001</span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ─── 安全 ─── -->
|
||||
<section class="pane" id="sec-security">
|
||||
<h3>安全</h3>
|
||||
<div class="pane-desc">// 登录密码、双因素、在用设备</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="lbl">登录密码</div>
|
||||
<div class="val">
|
||||
<span class="static mono">●●●●●●●●●●</span>
|
||||
<span class="muted-2 mono" style="font-size: 11px; margin-left: auto;">上次修改 2026-04-12</span>
|
||||
<button class="btn btn-sm" onclick="Shell.toast('修改密码', '/settings/password')" style="margin-left: 10px;">修改</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="lbl">两步验证<div class="lbl-sub">// 推荐开启</div></div>
|
||||
<div class="val">
|
||||
<label class="switch"><input type="checkbox" id="opt-2fa"><span class="slider"></span></label>
|
||||
<span class="static mono" style="font-size: 11.5px; color: var(--black-alpha-48);">短信 + Authenticator</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 style="margin-top: 24px;">在用设备</h3>
|
||||
<div class="pane-desc">// 不在此列表上的设备登录会触发短信告警</div>
|
||||
<div>
|
||||
<div class="device-row">
|
||||
<div class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="14" rx="2"/><path d="M2 20h20"/></svg></div>
|
||||
<div>
|
||||
<div class="nm">MacBook Pro · Chrome<span class="tag-cur">CURRENT</span></div>
|
||||
<div class="meta">// 上海 · 2026-05-21 14:08 · IP 116.xxx.xxx.42</div>
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
<span class="muted-2 mono" style="font-size: 11px;">当前会话</span>
|
||||
</div>
|
||||
<div class="device-row">
|
||||
<div class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="2" width="12" height="20" rx="2"/><path d="M11 18h2"/></svg></div>
|
||||
<div>
|
||||
<div class="nm">iPhone 15 · Safari</div>
|
||||
<div class="meta">// 上海 · 2026-05-20 21:43</div>
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
<button class="btn btn-ghost btn-sm" onclick="Shell.toast('已下线', 'iPhone 15')">下线</button>
|
||||
</div>
|
||||
<div class="device-row">
|
||||
<div class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="14" rx="2"/><path d="M2 20h20"/></svg></div>
|
||||
<div>
|
||||
<div class="nm">Windows · Edge</div>
|
||||
<div class="meta">// 杭州 · 2026-05-18 09:12</div>
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
<button class="btn btn-ghost btn-sm" onclick="Shell.toast('已下线', 'Windows Edge')">下线</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 14px;">
|
||||
<button class="btn" onclick="if(confirm('下线所有其他设备?')) Shell.toast('已下线其他设备', '2 个')">下线所有其他设备</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ─── 通知 ─── -->
|
||||
<section class="pane" id="sec-notify">
|
||||
<h3>通知</h3>
|
||||
<div class="pane-desc">// 邮件、短信、站内提示开关</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="lbl">项目完成通知<div class="lbl-sub">// 视频导出后</div></div>
|
||||
<div class="val">
|
||||
<label class="switch"><input type="checkbox" id="n-export" checked><span class="slider"></span></label>
|
||||
<span class="static mono" style="font-size: 11.5px; color: var(--black-alpha-48);">站内 · 邮件 · 短信</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="lbl">任务失败告警</div>
|
||||
<div class="val">
|
||||
<label class="switch"><input type="checkbox" id="n-fail" checked><span class="slider"></span></label>
|
||||
<span class="static mono" style="font-size: 11.5px; color: var(--black-alpha-48);">站内 · 邮件</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="lbl">额度不足提醒<div class="lbl-sub">// 团队或个人剩余 < 20%</div></div>
|
||||
<div class="val">
|
||||
<label class="switch"><input type="checkbox" id="n-quota" checked><span class="slider"></span></label>
|
||||
<span class="static mono" style="font-size: 11.5px; color: var(--black-alpha-48);">站内 · 短信</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="lbl">异地登录告警</div>
|
||||
<div class="val">
|
||||
<label class="switch"><input type="checkbox" id="n-login" checked><span class="slider"></span></label>
|
||||
<span class="static mono" style="font-size: 11.5px; color: var(--black-alpha-48);">短信</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="lbl">营销 / 产品更新</div>
|
||||
<div class="val">
|
||||
<label class="switch"><input type="checkbox" id="n-marketing"><span class="slider"></span></label>
|
||||
<span class="static mono" style="font-size: 11.5px; color: var(--black-alpha-48);">邮件</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ─── 创作默认 ─── -->
|
||||
<section class="pane" id="sec-pref">
|
||||
<h3>创作默认</h3>
|
||||
<div class="pane-desc">// 新建项目时的预填值,可在向导中改</div>
|
||||
|
||||
<div class="form-row" style="grid-template-columns: 160px 1fr; align-items: flex-start;">
|
||||
<div class="lbl" style="padding-top: 4px;">默认模板</div>
|
||||
<div class="val" style="display: block;">
|
||||
<div class="pref-choices" id="pref-template">
|
||||
<div class="pref-choice selected" data-v="pain"><div class="t">痛点种草</div><div class="d">// 30s 默认档</div></div>
|
||||
<div class="pref-choice" data-v="unbox"><div class="t">开箱测评</div><div class="d">// 45s 默认档</div></div>
|
||||
<div class="pref-choice" data-v="compare"><div class="t">对比展示</div><div class="d">// 45s 默认档</div></div>
|
||||
<div class="pref-choice" data-v="howto"><div class="t">教程演示</div><div class="d">// 60s 默认档</div></div>
|
||||
<div class="pref-choice" data-v="drama"><div class="t">剧情带货</div><div class="d">// 60s 默认档</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="lbl">默认时长档</div>
|
||||
<div class="val">
|
||||
<div class="duration-row" id="pref-duration">
|
||||
<span class="dur-chip" data-v="30">30s</span>
|
||||
<span class="dur-chip" data-v="45">45s</span>
|
||||
<span class="dur-chip selected" data-v="60">60s</span>
|
||||
</div>
|
||||
<span class="static mono" style="font-size: 11.5px; color: var(--black-alpha-48); margin-left: 10px;">// 60s = 4 段 × 15s</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row" style="grid-template-columns: 160px 1fr; align-items: flex-start;">
|
||||
<div class="lbl" style="padding-top: 4px;">默认字幕样式</div>
|
||||
<div class="val" style="display: block;">
|
||||
<div class="pref-choices" id="pref-subtitle">
|
||||
<div class="pref-choice selected" data-v="big-variety"><div class="t">大字综艺</div><div class="d">// 抖音热门</div></div>
|
||||
<div class="pref-choice" data-v="clean-ec"><div class="t">简洁电商</div><div class="d">// 信息清晰</div></div>
|
||||
<div class="pref-choice" data-v="premium"><div class="t">高级排版</div><div class="d">// 居中衬线</div></div>
|
||||
<div class="pref-choice" data-v="bullet"><div class="t">弹幕轻量</div><div class="d">// 滚动出现</div></div>
|
||||
<div class="pref-choice" data-v="emphasis"><div class="t">强调爆款</div><div class="d">// 高对比</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="lbl">默认 BGM 库</div>
|
||||
<div class="val">
|
||||
<select class="select" id="pref-bgm" data-track>
|
||||
<option value="kapian">抖音 Top10 卡点曲库</option>
|
||||
<option value="emotion">情绪向 · 治愈/悬念</option>
|
||||
<option value="urban">都市电子 · 通勤场景</option>
|
||||
<option value="none">无 BGM</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="lbl">默认转场</div>
|
||||
<div class="val">
|
||||
<select class="select" id="pref-transition" data-track>
|
||||
<option>无转场</option>
|
||||
<option selected>淡入淡出 · 0.3s</option>
|
||||
<option>滑动 · 0.3s</option>
|
||||
<option>缩放 · 0.3s</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="lbl">导出水印<div class="lbl-sub">// VIP 可关闭</div></div>
|
||||
<div class="val">
|
||||
<label class="switch"><input type="checkbox" id="opt-watermark" checked disabled><span class="slider"></span></label>
|
||||
<span class="static mono" style="font-size: 11.5px; color: var(--black-alpha-48);">右下角 · 流·Studio</span>
|
||||
<a href="account.html" style="font-size: 12px; color: var(--heat); text-decoration: none; margin-left: auto;">升级 VIP →</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ─── 显示 ─── -->
|
||||
<section class="pane" id="sec-display">
|
||||
<h3>显示</h3>
|
||||
<div class="pane-desc">// 界面外观与语言</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="lbl">外观</div>
|
||||
<div class="val">
|
||||
<select class="select" id="pref-theme" data-track>
|
||||
<option selected>跟随系统</option>
|
||||
<option>浅色</option>
|
||||
<option disabled>深色(V2)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="lbl">语言</div>
|
||||
<div class="val">
|
||||
<select class="select" id="pref-lang" data-track>
|
||||
<option selected>简体中文</option>
|
||||
<option disabled>English(V2)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="lbl">表格密度</div>
|
||||
<div class="val">
|
||||
<select class="select" id="pref-density" data-track>
|
||||
<option>紧凑</option>
|
||||
<option selected>标准</option>
|
||||
<option>宽松</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ─── 危险操作 ─── -->
|
||||
<section class="pane danger" id="sec-danger">
|
||||
<h3>危险操作</h3>
|
||||
<div class="pane-desc">// 这些操作不可撤销,请确认后再执行</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="lbl">导出我的数据<div class="lbl-sub">// 项目 + 资产元数据</div></div>
|
||||
<div class="val">
|
||||
<span class="static mono" style="font-size: 11.5px; color: var(--black-alpha-56);">// 准备时间约 24 小时,完成后邮件通知</span>
|
||||
<button class="btn btn-sm" style="margin-left: auto;" onclick="Shell.toast('已申请导出', '约 24 小时后邮件发送')">申请导出</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="lbl">退出登录</div>
|
||||
<div class="val">
|
||||
<span class="static mono" style="font-size: 11.5px; color: var(--black-alpha-56);">// 仅退出当前设备,数据保留</span>
|
||||
<button class="btn btn-sm" style="margin-left: auto; color: var(--accent-crimson); border-color: var(--accent-crimson);" onclick="if(confirm('确定退出登录?')) Shell.toast('已退出', '正在跳转登录页')">退出登录</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="lbl">注销账号</div>
|
||||
<div class="val">
|
||||
<span class="static mono" style="font-size: 11.5px; color: var(--black-alpha-56);">// 团队余额清零、所有项目作为孤儿归档</span>
|
||||
<button class="btn btn-sm" style="margin-left: auto; color: var(--accent-crimson); border-color: var(--accent-crimson);" onclick="if(confirm('彻底注销当前账号? 此操作不可恢复')) Shell.toast('已提交注销申请', '24 小时内人工复核')">注销账号</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div style="text-align: center; padding: 24px 0 8px; color: var(--black-alpha-32); font-family: var(--font-mono); font-size: 11px; letter-spacing: .04em;">
|
||||
// 流·Studio · v2.1 · build 20260521
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<script src="assets/shell.js"></script>
|
||||
<script>
|
||||
Shell.render({
|
||||
active: 'settings',
|
||||
crumbs: [{ label: '工作台', href: 'index.html' }, { label: '设置' }]
|
||||
});
|
||||
|
||||
/* ─── 侧边 nav 高亮 + 滚动联动 ─── */
|
||||
const sections = ['sec-profile', 'sec-security', 'sec-notify', 'sec-pref', 'sec-display', 'sec-danger'];
|
||||
const navLinks = document.querySelectorAll('.settings-nav a[data-jump]');
|
||||
|
||||
navLinks.forEach(a => {
|
||||
a.addEventListener('click', e => {
|
||||
e.preventDefault();
|
||||
const id = a.dataset.jump;
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
const contentEl = document.getElementById('page-content') || document.querySelector('.content');
|
||||
const offset = 16;
|
||||
if (contentEl) {
|
||||
contentEl.scrollTo({ top: el.offsetTop - contentEl.offsetTop - offset, behavior: 'smooth' });
|
||||
} else {
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const contentEl = document.getElementById('page-content') || document.querySelector('.content');
|
||||
const observer = new IntersectionObserver(entries => {
|
||||
entries.forEach(en => {
|
||||
if (en.isIntersecting) {
|
||||
const id = en.target.id;
|
||||
navLinks.forEach(a => a.classList.toggle('active', a.dataset.jump === id));
|
||||
}
|
||||
});
|
||||
}, { root: contentEl || null, rootMargin: '-20% 0px -60% 0px', threshold: 0 });
|
||||
sections.forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) observer.observe(el);
|
||||
});
|
||||
|
||||
/* ─── 偏好 chip 选择 ─── */
|
||||
function bindChoice(containerId, label) {
|
||||
const ct = document.getElementById(containerId);
|
||||
if (!ct) return;
|
||||
ct.querySelectorAll('.pref-choice').forEach(c => {
|
||||
c.addEventListener('click', () => {
|
||||
ct.querySelectorAll('.pref-choice').forEach(x => x.classList.remove('selected'));
|
||||
c.classList.add('selected');
|
||||
markDirty();
|
||||
});
|
||||
});
|
||||
}
|
||||
bindChoice('pref-template');
|
||||
bindChoice('pref-subtitle');
|
||||
|
||||
document.querySelectorAll('#pref-duration .dur-chip').forEach(c => {
|
||||
c.addEventListener('click', () => {
|
||||
document.querySelectorAll('#pref-duration .dur-chip').forEach(x => x.classList.remove('selected'));
|
||||
c.classList.add('selected');
|
||||
markDirty();
|
||||
});
|
||||
});
|
||||
|
||||
/* ─── dirty state ─── */
|
||||
let dirty = false;
|
||||
function markDirty() {
|
||||
if (dirty) return;
|
||||
dirty = true;
|
||||
const saveBtn = document.getElementById('save-btn');
|
||||
const cancelBtn = document.getElementById('save-cancel');
|
||||
[saveBtn, cancelBtn].forEach(b => {
|
||||
b.disabled = false;
|
||||
b.style.opacity = '';
|
||||
b.style.cursor = '';
|
||||
});
|
||||
}
|
||||
function clearDirty() {
|
||||
dirty = false;
|
||||
const saveBtn = document.getElementById('save-btn');
|
||||
const cancelBtn = document.getElementById('save-cancel');
|
||||
[saveBtn, cancelBtn].forEach(b => {
|
||||
b.disabled = true;
|
||||
b.style.opacity = '.5';
|
||||
b.style.cursor = 'not-allowed';
|
||||
});
|
||||
}
|
||||
document.querySelectorAll('[data-track], input[type="checkbox"], select').forEach(el => {
|
||||
el.addEventListener('change', markDirty);
|
||||
if (el.tagName === 'INPUT' && el.type !== 'checkbox') el.addEventListener('input', markDirty);
|
||||
});
|
||||
|
||||
document.getElementById('save-btn').addEventListener('click', () => {
|
||||
if (!dirty) return;
|
||||
Shell.toast('设置已保存', '所有变更已生效');
|
||||
clearDirty();
|
||||
});
|
||||
document.getElementById('save-cancel').addEventListener('click', () => {
|
||||
if (!dirty) return;
|
||||
if (confirm('放弃未保存的变更?')) location.reload();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1402
v2/studio-v2.html
1402
v2/studio-v2.html
File diff suppressed because it is too large
Load Diff
1634
v2/studio.html
1634
v2/studio.html
File diff suppressed because it is too large
Load Diff
518
v2/team.html
518
v2/team.html
@ -1,518 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>团队 · 流·Studio</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="assets/restraint.css">
|
||||
<style>
|
||||
/* ─── 团队信息卡(深色 banner · 上标题行 + 下统计行)─── */
|
||||
.team-banner {
|
||||
background: var(--accent-black);
|
||||
color: var(--accent-white);
|
||||
padding: 22px 28px 24px;
|
||||
margin-bottom: 24px;
|
||||
position: relative;
|
||||
border: 1px solid var(--accent-black);
|
||||
border-radius: var(--r-md);
|
||||
}
|
||||
/* 4 个装订线小十字(2 个 pseudo + 2 个 span)*/
|
||||
.team-banner::before, .team-banner::after,
|
||||
.team-banner > .corner-tr, .team-banner > .corner-bl {
|
||||
content: ''; position: absolute; width: 14px; height: 14px;
|
||||
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 21' fill='%23e8e8e8'%3E%3Cpath d='M10.5 4C10.5 7.31371 7.81371 10 4.5 10H0.5V11H4.5C7.81371 11 10.5 13.6863 10.5 17V21H11.5V17C11.5 13.6863 14.1863 11 17.5 11H21.5V10H17.5C14.1863 10 11.5 7.31371 11.5 4V0H10.5V4Z'/%3E%3C/svg%3E") no-repeat center;
|
||||
background-size: contain; pointer-events: none;
|
||||
}
|
||||
.team-banner::before { top: -7px; left: -7px; }
|
||||
.team-banner::after { bottom: -7px; right: -7px; }
|
||||
.team-banner > .corner-tr { top: -7px; right: -7px; }
|
||||
.team-banner > .corner-bl { bottom: -7px; left: -7px; }
|
||||
|
||||
/* 第 1 行:标题 + 主操作 */
|
||||
.banner-head { display: flex; align-items: flex-start; gap: 20px; }
|
||||
.banner-id { flex: 1; min-width: 0; }
|
||||
.banner-id .lbl { font-family: var(--font-mono); font-size: 10.5px; color: rgba(255,255,255,.55); letter-spacing: .06em; text-transform: uppercase; }
|
||||
.banner-id .nm { font-size: 22px; font-weight: 700; letter-spacing: -.012em; margin-top: 4px; display: flex; align-items: baseline; gap: 10px; }
|
||||
.banner-id .nm .tag { font-size: 10.5px; font-family: var(--font-mono); padding: 2px 8px; background: rgba(255,255,255,.12); border-radius: var(--r-pill); letter-spacing: .04em; font-weight: 500; }
|
||||
.banner-id .meta { font-size: 12px; color: rgba(255,255,255,.5); margin-top: 6px; font-family: var(--font-mono); letter-spacing: .02em; }
|
||||
.banner-actions { display: flex; gap: 8px; flex-shrink: 0; }
|
||||
.banner-actions .btn { background: var(--accent-white); color: var(--accent-black); border-color: var(--accent-white); }
|
||||
.banner-actions .btn:hover { background: var(--background-base); }
|
||||
.banner-actions .btn-ghost { background: transparent; color: var(--accent-white); border: 1px solid rgba(255,255,255,.25); }
|
||||
.banner-actions .btn-ghost:hover { background: rgba(255,255,255,.08); color: var(--accent-white); }
|
||||
|
||||
/* 分隔线 */
|
||||
.banner-divider { height: 1px; background: rgba(255,255,255,.1); margin: 20px 0 18px; }
|
||||
|
||||
/* 第 2 行:4 列统计 */
|
||||
.banner-stats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 24px; }
|
||||
.banner-stats .stat { min-width: 0; }
|
||||
.banner-stats .stat .lbl { font-family: var(--font-mono); font-size: 10.5px; color: rgba(255,255,255,.55); letter-spacing: .06em; text-transform: uppercase; }
|
||||
.banner-stats .stat .v { font-size: 22px; font-weight: 700; font-variant-numeric: tabular-nums; letter-spacing: -.012em; margin-top: 6px; }
|
||||
.banner-stats .stat .v.warn { color: #FFB870; }
|
||||
.banner-stats .stat .sub { font-size: 11px; color: rgba(255,255,255,.5); margin-top: 4px; font-family: var(--font-mono); letter-spacing: .02em; }
|
||||
|
||||
/* ─── 主体两栏 ─── */
|
||||
.team-grid { display: grid; grid-template-columns: minmax(0, 1fr) 320px; gap: 24px; align-items: start; }
|
||||
|
||||
.pane { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 20px; margin-bottom: 16px; }
|
||||
.pane h3 { font-size: 14px; font-weight: 600; margin-bottom: 14px; display: flex; align-items: center; gap: 8px; }
|
||||
.pane h3 .ct { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); font-weight: 400; }
|
||||
.pane h3 .spacer { margin-left: auto; }
|
||||
|
||||
/* ─── 成员表 ─── */
|
||||
.members-table .av { width: 32px; height: 32px; border-radius: 50%; background: var(--background-lighter); display: inline-grid; place-items: center; font-weight: 600; font-size: 13px; color: var(--accent-black); border: 1px solid var(--border-faint); }
|
||||
.members-table .who { display: flex; align-items: center; gap: 10px; }
|
||||
.members-table .nm { font-weight: 500; font-size: 13.5px; line-height: 1.2; }
|
||||
.members-table .em { font-size: 11.5px; color: var(--black-alpha-48); font-family: var(--font-mono); }
|
||||
.members-table .role-pill { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: var(--r-pill); font-size: 11px; font-weight: 500; }
|
||||
.members-table .role-pill .dot { width: 6px; height: 6px; border-radius: 50%; }
|
||||
.members-table .role-super { background: var(--heat-12); color: var(--heat); }
|
||||
.members-table .role-super .dot { background: var(--heat); }
|
||||
.members-table .role-admin { background: rgba(30,64,175,.1); color: #1E40AF; }
|
||||
.members-table .role-admin .dot { background: #1E40AF; }
|
||||
.members-table .role-member { background: var(--background-lighter); color: var(--black-alpha-56); }
|
||||
.members-table .role-member .dot { background: var(--black-alpha-56); }
|
||||
.members-table .quota-cell { font-variant-numeric: tabular-nums; font-family: var(--font-mono); font-size: 12px; }
|
||||
.members-table .quota-cell .lbl { color: var(--black-alpha-48); }
|
||||
.members-table .quota-cell .v { color: var(--accent-black); font-weight: 600; }
|
||||
.members-table .used-bar { width: 80px; height: 4px; background: var(--background-lighter); border-radius: 2px; overflow: hidden; margin-top: 4px; }
|
||||
.members-table .used-bar > span { display: block; height: 100%; background: var(--heat); }
|
||||
.members-table .used-bar > span.ok { background: var(--accent-forest); }
|
||||
.members-table .used-bar > span.warn { background: #B45309; }
|
||||
.members-table .acts { display: flex; gap: 4px; justify-content: flex-end; }
|
||||
.members-table .icon-btn-sm { width: 28px; height: 28px; display: inline-grid; place-items: center; border: 1px solid var(--border-faint); border-radius: var(--r-sm); background: var(--surface); cursor: pointer; color: var(--black-alpha-56); transition: all var(--t-base); }
|
||||
.members-table .icon-btn-sm:hover { color: var(--heat); border-color: var(--heat-20); }
|
||||
.members-table .icon-btn-sm svg { width: 14px; height: 14px; }
|
||||
.members-table .icon-btn-sm.danger:hover { color: var(--accent-crimson); border-color: var(--accent-crimson); }
|
||||
.members-table tr.pending td { opacity: .65; }
|
||||
.members-table tr.pending .nm::after { content: '· 待激活'; font-size: 11px; color: var(--black-alpha-48); margin-left: 6px; font-weight: 400; font-family: var(--font-mono); }
|
||||
|
||||
/* ─── 角色权限矩阵 ─── */
|
||||
.perm-table { width: 100%; border-collapse: collapse; font-size: 12.5px; }
|
||||
.perm-table th, .perm-table td { padding: 8px 4px; border-bottom: 1px solid var(--border-faint); }
|
||||
.perm-table th { font-family: var(--font-mono); font-size: 10.5px; font-weight: 500; color: var(--black-alpha-48); letter-spacing: .04em; text-transform: uppercase; text-align: left; }
|
||||
.perm-table th:not(:first-child), .perm-table td:not(:first-child) { text-align: center; }
|
||||
.perm-table tbody td:first-child { color: var(--accent-black); }
|
||||
.perm-table .yes { color: var(--accent-forest); font-weight: 600; }
|
||||
.perm-table .no { color: var(--black-alpha-32); }
|
||||
|
||||
/* ─── 额度检查规则 ─── */
|
||||
.quota-rules { font-size: 12.5px; color: var(--black-alpha-56); line-height: 1.8; }
|
||||
.quota-rules .step { display: flex; gap: 10px; padding: 6px 0; align-items: flex-start; }
|
||||
.quota-rules .num { width: 18px; height: 18px; border-radius: 50%; background: var(--heat-12); color: var(--heat); font-family: var(--font-mono); font-size: 10.5px; font-weight: 600; display: grid; place-items: center; flex: 0 0 18px; margin-top: 1px; }
|
||||
.quota-rules .v { color: var(--accent-black); font-weight: 500; }
|
||||
.quota-rules .formula { font-family: var(--font-mono); font-size: 11px; color: var(--heat); background: var(--heat-12); padding: 1px 6px; }
|
||||
|
||||
/* ─── 邀请 modal ─── */
|
||||
.invite-modal { width: min(480px, 92vw); }
|
||||
.invite-modal .field { margin-bottom: 14px; }
|
||||
.invite-modal label.field-label { display: block; font-size: 12px; color: var(--black-alpha-56); margin-bottom: 6px; font-family: var(--font-mono); letter-spacing: .02em; }
|
||||
.invite-modal label.field-label .req { color: var(--accent-crimson); margin-left: 2px; }
|
||||
.role-choices { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
||||
.role-choice { padding: 12px 14px; border: 1px solid var(--border-faint); border-radius: var(--r-md); cursor: pointer; transition: all var(--t-base); }
|
||||
.role-choice:hover { background: var(--background-lighter); }
|
||||
.role-choice.selected { border-color: var(--heat); background: var(--heat-12); }
|
||||
.role-choice .title { font-size: 13px; font-weight: 600; color: var(--accent-black); }
|
||||
.role-choice .desc { font-size: 11px; color: var(--black-alpha-56); margin-top: 2px; font-family: var(--font-mono); letter-spacing: .02em; }
|
||||
.quota-input-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
||||
.quota-input-row .input { font-variant-numeric: tabular-nums; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="page">
|
||||
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1>团队管理</h1>
|
||||
<div class="sub"><span class="mono">// 成员 · 角色 · 额度 · 共享资产库</span></div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn" onclick="Shell.toast('团队设置', '/team/settings')">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8 2 2 0 0 1-2.8 2.8 1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5 2 2 0 0 1-4 0 1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3 2 2 0 0 1-2.8-2.8 1.7 1.7 0 0 0 .3-1.8"/></svg>
|
||||
团队设置
|
||||
</button>
|
||||
<button class="btn btn-primary" id="open-invite">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M19 8v6M22 11h-6"/></svg>
|
||||
邀请成员
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 团队信息 banner -->
|
||||
<div class="team-banner">
|
||||
<span class="corner-tr" aria-hidden></span><span class="corner-bl" aria-hidden></span>
|
||||
|
||||
<div class="banner-head">
|
||||
<div class="banner-id">
|
||||
<div class="lbl">[ TEAM ]</div>
|
||||
<div class="nm">小李的店 <span class="tag">企业</span></div>
|
||||
<div class="meta">// 团队 ID: T-2026-A8F2 · 创建于 2026-04-12 · 5 名成员</div>
|
||||
</div>
|
||||
<div class="banner-actions">
|
||||
<button class="btn btn-sm" onclick="location.href='account.html'">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 7v10M9 10h4a1.5 1.5 0 0 1 0 3h-3a1.5 1.5 0 0 0 0 3h4"/></svg>
|
||||
充值
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm" onclick="Shell.toast('编辑月限额', '/team/limit')">设置月限额</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="banner-divider"></div>
|
||||
|
||||
<div class="banner-stats">
|
||||
<div class="stat">
|
||||
<div class="lbl">[ 充值余额 ]</div>
|
||||
<div class="v">¥327.40</div>
|
||||
<div class="sub">// 团队总池</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="lbl">[ 月限额 ]</div>
|
||||
<div class="v">¥3,000</div>
|
||||
<div class="sub">// 自然月重置</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="lbl">[ 当月已用 ]</div>
|
||||
<div class="v">¥162.60</div>
|
||||
<div class="sub">// 占月限 5.4%</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="lbl">[ 当月剩余 ]</div>
|
||||
<div class="v warn">¥2,837.40</div>
|
||||
<div class="sub">// 还可生成约 280 个项目</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="team-grid">
|
||||
<!-- 左:成员表 -->
|
||||
<div>
|
||||
<div class="pane" style="padding: 0;">
|
||||
<div style="display: flex; align-items: center; padding: 16px 20px; border-bottom: 1px solid var(--border-faint);">
|
||||
<h3 style="margin: 0;">成员列表 <span class="ct">// 5 人 · 1 超管 / 1 团管 / 3 成员</span></h3>
|
||||
<span class="spacer"></span>
|
||||
<input class="input" id="member-search" placeholder="搜索姓名 / 手机号" style="height: 32px; font-size: 12px; width: 220px;">
|
||||
</div>
|
||||
<table class="t members-table" style="border: 0; border-radius: 0;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>成员</th>
|
||||
<th>角色</th>
|
||||
<th>每日额度</th>
|
||||
<th>月度额度</th>
|
||||
<th style="width: 140px;">当月已用</th>
|
||||
<th style="text-align: right; width: 88px;">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="members-tbody">
|
||||
<!-- JS 注入 -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右:权限矩阵 + 额度规则 -->
|
||||
<div>
|
||||
<div class="pane">
|
||||
<h3>角色权限</h3>
|
||||
<div style="font-size: 12px; color: var(--black-alpha-48); margin-top: -10px; margin-bottom: 12px; font-family: var(--font-mono); letter-spacing: .02em;">// PRD §10.2 权限矩阵节选</div>
|
||||
<table class="perm-table">
|
||||
<thead>
|
||||
<tr><th>能力</th><th>超管</th><th>团管</th><th>成员</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>邀请 / 移除成员</td><td class="yes">✓</td><td class="yes">✓</td><td class="no">—</td></tr>
|
||||
<tr><td>设置成员额度</td><td class="yes">✓</td><td class="yes">✓</td><td class="no">—</td></tr>
|
||||
<tr><td>团队充值</td><td class="yes">✓</td><td class="no">—</td><td class="no">—</td></tr>
|
||||
<tr><td>设置月限额</td><td class="yes">✓</td><td class="no">—</td><td class="no">—</td></tr>
|
||||
<tr><td>编辑别人项目</td><td class="yes">✓</td><td class="yes">✓</td><td class="no">—</td></tr>
|
||||
<tr><td>团队共享资产库管理</td><td class="yes">✓</td><td class="yes">✓</td><td class="no">仅自传</td></tr>
|
||||
<tr><td>查看团队消费明细</td><td class="yes">✓</td><td class="yes">✓</td><td class="no">仅自己</td></tr>
|
||||
<tr style="border-bottom: 0;"><td>创建项目 / 用 AI 流程</td><td class="yes">✓</td><td class="yes">✓</td><td class="yes">✓</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="pane">
|
||||
<h3>额度预检规则</h3>
|
||||
<div style="font-size: 12px; color: var(--black-alpha-48); margin-top: -10px; margin-bottom: 12px; font-family: var(--font-mono); letter-spacing: .02em;">// 任一不通过即拦截</div>
|
||||
<div class="quota-rules">
|
||||
<div class="step"><span class="num">1</span><span><span class="v">个人日剩余</span> ≥ 任务预估 × <span class="formula">1.2</span></span></div>
|
||||
<div class="step"><span class="num">2</span><span><span class="v">个人月剩余</span> ≥ 同上</span></div>
|
||||
<div class="step"><span class="num">3</span><span><span class="v">团队月剩余</span> ≥ 同上</span></div>
|
||||
<div class="step"><span class="num">4</span><span><span class="v">团队总余额</span> ≥ 同上</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pane" style="background: var(--heat-12); border-color: var(--heat-20);">
|
||||
<h3 style="color: var(--heat);">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 8v4M12 16h.01"/></svg>
|
||||
失败不扣费
|
||||
</h3>
|
||||
<div style="font-size: 12.5px; color: var(--black-alpha-56); line-height: 1.7;">
|
||||
所有生成任务<strong style="color: var(--accent-black);">仅在用户 <span class="mono" style="background: var(--surface); padding: 1px 5px; font-family: var(--font-mono); color: var(--heat); font-size: 11.5px;">[ 通过 ]</span> 时才扣费</strong>。失败 / 超时 / 重跑(旧版本作废)一律不扣。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 邀请成员 modal -->
|
||||
<div class="modal-bg" id="invite-bg" onclick="if(event.target===this)Shell.closeModal('invite-bg')">
|
||||
<div class="modal invite-modal">
|
||||
<span class="corner-tr" aria-hidden></span><span class="corner-bl" aria-hidden></span>
|
||||
<div class="modal-h">
|
||||
<div class="ic-m">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M19 8v6M22 11h-6"/></svg>
|
||||
</div>
|
||||
<div class="ti">邀请成员<span>// 短信邀请 · 接受后自动加入团队</span></div>
|
||||
</div>
|
||||
<div class="modal-b">
|
||||
<div class="field">
|
||||
<label class="field-label">手机号 <span class="req">*</span></label>
|
||||
<input class="input" id="inv-phone" placeholder="例: 138 0013 8000">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field-label">备注姓名(可选)</label>
|
||||
<input class="input" id="inv-name" placeholder="例: 张某">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field-label">分配角色 <span class="req">*</span></label>
|
||||
<div class="role-choices">
|
||||
<div class="role-choice selected" data-role="member">
|
||||
<div class="title">成员</div>
|
||||
<div class="desc">// 创建项目 + 用资产</div>
|
||||
</div>
|
||||
<div class="role-choice" data-role="admin">
|
||||
<div class="title">团管</div>
|
||||
<div class="desc">// 管成员 + 改额度</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field" style="margin-bottom: 0;">
|
||||
<label class="field-label">默认额度</label>
|
||||
<div class="quota-input-row">
|
||||
<input class="input" id="inv-daily" placeholder="每日 (¥)" value="100">
|
||||
<input class="input" id="inv-monthly" placeholder="每月 (¥)" value="2000">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-f">
|
||||
<button class="btn" type="button" onclick="Shell.closeModal('invite-bg')">取消</button>
|
||||
<button class="btn btn-primary" type="button" id="inv-send">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="m22 2-7 20-4-9-9-4z"/><path d="m22 2-11 11"/></svg>
|
||||
发送邀请
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑成员 modal -->
|
||||
<div class="modal-bg" id="edit-member-bg" onclick="if(event.target===this)Shell.closeModal('edit-member-bg')">
|
||||
<div class="modal invite-modal">
|
||||
<span class="corner-tr" aria-hidden></span><span class="corner-bl" aria-hidden></span>
|
||||
<div class="modal-h">
|
||||
<div class="ic-m">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4z"/></svg>
|
||||
</div>
|
||||
<div class="ti" id="edit-title">编辑成员<span id="edit-sub">// 调整角色 / 额度</span></div>
|
||||
</div>
|
||||
<div class="modal-b">
|
||||
<div class="field">
|
||||
<label class="field-label">角色</label>
|
||||
<div class="role-choices">
|
||||
<div class="role-choice" data-edit-role="member">
|
||||
<div class="title">成员</div>
|
||||
<div class="desc">// 创建项目 + 用资产</div>
|
||||
</div>
|
||||
<div class="role-choice" data-edit-role="admin">
|
||||
<div class="title">团管</div>
|
||||
<div class="desc">// 管成员 + 改额度</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field" style="margin-bottom: 0;">
|
||||
<label class="field-label">额度</label>
|
||||
<div class="quota-input-row">
|
||||
<input class="input" id="edit-daily" placeholder="每日 (¥)">
|
||||
<input class="input" id="edit-monthly" placeholder="每月 (¥)">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-f">
|
||||
<button class="btn" type="button" style="margin-right: auto; color: var(--accent-crimson); border-color: var(--accent-crimson);" id="edit-remove">移出团队</button>
|
||||
<button class="btn" type="button" onclick="Shell.closeModal('edit-member-bg')">取消</button>
|
||||
<button class="btn btn-primary" type="button" id="edit-save">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<script src="assets/shell.js"></script>
|
||||
<script>
|
||||
Shell.render({
|
||||
active: 'team',
|
||||
crumbs: [{ label: '工作台', href: 'index.html' }, { label: '团队' }]
|
||||
});
|
||||
|
||||
/* ─── 团队成员 mock + 渲染 ─── */
|
||||
const ROLE_META = {
|
||||
super: { cls: 'role-super', label: '超管' },
|
||||
admin: { cls: 'role-admin', label: '团管' },
|
||||
member: { cls: 'role-member', label: '成员' },
|
||||
};
|
||||
|
||||
const MEMBERS = [
|
||||
{ id: 'u1', av: '李', name: '小李', email: 'li@shop.com', role: 'super', daily: 500, monthly: 10000, used: 162.60, pending: false, creator: true },
|
||||
{ id: 'u2', av: '张', name: '张运营', email: 'zhang@shop.com', role: 'admin', daily: 300, monthly: 6000, used: 98.40, pending: false },
|
||||
{ id: 'u3', av: '王', name: '王小姐', email: 'wang@shop.com', role: 'member', daily: 100, monthly: 2000, used: 45.20, pending: false },
|
||||
{ id: 'u4', av: '陈', name: '陈策划', email: 'chen@shop.com', role: 'member', daily: 100, monthly: 2000, used: 12.80, pending: false },
|
||||
{ id: 'u5', av: '林', name: '林新人', email: '186****1102', role: 'member', daily: 100, monthly: 2000, used: 0, pending: true },
|
||||
];
|
||||
|
||||
function fmtMoney(n) { return '¥' + n.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ','); }
|
||||
function usedClass(pct) { if (pct < 0.5) return 'ok'; if (pct < 0.85) return ''; return 'warn'; }
|
||||
|
||||
function renderMembers(filter = '') {
|
||||
const tb = document.getElementById('members-tbody');
|
||||
const list = MEMBERS.filter(m => {
|
||||
if (!filter) return true;
|
||||
const q = filter.toLowerCase();
|
||||
return m.name.toLowerCase().includes(q) || m.email.toLowerCase().includes(q);
|
||||
});
|
||||
tb.innerHTML = list.map(m => {
|
||||
const r = ROLE_META[m.role];
|
||||
const pct = m.monthly > 0 ? m.used / m.monthly : 0;
|
||||
return `
|
||||
<tr data-id="${m.id}"${m.pending ? ' class="pending"' : ''}>
|
||||
<td>
|
||||
<div class="who">
|
||||
<div class="av">${m.av}</div>
|
||||
<div>
|
||||
<div class="nm">${m.name}${m.creator ? ' <span style="font-family:var(--font-mono);font-size:10px;color:var(--black-alpha-48);">· 创建者</span>' : ''}</div>
|
||||
<div class="em">${m.email}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td><span class="role-pill ${r.cls}"><span class="dot"></span>${r.label}</span></td>
|
||||
<td><span class="quota-cell"><span class="v">${fmtMoney(m.daily)}</span></span></td>
|
||||
<td><span class="quota-cell"><span class="v">${fmtMoney(m.monthly)}</span></span></td>
|
||||
<td>
|
||||
<div class="quota-cell"><span class="v">${fmtMoney(m.used)}</span> <span class="lbl">/ ${(pct * 100).toFixed(0)}%</span></div>
|
||||
<div class="used-bar"><span class="${usedClass(pct)}" style="width: ${Math.min(100, pct * 100).toFixed(1)}%"></span></div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="acts">
|
||||
${m.creator ? '<span style="font-family:var(--font-mono);font-size:10.5px;color:var(--black-alpha-32);align-self:center;">不可编辑</span>' : `
|
||||
<button class="icon-btn-sm" data-act="edit" data-id="${m.id}" title="编辑">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4z"/></svg>
|
||||
</button>
|
||||
<button class="icon-btn-sm danger" data-act="remove" data-id="${m.id}" title="移出">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
|
||||
</button>
|
||||
`}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
tb.querySelectorAll('[data-act="edit"]').forEach(btn => {
|
||||
btn.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
openEdit(btn.dataset.id);
|
||||
});
|
||||
});
|
||||
tb.querySelectorAll('[data-act="remove"]').forEach(btn => {
|
||||
btn.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
const m = MEMBERS.find(x => x.id === btn.dataset.id);
|
||||
if (!m) return;
|
||||
if (confirm('确定将「' + m.name + '」移出团队?')) {
|
||||
const i = MEMBERS.findIndex(x => x.id === m.id);
|
||||
MEMBERS.splice(i, 1);
|
||||
Shell.toast('已移除成员', m.name);
|
||||
renderMembers(document.getElementById('member-search').value);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
renderMembers();
|
||||
|
||||
document.getElementById('member-search').addEventListener('input', e => {
|
||||
renderMembers(e.target.value);
|
||||
});
|
||||
|
||||
/* ─── 邀请 modal ─── */
|
||||
document.getElementById('open-invite').addEventListener('click', () => {
|
||||
Shell.openModal('invite-bg');
|
||||
});
|
||||
document.querySelectorAll('#invite-bg .role-choice').forEach(c => {
|
||||
c.addEventListener('click', () => {
|
||||
document.querySelectorAll('#invite-bg .role-choice').forEach(x => x.classList.remove('selected'));
|
||||
c.classList.add('selected');
|
||||
});
|
||||
});
|
||||
document.getElementById('inv-send').addEventListener('click', () => {
|
||||
const phone = document.getElementById('inv-phone').value.trim();
|
||||
if (!phone) { Shell.toast('请填手机号', '邀请失败'); return; }
|
||||
const role = document.querySelector('#invite-bg .role-choice.selected')?.dataset.role || 'member';
|
||||
const name = document.getElementById('inv-name').value.trim() || '待激活成员';
|
||||
const daily = Number(document.getElementById('inv-daily').value) || 100;
|
||||
const monthly = Number(document.getElementById('inv-monthly').value) || 2000;
|
||||
MEMBERS.push({
|
||||
id: 'u' + Date.now(),
|
||||
av: name[0] || '新',
|
||||
name, email: phone, role,
|
||||
daily, monthly, used: 0, pending: true,
|
||||
});
|
||||
renderMembers();
|
||||
Shell.closeModal('invite-bg');
|
||||
Shell.toast('邀请已发送', phone);
|
||||
document.getElementById('inv-phone').value = '';
|
||||
document.getElementById('inv-name').value = '';
|
||||
});
|
||||
|
||||
/* ─── 编辑 modal ─── */
|
||||
let editingId = null;
|
||||
function openEdit(id) {
|
||||
const m = MEMBERS.find(x => x.id === id);
|
||||
if (!m) return;
|
||||
editingId = id;
|
||||
document.getElementById('edit-title').innerHTML = '编辑「' + m.name + '」<span id="edit-sub">// ' + m.email + '</span>';
|
||||
document.querySelectorAll('#edit-member-bg .role-choice').forEach(c => {
|
||||
c.classList.toggle('selected', c.dataset.editRole === (m.role === 'super' ? 'admin' : m.role));
|
||||
});
|
||||
document.getElementById('edit-daily').value = m.daily;
|
||||
document.getElementById('edit-monthly').value = m.monthly;
|
||||
Shell.openModal('edit-member-bg');
|
||||
}
|
||||
document.querySelectorAll('#edit-member-bg .role-choice').forEach(c => {
|
||||
c.addEventListener('click', () => {
|
||||
document.querySelectorAll('#edit-member-bg .role-choice').forEach(x => x.classList.remove('selected'));
|
||||
c.classList.add('selected');
|
||||
});
|
||||
});
|
||||
document.getElementById('edit-save').addEventListener('click', () => {
|
||||
const m = MEMBERS.find(x => x.id === editingId);
|
||||
if (!m) return;
|
||||
m.role = document.querySelector('#edit-member-bg .role-choice.selected')?.dataset.editRole || m.role;
|
||||
m.daily = Number(document.getElementById('edit-daily').value) || m.daily;
|
||||
m.monthly = Number(document.getElementById('edit-monthly').value) || m.monthly;
|
||||
Shell.closeModal('edit-member-bg');
|
||||
Shell.toast('已保存', m.name);
|
||||
renderMembers(document.getElementById('member-search').value);
|
||||
});
|
||||
document.getElementById('edit-remove').addEventListener('click', () => {
|
||||
const m = MEMBERS.find(x => x.id === editingId);
|
||||
if (!m) return;
|
||||
if (confirm('确定将「' + m.name + '」移出团队?')) {
|
||||
const i = MEMBERS.findIndex(x => x.id === m.id);
|
||||
MEMBERS.splice(i, 1);
|
||||
Shell.closeModal('edit-member-bg');
|
||||
Shell.toast('已移除', m.name);
|
||||
renderMembers(document.getElementById('member-search').value);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -118,8 +118,9 @@
|
||||
.model-visual .stack .placeholder { aspect-ratio: 3 / 4; }
|
||||
.kit-visual { grid-template-columns: repeat(4, 1fr); }
|
||||
.kit-visual .placeholder { aspect-ratio: 1 / 1; }
|
||||
.tri-visual { grid-template-columns: repeat(3, 1fr); }
|
||||
.tri-visual .placeholder { aspect-ratio: 1 / 1; }
|
||||
.tri-visual { display: block; }
|
||||
.tri-visual .placeholder { aspect-ratio: 16 / 9; }
|
||||
.tri-visual .placeholder + .placeholder { display: none; }
|
||||
|
||||
/* ─── 任务中心 · section header ─── */
|
||||
.section-h { display: flex; align-items: center; gap: 12px; margin-top: 24px; margin-bottom: 14px; }
|
||||
@ -432,9 +433,7 @@
|
||||
</div>
|
||||
|
||||
<div class="factory-visual tri-visual">
|
||||
<div class="placeholder"><span class="ph-frame">正面</span></div>
|
||||
<div class="placeholder"><span class="ph-frame">侧面</span></div>
|
||||
<div class="placeholder"><span class="ph-frame">背面</span></div>
|
||||
<div class="placeholder"><span class="ph-frame">正 / 侧 / 背 · 三视图</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -501,117 +500,13 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- ============= GRID VIEW ============= -->
|
||||
<div class="history-grid" id="task-grid" hidden>
|
||||
|
||||
<div class="task-card history-card" data-status="ok" data-type="model" data-name="补水面膜 × Ava">
|
||||
<button class="card-del-btn" type="button" title="删除任务" onclick="event.stopPropagation();" data-action="delete-task"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
|
||||
<div class="placeholder"><span class="ph-frame">Ava · 1:1</span></div>
|
||||
<div class="history-body">
|
||||
<div class="history-name">补水面膜 × Ava</div>
|
||||
<div class="history-type">模特上身图 · 4 张</div>
|
||||
<div class="history-foot">
|
||||
<span class="mono">// 05.19 · 14:30</span>
|
||||
<span class="pill ok"><span class="dot"></span>已完成</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="task-card history-card" data-status="ok" data-type="platform" data-name="精华液 × 平台套图">
|
||||
<button class="card-del-btn" type="button" title="删除任务" onclick="event.stopPropagation();"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
|
||||
<div class="placeholder"><span class="ph-frame">TB / XHS · 1:1</span></div>
|
||||
<div class="history-body">
|
||||
<div class="history-name">精华液 × 平台套图</div>
|
||||
<div class="history-type">平台套图 · 4 张</div>
|
||||
<div class="history-foot">
|
||||
<span class="mono">// 05.19 · 14:25</span>
|
||||
<span class="pill ok"><span class="dot"></span>已完成</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="task-card history-card" data-status="gen" data-type="model" data-name="防晒霜 × Luna">
|
||||
<button class="card-del-btn" type="button" title="删除任务" onclick="event.stopPropagation();"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
|
||||
<div class="placeholder"><span class="ph-frame">Luna · 1:1</span></div>
|
||||
<div class="history-body">
|
||||
<div class="history-name">防晒霜 × Luna</div>
|
||||
<div class="history-type">模特上身图 · 4 张</div>
|
||||
<div class="history-foot">
|
||||
<span class="mono">// 05.19 · 14:20</span>
|
||||
<span class="pill info"><span class="dot"></span>生成中</span>
|
||||
</div>
|
||||
<div class="history-prog">
|
||||
<div class="bar"><span style="width:65%"></span></div>
|
||||
<span class="pct">65%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="task-card history-card" data-status="err" data-type="platform" data-name="面霜 × 平台套图">
|
||||
<button class="card-del-btn" type="button" title="删除任务" onclick="event.stopPropagation();"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
|
||||
<div class="placeholder"><span class="ph-frame">DY / PDD · 1:1</span></div>
|
||||
<div class="history-body">
|
||||
<div class="history-name">面霜 × 平台套图</div>
|
||||
<div class="history-type">平台套图 · 4 张</div>
|
||||
<div class="history-foot">
|
||||
<span class="mono">// 05.19 · 14:15</span>
|
||||
<span class="pill err"><span class="dot"></span>失败</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="task-card history-card" data-status="ok" data-type="model" data-name="口红 × Mia">
|
||||
<button class="card-del-btn" type="button" title="删除任务" onclick="event.stopPropagation();"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
|
||||
<div class="placeholder"><span class="ph-frame">Mia · 1:1</span></div>
|
||||
<div class="history-body">
|
||||
<div class="history-name">口红 × Mia</div>
|
||||
<div class="history-type">模特上身图 · 4 张</div>
|
||||
<div class="history-foot">
|
||||
<span class="mono">// 05.18 · 21:08</span>
|
||||
<span class="pill ok"><span class="dot"></span>已完成</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="task-card history-card" data-status="ok" data-type="platform" data-name="眼霜 × 平台套图">
|
||||
<button class="card-del-btn" type="button" title="删除任务" onclick="event.stopPropagation();"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
|
||||
<div class="placeholder"><span class="ph-frame">TB / DY · 1:1</span></div>
|
||||
<div class="history-body">
|
||||
<div class="history-name">眼霜 × 平台套图</div>
|
||||
<div class="history-type">平台套图 · 8 张</div>
|
||||
<div class="history-foot">
|
||||
<span class="mono">// 05.18 · 18:42</span>
|
||||
<span class="pill ok"><span class="dot"></span>已完成</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="task-card history-card" data-status="ok" data-type="model" data-name="瑜伽裤 × Zoe">
|
||||
<button class="card-del-btn" type="button" title="删除任务" onclick="event.stopPropagation();"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
|
||||
<div class="placeholder"><span class="ph-frame">Zoe · 1:1</span></div>
|
||||
<div class="history-body">
|
||||
<div class="history-name">瑜伽裤 × Zoe</div>
|
||||
<div class="history-type">模特上身图 · 12 张</div>
|
||||
<div class="history-foot">
|
||||
<span class="mono">// 05.18 · 11:20</span>
|
||||
<span class="pill ok"><span class="dot"></span>已完成</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="task-card history-card" data-status="ok" data-type="platform" data-name="咖啡粉 × 平台套图">
|
||||
<button class="card-del-btn" type="button" title="删除任务" onclick="event.stopPropagation();"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
|
||||
<div class="placeholder"><span class="ph-frame">XHS / AMZ · 1:1</span></div>
|
||||
<div class="history-body">
|
||||
<div class="history-name">咖啡粉 × 平台套图</div>
|
||||
<div class="history-type">平台套图 · 4 张</div>
|
||||
<div class="history-foot">
|
||||
<span class="mono">// 05.17 · 16:50</span>
|
||||
<span class="pill ok"><span class="dot"></span>已完成</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- ============= GRID VIEW (JS 从 localStorage 动态渲染) ============= -->
|
||||
<div class="history-grid" id="task-grid" hidden></div>
|
||||
|
||||
<!-- 空态 -->
|
||||
<div id="tc-empty" hidden style="padding:60px 20px;text-align:center;color:var(--black-alpha-48);font-size:13px;line-height:1.6">
|
||||
<div style="font-family:var(--font-mono);font-size:11px;letter-spacing:.04em;margin-bottom:6px;">// NO TASKS YET</div>
|
||||
<div id="tc-empty-text">还没有任务,去上方选一个工序开始生成吧</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@ -647,68 +542,112 @@ Shell.render({
|
||||
});
|
||||
|
||||
/* ============================================================
|
||||
任务中心 · projects.html 风格 (tabs + toolbar + 双视图)
|
||||
任务中心 · 从 localStorage 读取(与 model-photo / platform-cover 共享)
|
||||
============================================================ */
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const TYPE_LABEL = { model: '模特上身图', platform: '平台套图' };
|
||||
const TYPE_LABEL = { model: '模特上身图', platform: '平台套图' };
|
||||
const STATUS_LABEL = { ok: '已完成', gen: '生成中', err: '失败' };
|
||||
const STATUS_PILL = { ok: 'ok', gen: 'info', err: 'err' };
|
||||
const STATUS_PILL = { ok: 'ok', gen: 'info', err: 'err' };
|
||||
const KEY_BY_TYPE = { model: 'fs-image-tasks-model', platform: 'fs-image-tasks-platform' };
|
||||
|
||||
const taskGrid = document.getElementById('task-grid');
|
||||
const listTbody = document.getElementById('task-list-tbody');
|
||||
const gridView = document.getElementById('task-grid');
|
||||
const gridView = taskGrid;
|
||||
const listView = document.getElementById('task-list-view');
|
||||
const cards = [...taskGrid.querySelectorAll('.task-card')];
|
||||
|
||||
let cards = []; // 动态生成的 .task-card 元素数组(顺序与任务时间倒序一致)
|
||||
|
||||
const state = { filter: 'all', type: 'all', search: '', view: 'list' };
|
||||
|
||||
function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
|
||||
|
||||
// 任务行点击 → 跳转到对应工作台,携带商品名(任务名一般是「商品 × 模特/平台」格式)
|
||||
function goToWorkbench(type, name) {
|
||||
const productName = (name || '').split(/\s[×x]\s/)[0].trim();
|
||||
const q = '?t=' + Date.now() + (productName ? '&product=' + encodeURIComponent(productName) : '');
|
||||
const url = (type === 'model') ? 'model-photo.html' + q
|
||||
: (type === 'platform') ? 'platform-cover.html' + q
|
||||
: 'projects-new.html' + q;
|
||||
/* ---------- localStorage 读写 ---------- */
|
||||
function loadType(type) {
|
||||
try { return JSON.parse(localStorage.getItem(KEY_BY_TYPE[type]) || '[]'); } catch (e) { return []; }
|
||||
}
|
||||
function saveType(type, arr) {
|
||||
try { localStorage.setItem(KEY_BY_TYPE[type], JSON.stringify(arr)); } catch (e) {}
|
||||
}
|
||||
function loadAllTasks() {
|
||||
const all = [];
|
||||
Object.keys(KEY_BY_TYPE).forEach(type => {
|
||||
loadType(type).forEach(t => all.push({ ...t, type })); // type 兜底
|
||||
});
|
||||
all.sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));
|
||||
return all;
|
||||
}
|
||||
|
||||
/* ---------- task → 渲染辅助 ---------- */
|
||||
function subTextOf(t) {
|
||||
const lbl = TYPE_LABEL[t.type] || t.type;
|
||||
const count = (t.snap && t.snap.count) || 4;
|
||||
return `${lbl} · ${count} 张`;
|
||||
}
|
||||
function thumbLabelOf(t) {
|
||||
// 取「商品 × 模特/平台」中右半作为缩略图占位文字
|
||||
const parts = (t.name || '').split(/\s[×x]\s/);
|
||||
return (parts[1] || parts[0] || '—').slice(0, 8);
|
||||
}
|
||||
|
||||
/* ---------- 点击行 / 卡片 → 跳转工作台(携带 taskId) ---------- */
|
||||
function goToWorkbench(t) {
|
||||
const url = (t.type === 'model' ? 'model-photo.html' : 'platform-cover.html')
|
||||
+ '?taskId=' + encodeURIComponent(t.id);
|
||||
location.href = url;
|
||||
}
|
||||
|
||||
/* ---------- 1. 从卡片生成 list 表行 (单数据源) ---------- */
|
||||
function rowFor(card) {
|
||||
const name = card.dataset.name;
|
||||
const type = card.dataset.type;
|
||||
const status = card.dataset.status;
|
||||
const subText = (card.querySelector('.history-type')?.textContent || '').trim();
|
||||
const timeText = (card.querySelector('.history-foot .mono')?.textContent || '').replace(/^\/\/\s*/, '');
|
||||
const pct = card.querySelector('.history-prog .pct')?.textContent || '';
|
||||
const pillClass = STATUS_PILL[status] || 'info';
|
||||
const pillLabel = STATUS_LABEL[status] || status;
|
||||
/* ---------- 1. 从 task 数据生成卡片 + list 行 ---------- */
|
||||
function cardFor(t) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'task-card history-card';
|
||||
card.dataset.status = t.status;
|
||||
card.dataset.type = t.type;
|
||||
card.dataset.name = t.name;
|
||||
card.dataset.taskId = t.id;
|
||||
card.style.cursor = 'pointer';
|
||||
card.innerHTML = `
|
||||
<button class="card-del-btn" type="button" title="删除任务">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg>
|
||||
</button>
|
||||
<div class="placeholder"><span class="ph-frame">${esc(thumbLabelOf(t))}</span></div>
|
||||
<div class="history-body">
|
||||
<div class="history-name">${esc(t.name)}</div>
|
||||
<div class="history-type">${esc(subTextOf(t))}</div>
|
||||
<div class="history-foot">
|
||||
<span class="mono">// ${esc(t.time || '')}</span>
|
||||
<span class="pill ${STATUS_PILL[t.status] || 'info'}"><span class="dot"></span>${esc(STATUS_LABEL[t.status] || t.status)}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
return card;
|
||||
}
|
||||
|
||||
function rowFor(t) {
|
||||
const tr = document.createElement('tr');
|
||||
tr.dataset.taskRow = '1';
|
||||
tr.dataset.name = name;
|
||||
tr.dataset.type = type;
|
||||
tr.dataset.status = status;
|
||||
tr.addEventListener('click', () => goToWorkbench(type, name));
|
||||
tr.dataset.name = t.name;
|
||||
tr.dataset.type = t.type;
|
||||
tr.dataset.status = t.status;
|
||||
tr.dataset.taskId = t.id;
|
||||
tr.addEventListener('click', () => goToWorkbench(t));
|
||||
tr.innerHTML = `
|
||||
<td>
|
||||
<div class="task-name-cell">
|
||||
<div class="placeholder task-thumb"><span class="ph-frame">${esc(name.split(' ')[0] || '')}</span></div>
|
||||
<div class="placeholder task-thumb"><span class="ph-frame">${esc(thumbLabelOf(t))}</span></div>
|
||||
<div>
|
||||
<div class="task-name">${esc(name)}</div>
|
||||
<div class="task-sub">${esc(subText)}</div>
|
||||
<div class="task-name">${esc(t.name)}</div>
|
||||
<div class="task-sub">${esc(subTextOf(t))}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td><span class="muted">${esc(TYPE_LABEL[type] || type)}</span></td>
|
||||
<td>${status === 'gen'
|
||||
? `<div class="task-list-prog"><div class="bar"><span style="width:${pct || '60%'}"></span></div><span class="pct">${esc(pct || '60%')}</span></div>`
|
||||
: (status === 'ok' ? '<span class="muted-2 mono" style="font-size:11px;">已完成</span>' : '<span class="muted-2 mono" style="font-size:11px;">—</span>')}</td>
|
||||
<td><span class="pill ${pillClass}"><span class="dot"></span>${esc(pillLabel)}</span></td>
|
||||
<td class="muted-2">${esc(timeText)}</td>
|
||||
<td><span class="muted">${esc(TYPE_LABEL[t.type] || t.type)}</span></td>
|
||||
<td>${t.status === 'gen'
|
||||
? `<div class="task-list-prog"><div class="bar"><span style="width:60%"></span></div><span class="pct">60%</span></div>`
|
||||
: (t.status === 'ok' ? '<span class="muted-2 mono" style="font-size:11px;">已完成</span>' : '<span class="muted-2 mono" style="font-size:11px;">—</span>')}</td>
|
||||
<td><span class="pill ${STATUS_PILL[t.status] || 'info'}"><span class="dot"></span>${esc(STATUS_LABEL[t.status] || t.status)}</span></td>
|
||||
<td class="muted-2">${esc(t.time || '')}</td>
|
||||
<td>
|
||||
<div class="row-action">
|
||||
<span class="row-more"><svg width="14" height="14" viewBox="0 0 16 16"><circle cx="3" cy="8" r="1.2" fill="currentColor"/><circle cx="8" cy="8" r="1.2" fill="currentColor"/><circle cx="13" cy="8" r="1.2" fill="currentColor"/></svg>
|
||||
@ -717,24 +656,54 @@ Shell.render({
|
||||
</div>
|
||||
</td>
|
||||
`;
|
||||
// 阻止 row-more 区域冒泡触发整行点击
|
||||
tr.querySelector('.row-more').addEventListener('click', e => e.stopPropagation());
|
||||
return tr;
|
||||
}
|
||||
|
||||
cards.forEach(card => {
|
||||
const tr = rowFor(card);
|
||||
listTbody.appendChild(tr);
|
||||
card._listRow = tr;
|
||||
tr._card = card;
|
||||
});
|
||||
/* ---------- 2. 全量重渲染(load → render → filter) ---------- */
|
||||
function renderAll() {
|
||||
// 清空 grid / list
|
||||
taskGrid.innerHTML = '';
|
||||
listTbody.innerHTML = '';
|
||||
cards = [];
|
||||
|
||||
/* ---------- 2. 构建类型 chip 菜单 ---------- */
|
||||
const tasks = loadAllTasks();
|
||||
tasks.forEach(t => {
|
||||
const card = cardFor(t);
|
||||
const row = rowFor(t);
|
||||
taskGrid.appendChild(card);
|
||||
listTbody.appendChild(row);
|
||||
card._listRow = row;
|
||||
card._task = t;
|
||||
row._card = card;
|
||||
cards.push(card);
|
||||
|
||||
// 卡片点击 → 跳工作台
|
||||
card.addEventListener('click', e => {
|
||||
if (e.target.closest('.card-del-btn')) return;
|
||||
goToWorkbench(t);
|
||||
});
|
||||
// 卡片删除按钮
|
||||
card.querySelector('.card-del-btn').addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
openDelConfirm(card);
|
||||
});
|
||||
});
|
||||
|
||||
// 类型 chip 菜单(基于现有 task 的 type 集合,动态)
|
||||
rebuildTypeMenu();
|
||||
applyFilter();
|
||||
}
|
||||
|
||||
/* ---------- 3. 构建类型 chip 菜单(基于当前 cards 的 type 集合) ---------- */
|
||||
const checkSvg = '<svg class="mi-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg>';
|
||||
const typeMenu = document.querySelector('.chip-wrap[data-key="type"] .chip-menu');
|
||||
const typeOptions = [...new Set(cards.map(c => c.dataset.type))];
|
||||
typeMenu.innerHTML = `<div class="mi selected" data-value="all">${checkSvg}<span>全部任务类型</span></div><div class="mi-sep"></div>`
|
||||
+ typeOptions.map(v => `<div class="mi" data-value="${esc(v)}">${checkSvg}<span>${esc(TYPE_LABEL[v] || v)}</span></div>`).join('');
|
||||
function rebuildTypeMenu() {
|
||||
const typeOptions = [...new Set(cards.map(c => c.dataset.type))];
|
||||
typeMenu.innerHTML = `<div class="mi selected" data-value="all">${checkSvg}<span>全部任务类型</span></div><div class="mi-sep"></div>`
|
||||
+ typeOptions.map(v => `<div class="mi" data-value="${esc(v)}">${checkSvg}<span>${esc(TYPE_LABEL[v] || v)}</span></div>`).join('');
|
||||
syncTypeChip();
|
||||
}
|
||||
|
||||
function syncTypeChip() {
|
||||
const wrap = document.querySelector('.chip-wrap[data-key="type"]');
|
||||
@ -778,6 +747,24 @@ Shell.render({
|
||||
|
||||
document.getElementById('tc-result-meta').innerHTML = `// 显示 <span class="count">${visible}</span> / ${cards.length} 个任务`;
|
||||
document.getElementById('tc-clear').hidden = !(state.search || state.type !== 'all');
|
||||
|
||||
// 空态
|
||||
const emptyEl = document.getElementById('tc-empty');
|
||||
const emptyText = document.getElementById('tc-empty-text');
|
||||
if (cards.length === 0) {
|
||||
emptyEl.hidden = false;
|
||||
emptyText.textContent = '还没有任务,去上方选一个工序开始生成吧';
|
||||
listView.hidden = true; gridView.hidden = true;
|
||||
} else if (visible === 0) {
|
||||
emptyEl.hidden = false;
|
||||
emptyText.textContent = '没有符合筛选条件的任务';
|
||||
// 视图本身保留,空表头也保留
|
||||
} else {
|
||||
emptyEl.hidden = true;
|
||||
// 恢复 view 显示(可能在 length===0 分支被隐藏)
|
||||
if (state.view === 'list') { listView.hidden = false; gridView.hidden = true; }
|
||||
else { listView.hidden = true; gridView.hidden = false; }
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- 4. 事件绑定 ---------- */
|
||||
@ -835,7 +822,7 @@ Shell.render({
|
||||
});
|
||||
});
|
||||
|
||||
/* ---------- 5. 删除 modal + 配对删除 (复用 projects.html 模式) ---------- */
|
||||
/* ---------- 6. 删除 modal + 同步 localStorage ---------- */
|
||||
const delBg = document.getElementById('tc-del-bg');
|
||||
const delBody = document.getElementById('tc-del-body');
|
||||
const delCancel = document.getElementById('tc-del-cancel');
|
||||
@ -853,37 +840,21 @@ Shell.render({
|
||||
delBg.addEventListener('click', e => { if (e.target === delBg) closeDelConfirm(); });
|
||||
delOk.addEventListener('click', () => {
|
||||
if (!_delTarget) return;
|
||||
const card = _delTarget._card || _delTarget; // 可能传 row (with ._card) 或 card (with ._listRow)
|
||||
const row = card._listRow;
|
||||
const card = _delTarget._card || _delTarget; // 可能传 row 或 card
|
||||
const taskId = card.dataset.taskId;
|
||||
const taskType = card.dataset.type;
|
||||
const name = card.dataset.name;
|
||||
card.remove();
|
||||
if (row) row.remove();
|
||||
// 同步 cards 数组
|
||||
const idx = cards.indexOf(card);
|
||||
if (idx >= 0) cards.splice(idx, 1);
|
||||
// 从 localStorage 移除对应任务
|
||||
if (taskType && KEY_BY_TYPE[taskType]) {
|
||||
const arr = loadType(taskType).filter(t => t.id !== taskId);
|
||||
saveType(taskType, arr);
|
||||
}
|
||||
closeDelConfirm();
|
||||
Shell.toast('已删除', name);
|
||||
applyFilter();
|
||||
renderAll();
|
||||
});
|
||||
|
||||
// 绑定网格卡 删除按钮
|
||||
document.querySelectorAll('.task-card .card-del-btn').forEach(btn => {
|
||||
btn.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
const card = btn.closest('.task-card');
|
||||
if (card) openDelConfirm(card);
|
||||
});
|
||||
});
|
||||
|
||||
// 绑定网格卡片点击 → 跳工作台
|
||||
cards.forEach(card => {
|
||||
card.style.cursor = 'pointer';
|
||||
card.addEventListener('click', e => {
|
||||
if (e.target.closest('.card-del-btn')) return;
|
||||
goToWorkbench(card.dataset.type, card.dataset.name);
|
||||
});
|
||||
});
|
||||
// 绑定列表行 删除按钮 (事件委托, 因为是动态生成)
|
||||
// 列表行 删除按钮(事件委托,因为是动态生成)
|
||||
listTbody.addEventListener('click', e => {
|
||||
const btn = e.target.closest('.mi-del-task');
|
||||
if (!btn) return;
|
||||
@ -892,8 +863,8 @@ Shell.render({
|
||||
if (tr) openDelConfirm(tr);
|
||||
});
|
||||
|
||||
/* ---------- 6. 初始化 ---------- */
|
||||
applyFilter();
|
||||
/* ---------- 7. 初始化 ---------- */
|
||||
renderAll();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@ -204,6 +204,25 @@
|
||||
font-size: 13.5px;
|
||||
}
|
||||
#${DRAWER_ID} .bullet-list .bl-add { background: transparent; border-style: dashed; }
|
||||
#${DRAWER_ID} .bullet-list .bl-add-row {
|
||||
margin: 8px 0 0;
|
||||
display: none;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
#${DRAWER_ID} .bullet-list .bl-add-row.show { display: flex; justify-content: flex-start; }
|
||||
#${DRAWER_ID} .bl-add-btn {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
padding: 7px 14px;
|
||||
background: var(--heat); color: #fff;
|
||||
border: 0; border-radius: var(--r-md);
|
||||
font-size: 13px; font-weight: 600;
|
||||
cursor: pointer; font-family: inherit;
|
||||
transition: filter var(--t-base);
|
||||
}
|
||||
#${DRAWER_ID} .bl-add-btn:hover { filter: brightness(.94); }
|
||||
#${DRAWER_ID} .bl-add-btn svg { width: 12px; height: 12px; }
|
||||
#${DRAWER_ID} .bullet-list .num {
|
||||
width: 22px; height: 22px;
|
||||
background: var(--surface);
|
||||
@ -298,7 +317,13 @@
|
||||
<div class="field" style="margin-bottom: 0;">
|
||||
<label class="field-label">核心卖点<span class="req">*</span></label>
|
||||
<ul class="bullet-list" data-f="bullets">
|
||||
<li class="bl-add"><span class="num">+</span><input class="bl-input" placeholder="添加新卖点 · 回车确认"></li>
|
||||
<li class="bl-add"><span class="num">+</span><input class="bl-input" placeholder="添加新卖点 · 回车或点击下方按钮"></li>
|
||||
<li class="bl-add-row" data-f="add-row">
|
||||
<button type="button" class="bl-add-btn" data-act="bl-add">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"><path d="M8 3v10M3 8h10"/></svg>
|
||||
添加卖点
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@ -371,6 +396,13 @@
|
||||
if (act === 'guide') return toast('使用指南', '点击查看完整填写指南');
|
||||
if (act === 'save') return save();
|
||||
if (act === 'upload-zone') return openFilePicker();
|
||||
if (act === 'bl-add') {
|
||||
blAdd($blInput.value);
|
||||
$blInput.value = '';
|
||||
$blInput.focus();
|
||||
updateBlAddBtn();
|
||||
return;
|
||||
}
|
||||
});
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'Escape' && drawer.classList.contains('show')) close();
|
||||
@ -388,8 +420,20 @@
|
||||
|
||||
// 卖点 bullet-list
|
||||
$blInput.addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter') { e.preventDefault(); blAdd($blInput.value); $blInput.value = ''; }
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
blAdd($blInput.value);
|
||||
$blInput.value = '';
|
||||
updateBlAddBtn();
|
||||
}
|
||||
});
|
||||
$blInput.addEventListener('input', updateBlAddBtn);
|
||||
}
|
||||
|
||||
function updateBlAddBtn() {
|
||||
const row = $bullets && $bullets.querySelector('[data-f="add-row"]');
|
||||
if (!row) return;
|
||||
row.classList.toggle('show', !!($blInput.value || '').trim());
|
||||
}
|
||||
|
||||
function openFilePicker() { if (pfFiles.length < PF_MAX) $f.file.click(); }
|
||||
@ -465,6 +509,7 @@
|
||||
pfRender();
|
||||
[...$bullets.querySelectorAll('.bl-item')].forEach(li => li.remove());
|
||||
$blInput.value = '';
|
||||
updateBlAddBtn();
|
||||
}
|
||||
|
||||
function lockBody() {
|
||||
@ -550,9 +595,36 @@
|
||||
images,
|
||||
imgs: images.length,
|
||||
};
|
||||
// 持久化到 localStorage('fs-extra-products'),让 products.html 下次加载时
|
||||
// 自动从 storage 读出并 prepend 到 grid(否则用户在工作台创建后 → 跳详情 →
|
||||
// 回商品库会看不到刚创建的商品)
|
||||
try {
|
||||
const KEY = 'fs-extra-products';
|
||||
const list = JSON.parse(localStorage.getItem(KEY) || '[]');
|
||||
list.push({
|
||||
id: product.id,
|
||||
name, cat,
|
||||
tags: '',
|
||||
assets: 0,
|
||||
videos: 0,
|
||||
bullets: points,
|
||||
date: new Date().toISOString().slice(0, 10),
|
||||
createdAt: Date.now(),
|
||||
});
|
||||
localStorage.setItem(KEY, JSON.stringify(list));
|
||||
} catch (e) { /* storage 不可用降级到只跳转 */ }
|
||||
|
||||
if (typeof currentOpts.onSave === 'function') {
|
||||
toast('商品已创建', '+ ' + name);
|
||||
currentOpts.onSave(product);
|
||||
close();
|
||||
return;
|
||||
}
|
||||
// 默认行为: 不 close drawer · 跳转期间 drawer 仍覆盖 host 页面 → 视觉上彻底
|
||||
// 消除"闪 host"(浏览器导航开始后,整页被新页面替换,drawer 自然消失)
|
||||
toast('商品已创建', '+ ' + name);
|
||||
if (typeof currentOpts.onSave === 'function') currentOpts.onSave(product);
|
||||
close();
|
||||
const url = 'product-detail.html?product=' + encodeURIComponent(name) + '&id=new';
|
||||
location.href = url;
|
||||
}
|
||||
|
||||
window.NewProductDrawer = { open, close };
|
||||
|
||||
@ -21,7 +21,7 @@ const NAV = [
|
||||
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="4" width="18" height="16"/><path d="M7 4v16M16 4v16M3 9h18M3 15h18"/></svg>'
|
||||
},
|
||||
{
|
||||
id: 'asset-factory', label: '图片生成', href: 'asset-factory.html',
|
||||
id: 'asset-factory', label: '图片生成', href: 'asset-factory.html', badge: '12',
|
||||
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l1.7 4.6L18 9l-4.3 1.4L12 15l-1.7-4.6L6 9l4.3-1.4L12 3z"/><path d="M19 16l.85 2.3L22 19l-2.15.7L19 22l-.85-2.3L16 19l2.15-.7L19 16z"/></svg>'
|
||||
},
|
||||
{
|
||||
@ -29,11 +29,11 @@ const NAV = [
|
||||
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="4" width="6" height="16"/><rect x="11" y="4" width="4" height="16"/><rect x="17" y="6" width="4" height="14"/></svg>'
|
||||
},
|
||||
{
|
||||
id: 'team', label: '团队', href: 'team.html', badge: '5',
|
||||
id: 'team', label: '团队', href: 'team.html',
|
||||
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="9" cy="8" r="3"/><circle cx="17" cy="9" r="2.5"/><path d="M3 19c0-3 2.7-5 6-5s6 2 6 5M14 19c.5-2.4 2.4-4 5-4 .8 0 1.5.2 2 .5"/></svg>'
|
||||
},
|
||||
{
|
||||
id: 'account', label: '账户', href: 'account.html',
|
||||
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>'
|
||||
},
|
||||
{
|
||||
@ -574,8 +574,18 @@ window.Shell = {
|
||||
openModal(id) {
|
||||
const el = document.getElementById(id);
|
||||
if (!el || el.classList.contains('show')) return;
|
||||
// 把 modal-bg 挪到 body 直接子节点,绕开 .content 的层叠上下文(z-index:1)困住模态、让 topbar 漏出来的问题
|
||||
if (el.parentNode !== document.body) document.body.appendChild(el);
|
||||
el.classList.add('show');
|
||||
this.lockScroll();
|
||||
if (!this._modalEsc) {
|
||||
this._modalEsc = (e) => {
|
||||
if (e.key !== 'Escape') return;
|
||||
const open = document.querySelector('.modal-bg.show');
|
||||
if (open) this.closeModal(open.id);
|
||||
};
|
||||
document.addEventListener('keydown', this._modalEsc);
|
||||
}
|
||||
},
|
||||
closeModal(id) {
|
||||
const el = document.getElementById(id);
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
<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?v=202605211643">
|
||||
<link rel="stylesheet" href="assets/restraint.css?v=202605211800">
|
||||
<style>
|
||||
.asset-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 14px; }
|
||||
.asset-grid.video-grid { grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); }
|
||||
@ -92,6 +92,25 @@
|
||||
}
|
||||
.asset-card { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); cursor: pointer; transition: background .15s; position: relative; }
|
||||
.asset-card:hover { background: var(--background-lighter); border-color: var(--black-alpha-48); }
|
||||
/* 下载按钮 · hover 卡片显示,与 card-del-btn 并列 · PRD §6.5 中间产物可下载 */
|
||||
.asset-card .card-dl-btn {
|
||||
position: absolute;
|
||||
top: 8px; right: 48px;
|
||||
width: 32px; height: 32px;
|
||||
background: rgba(255,255,255,.95);
|
||||
border: 1px solid var(--black-alpha-12);
|
||||
border-radius: var(--r-md);
|
||||
display: grid; place-items: center;
|
||||
color: var(--black-alpha-56);
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: opacity var(--t-base), background var(--t-base), color var(--t-base);
|
||||
z-index: 4;
|
||||
}
|
||||
.asset-card .card-dl-btn svg { width: 14px; height: 14px; }
|
||||
.asset-card:hover .card-dl-btn { opacity: 1; }
|
||||
.asset-card .card-dl-btn:hover { background: var(--heat); color: var(--accent-white); border-color: var(--heat); }
|
||||
body.edit-mode .asset-card .card-dl-btn { opacity: 0 !important; pointer-events: none !important; }
|
||||
/* 编辑模式 checkbox */
|
||||
.asset-card .card-check {
|
||||
position: absolute; top: 10px; left: 10px;
|
||||
@ -376,10 +395,13 @@
|
||||
<div class="asset-meta">男 · 青年 · 都市白领 · 用过 2 次</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="asset-card" data-name="妈妈 · 王姐" data-gender="女" data-age="中年" data-role="居家" data-source="手动上传" data-used="1" data-added="20260415" onclick="Shell.toast('查看资产', '王姐')">
|
||||
<div class="asset-card" data-name="妈妈 · 王姐" data-gender="女" data-age="中年" data-role="居家" data-source="手动上传" data-triview="0" data-used="1" data-added="20260415" onclick="Shell.toast('查看资产', '王姐')">
|
||||
<span class="card-check" aria-hidden="true"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg></span>
|
||||
<button class="card-del-btn" type="button" title="删除资产" onclick="event.stopPropagation();" data-action="delete-asset"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
|
||||
<div class="placeholder asset-thumb"><span class="ph-frame">妈妈 · 居家</span></div>
|
||||
<div class="placeholder asset-thumb">
|
||||
<span class="tri-missing-badge" title="手动上传的人物未生成三视图,前往图片生成可补齐"><span class="lbl-mono">缺三视图</span></span>
|
||||
<span class="ph-frame">妈妈 · 居家</span>
|
||||
</div>
|
||||
<div class="asset-body">
|
||||
<div class="asset-name">妈妈 · 王姐</div>
|
||||
<div class="asset-meta">女 · 中年 · 居家 · 用过 1 次</div>
|
||||
@ -430,10 +452,13 @@
|
||||
<div class="asset-meta">男 · 少年 · 学生 · 用过 1 次</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="asset-card" data-name="李爷爷" data-gender="男" data-age="老年" data-role="居家" data-source="手动上传" data-used="1" data-added="20260418" onclick="Shell.toast('查看资产', '李爷爷')">
|
||||
<div class="asset-card" data-name="李爷爷" data-gender="男" data-age="老年" data-role="居家" data-source="手动上传" data-triview="0" data-used="1" data-added="20260418" onclick="Shell.toast('查看资产', '李爷爷')">
|
||||
<span class="card-check" aria-hidden="true"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg></span>
|
||||
<button class="card-del-btn" type="button" title="删除资产" onclick="event.stopPropagation();" data-action="delete-asset"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
|
||||
<div class="placeholder asset-thumb"><span class="ph-frame">李爷爷 · 居家</span></div>
|
||||
<div class="placeholder asset-thumb">
|
||||
<span class="tri-missing-badge" title="手动上传的人物未生成三视图,前往图片生成可补齐"><span class="lbl-mono">缺三视图</span></span>
|
||||
<span class="ph-frame">李爷爷 · 居家</span>
|
||||
</div>
|
||||
<div class="asset-body">
|
||||
<div class="asset-name">李爷爷</div>
|
||||
<div class="asset-meta">男 · 老年 · 居家 · 用过 1 次</div>
|
||||
@ -848,6 +873,29 @@
|
||||
<script>
|
||||
Shell.render({ active: 'library', crumbs: [{ label: '工作台', href: 'index.html' }, { label: '资产库' }] });
|
||||
|
||||
/* ─── 给所有资产卡注入下载按钮 · PRD §6.5 所有中间产物可下载 ─── */
|
||||
(function injectDownloadBtns() {
|
||||
const dlSvg = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>';
|
||||
document.querySelectorAll('.asset-card').forEach(card => {
|
||||
if (card.querySelector('.card-dl-btn')) return;
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'card-dl-btn';
|
||||
btn.type = 'button';
|
||||
btn.title = '下载资产';
|
||||
btn.innerHTML = dlSvg;
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const name = card.dataset.name || '资产';
|
||||
// 推测卡片类型用作 mono 后缀
|
||||
const grid = card.closest('.asset-grid');
|
||||
const kind = grid ? grid.dataset.tab : '';
|
||||
const kindLabel = { people: '人物 · PNG', scenes: '场景 · PNG', products: '商品 · PNG', finals: '成片 · MP4 1080p', uploads: '原始素材' }[kind] || '资产';
|
||||
Shell.toast('下载中', name + ' · ' + kindLabel);
|
||||
});
|
||||
card.appendChild(btn);
|
||||
});
|
||||
})();
|
||||
|
||||
// ============== State ==============
|
||||
const TAB_KEYS = ['people', 'scenes', 'products', 'finals', 'uploads'];
|
||||
const PAGE_SIZES = [12, 24, 48, 96];
|
||||
@ -1671,43 +1719,124 @@ document.querySelectorAll('.asset-card').forEach(card => {
|
||||
});
|
||||
|
||||
/* ============================================================
|
||||
资产详情 modal · 替代旧 Shell.toast 点击
|
||||
资产详情 modal · 与 pipeline.html 共用参考布局 v2
|
||||
============================================================ */
|
||||
(function () {
|
||||
// 注入 modal HTML
|
||||
// 注入 modal CSS (与 pipeline.html 保持一致的 .asset-* 命名)
|
||||
const css = `
|
||||
.asset-modal-bg { position: fixed; inset: 0; background: rgba(0,0,0,.4); z-index: 1000; display: none; align-items: center; justify-content: center; padding: 40px; }
|
||||
.asset-modal-bg.show { display: flex; }
|
||||
.asset-modal { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); width: min(1040px, 100%); max-height: calc(100vh - 80px); overflow: hidden; display: flex; flex-direction: column; box-shadow: 0 16px 48px rgba(0,0,0,.18); }
|
||||
.asset-modal-h { display: flex; align-items: center; gap: 10px; padding: 14px 20px; border-bottom: 1px solid var(--border-faint); }
|
||||
.asset-modal-h h2 { font-size: 15px; font-weight: 600; }
|
||||
.asset-modal-h .ad-tag { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .02em; }
|
||||
.asset-modal-h .x { width: 30px; height: 30px; display: grid; place-items: center; background: transparent; border: 0; cursor: pointer; color: var(--black-alpha-56); border-radius: var(--r-sm); margin-left: auto; }
|
||||
.asset-modal-h .x:hover { background: var(--black-alpha-8); color: var(--accent-black); }
|
||||
.asset-modal-body { padding: 20px 24px 24px; overflow-y: auto; flex: 1; }
|
||||
.asset-detail-grid { display: grid; grid-template-columns: 340px 1fr; gap: 24px; }
|
||||
.asset-detail-lead { display: flex; flex-direction: column; gap: 10px; }
|
||||
.asset-detail-lead .ad-lead-wrap { position: relative; }
|
||||
.asset-detail-lead .placeholder.ad-lead-img { aspect-ratio: 3/4; border-radius: var(--r-md); }
|
||||
.asset-detail-lead .ad-zoom-btn { position: absolute; right: 10px; bottom: 10px; height: 28px; padding: 0 12px; background: rgba(21,20,15,.7); color: #fff; border: 0; border-radius: var(--r-pill); display: inline-flex; align-items: center; gap: 4px; font-size: 11.5px; font-family: inherit; cursor: pointer; }
|
||||
.asset-detail-lead .ad-zoom-btn:hover { background: rgba(21,20,15,.9); }
|
||||
.asset-detail-lead .ad-zoom-btn svg { width: 12px; height: 12px; }
|
||||
.asset-detail-lead .ad-thumbs { display: flex; gap: 8px; }
|
||||
.asset-detail-lead .ad-thumbs .thumb { flex: 0 0 64px; aspect-ratio: 3/4; border-radius: var(--r-sm); border: 1px solid var(--border-faint); cursor: pointer; overflow: hidden; }
|
||||
.asset-detail-lead .ad-thumbs .thumb:hover { border-color: var(--heat-40); }
|
||||
.asset-detail-lead .ad-thumbs .thumb.active { border-color: var(--heat); border-width: 2px; }
|
||||
.asset-detail-right .ad-section + .ad-section { margin-top: 18px; }
|
||||
.asset-detail-section-h { display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 600; color: var(--accent-black); margin-bottom: 10px; }
|
||||
.asset-detail-section-h .ic { width: 14px; height: 14px; color: var(--heat); display: grid; place-items: center; }
|
||||
.asset-detail-section-h .ic svg { width: 14px; height: 14px; }
|
||||
.asset-detail-section-h .ad-ratio-chip { margin-left: auto; font-family: var(--font-mono); font-size: 10.5px; padding: 2px 8px; border-radius: var(--r-sm); background: var(--background-lighter); border: 1px solid var(--border-faint); color: var(--black-alpha-56); }
|
||||
.asset-detail-section-h .ad-icon-btn { width: 28px; height: 28px; display: grid; place-items: center; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-sm); color: var(--black-alpha-56); cursor: pointer; }
|
||||
.asset-detail-section-h .ad-icon-btn:hover { color: var(--heat); border-color: var(--heat-40); }
|
||||
.asset-detail-section-h .ad-icon-btn svg { width: 12px; height: 12px; }
|
||||
.asset-detail-tri-row .placeholder { aspect-ratio: 16/9; border-radius: var(--r-md); }
|
||||
.asset-detail-tri-row .placeholder.missing { display: grid; place-items: center; border: 1px dashed var(--border-faint); background: var(--background-lighter); color: var(--black-alpha-48); font-family: var(--font-mono); font-size: 12px; padding: 12px; text-align: center; cursor: pointer; gap: 8px; }
|
||||
.asset-detail-tri-row .placeholder.missing:hover { border-color: var(--heat); color: var(--heat); }
|
||||
.ad-intro { font-size: 13px; line-height: 1.65; color: var(--black-alpha-72); margin: 0 0 12px; }
|
||||
.ad-tags { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.ad-tags .ad-tag-chip { height: 26px; padding: 0 12px; display: inline-flex; align-items: center; background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-sm); font-size: 12px; color: var(--accent-black); }
|
||||
.ad-tags .ad-tag-add { width: 26px; height: 26px; display: grid; place-items: center; background: var(--background-lighter); border: 1px dashed var(--black-alpha-24); border-radius: var(--r-sm); color: var(--black-alpha-56); cursor: pointer; }
|
||||
.ad-tags .ad-tag-add:hover { border-color: var(--heat); color: var(--heat); }
|
||||
.ad-tags .ad-tag-add svg { width: 12px; height: 12px; }
|
||||
.ad-props { margin-top: 18px; display: grid; grid-template-columns: repeat(3, 1fr); column-gap: 24px; row-gap: 0; border-top: 1px solid var(--border-faint); padding-top: 16px; }
|
||||
.ad-props .ad-prop { display: flex; align-items: baseline; padding: 10px 0; border-bottom: 1px solid var(--border-faint); font-size: 12.5px; min-height: 38px; }
|
||||
.ad-props .ad-prop:nth-last-child(-n+3) { border-bottom: 0; }
|
||||
.ad-props .ad-prop .k { flex: 0 0 64px; color: var(--black-alpha-56); font-family: var(--font-mono); font-size: 11px; }
|
||||
.ad-props .ad-prop .v { color: var(--accent-black); font-weight: 500; word-break: break-all; }
|
||||
.asset-detail-tip { margin-top: 10px; padding: 10px 12px; background: var(--heat-12); border: 1px solid var(--heat-20); border-radius: var(--r-sm); font-size: 12px; color: var(--accent-black); display: flex; align-items: center; gap: 8px; line-height: 1.5; }
|
||||
.asset-detail-tip svg { width: 14px; height: 14px; color: var(--heat); flex-shrink: 0; }
|
||||
.asset-detail-tip .ai-gen-btn { margin-left: auto; height: 26px; padding: 0 10px; background: var(--heat); color: #fff; border: 1px solid var(--heat); border-radius: var(--r-sm); font-size: 11.5px; cursor: pointer; font-family: inherit; flex-shrink: 0; }
|
||||
.asset-modal-f { padding: 14px 20px; border-top: 1px solid var(--border-faint); display: flex; align-items: center; gap: 8px; }
|
||||
.asset-modal-f .ad-foot-stats { display: flex; gap: 6px; margin-right: auto; }
|
||||
.asset-modal-f .ad-stat-btn { height: 32px; padding: 0 12px; display: inline-flex; align-items: center; gap: 6px; background: transparent; border: 1px solid var(--border-faint); border-radius: var(--r-sm); color: var(--black-alpha-72); font-size: 12.5px; font-family: inherit; cursor: pointer; }
|
||||
.asset-modal-f .ad-stat-btn:hover { background: var(--background-lighter); color: var(--accent-black); border-color: var(--black-alpha-24); }
|
||||
.asset-modal-f .ad-stat-btn svg { width: 13px; height: 13px; }
|
||||
.asset-modal-f .ad-stat-btn b { color: var(--accent-black); font-weight: 600; }
|
||||
`;
|
||||
const style = document.createElement('style');
|
||||
style.textContent = css;
|
||||
document.head.appendChild(style);
|
||||
|
||||
const modalHTML = `
|
||||
<div class="lib-detail-bg" id="lib-detail-bg" style="position:fixed; inset:0; background:rgba(0,0,0,.4); z-index:1000; display:none; align-items:center; justify-content:center; padding:40px;">
|
||||
<div class="lib-detail" style="background:var(--surface); border:1px solid var(--border-faint); border-radius:var(--r-md); width:min(880px,100%); max-height:calc(100vh - 80px); overflow:hidden; display:flex; flex-direction:column; box-shadow:0 16px 48px rgba(0,0,0,.18);">
|
||||
<div style="display:flex; align-items:center; gap:10px; padding:14px 20px; border-bottom:1px solid var(--border-faint);">
|
||||
<h2 id="lib-detail-title" style="font-size:15px; font-weight:600;">资产详情</h2>
|
||||
<span id="lib-detail-kind" style="font-family:var(--font-mono); font-size:11px; color:var(--black-alpha-48);">// kind</span>
|
||||
<button id="lib-detail-x" type="button" aria-label="关闭" style="width:30px; height:30px; display:grid; place-items:center; background:transparent; border:0; cursor:pointer; color:var(--black-alpha-56); border-radius:var(--r-sm); margin-left:auto;"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 6l12 12M6 18L18 6"/></svg></button>
|
||||
<div class="asset-modal-bg" id="lib-detail-bg">
|
||||
<div class="asset-modal">
|
||||
<div class="asset-modal-h">
|
||||
<h2 id="lib-detail-title">资产详情</h2>
|
||||
<span class="ad-tag" id="lib-detail-kind">/ kind</span>
|
||||
<button class="x" id="lib-detail-x" type="button" aria-label="关闭"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 6l12 12M6 18L18 6"/></svg></button>
|
||||
</div>
|
||||
<div style="padding:22px 24px; overflow-y:auto; flex:1;">
|
||||
<div style="display:grid; grid-template-columns:1fr 1fr; gap:22px;">
|
||||
<div>
|
||||
<div class="placeholder" style="aspect-ratio:3/4;" id="lib-detail-lead-img"><span class="ph-frame">立绘 / 主图</span></div>
|
||||
<div style="font-family:var(--font-mono); font-size:10.5px; color:var(--black-alpha-48); letter-spacing:.06em; text-transform:uppercase; margin:14px 0 8px;">// 三视图</div>
|
||||
<div id="lib-detail-tri" style="display:grid; grid-template-columns:repeat(3,1fr); gap:8px;">
|
||||
<div class="placeholder" style="aspect-ratio:1;"><span class="ph-frame">正面</span></div>
|
||||
<div class="placeholder" style="aspect-ratio:1;"><span class="ph-frame">侧面</span></div>
|
||||
<div class="placeholder" style="aspect-ratio:1;"><span class="ph-frame">背面</span></div>
|
||||
<div class="asset-modal-body">
|
||||
<div class="asset-detail-grid">
|
||||
<div class="asset-detail-lead">
|
||||
<div class="ad-lead-wrap">
|
||||
<div class="placeholder ad-lead-img" id="lib-detail-lead-img"><span class="ph-frame">立绘 / 主图</span></div>
|
||||
<button class="ad-zoom-btn" type="button">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 8V3h5M16 3h5v5M21 16v5h-5M8 21H3v-5"/></svg>
|
||||
查看大图
|
||||
</button>
|
||||
</div>
|
||||
<div class="ad-thumbs" id="lib-detail-thumbs"></div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-family:var(--font-mono); font-size:10.5px; color:var(--black-alpha-48); letter-spacing:.06em; text-transform:uppercase; margin-bottom:8px;">// 基础信息</div>
|
||||
<div id="lib-detail-info"></div>
|
||||
<div id="lib-detail-tip" style="display:none; margin-top:10px; padding:10px 12px; background:var(--heat-12); border:1px solid var(--heat-20); border-radius:var(--r-sm); font-size:12px; color:var(--accent-black); align-items:center; gap:8px; line-height:1.5;">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" style="color:var(--heat); flex-shrink:0;"><circle cx="12" cy="12" r="9"/><path d="M12 8v4M12 16h.01"/></svg>
|
||||
<span>暂无三视图,建议用 AI 生成以保证多角度一致性</span>
|
||||
<button id="lib-detail-aigen" type="button" style="margin-left:auto; height:26px; padding:0 10px; background:var(--heat); color:var(--accent-white); border:1px solid var(--heat); border-radius:var(--r-sm); font-size:11.5px; cursor:pointer; flex-shrink:0;">AI 生成三视图</button>
|
||||
<div class="asset-detail-right">
|
||||
<div class="ad-section" id="lib-detail-tri-section">
|
||||
<div class="asset-detail-section-h">
|
||||
<span class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg></span>
|
||||
<span class="t">三视图</span>
|
||||
<span class="ad-ratio-chip" id="lib-detail-ratio">16:9</span>
|
||||
<button class="ad-icon-btn" type="button" title="下载"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg></button>
|
||||
</div>
|
||||
<div class="asset-detail-tri-row" id="lib-detail-tri">
|
||||
<div class="placeholder"><span class="ph-frame">正 / 侧 / 背 · 三视图</span></div>
|
||||
</div>
|
||||
<div class="asset-detail-tip" id="lib-detail-tip" style="display:none;">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 8v4M12 16h.01"/></svg>
|
||||
<span>暂无三视图,建议用 AI 生成以保证多角度一致性</span>
|
||||
<button class="ai-gen-btn" type="button" id="lib-detail-aigen">AI 生成三视图</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ad-section">
|
||||
<div class="asset-detail-section-h">
|
||||
<span class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6h16M4 12h16M4 18h10"/></svg></span>
|
||||
<span class="t">简介</span>
|
||||
</div>
|
||||
<p class="ad-intro" id="lib-detail-intro"></p>
|
||||
<div class="ad-tags" id="lib-detail-tags"></div>
|
||||
</div>
|
||||
<div class="ad-props" id="lib-detail-props"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding:14px 20px; border-top:1px solid var(--border-faint); display:flex; justify-content:flex-end; gap:8px;">
|
||||
<button class="btn btn-ghost" type="button" id="lib-detail-close">关闭</button>
|
||||
<button class="btn btn-primary" type="button" id="lib-detail-apply">应用到当前项目</button>
|
||||
<div class="asset-modal-f">
|
||||
<div class="ad-foot-stats">
|
||||
<button class="ad-stat-btn" type="button">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg>
|
||||
下载
|
||||
</button>
|
||||
</div>
|
||||
<button class="btn btn-primary" type="button" id="lib-detail-apply">使用该资产</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
@ -1717,69 +1846,111 @@ document.querySelectorAll('.asset-card').forEach(card => {
|
||||
const titleEl = document.getElementById('lib-detail-title');
|
||||
const kindEl = document.getElementById('lib-detail-kind');
|
||||
const leadImg = document.getElementById('lib-detail-lead-img');
|
||||
const thumbsEl = document.getElementById('lib-detail-thumbs');
|
||||
const triSection = document.getElementById('lib-detail-tri-section');
|
||||
const triEl = document.getElementById('lib-detail-tri');
|
||||
const infoEl = document.getElementById('lib-detail-info');
|
||||
const ratioChip = document.getElementById('lib-detail-ratio');
|
||||
const introEl = document.getElementById('lib-detail-intro');
|
||||
const tagsEl = document.getElementById('lib-detail-tags');
|
||||
const propsEl = document.getElementById('lib-detail-props');
|
||||
const tipEl = document.getElementById('lib-detail-tip');
|
||||
|
||||
function _hash(s) { let h = 0; for (let i = 0; i < s.length; i++) h = ((h << 5) - h + s.charCodeAt(i)) | 0; return Math.abs(h); }
|
||||
function _fmtAssetId(name, k) { return 'ASSET-20240520-' + (k === 'person' ? 'M' : k === 'scene' ? 'S' : 'P') + String(_hash(name) % 1000).padStart(3, '0'); }
|
||||
function _fmtSize(name) { return (4 + (_hash(name) % 100) / 10).toFixed(1) + 'MB'; }
|
||||
function _fmtFav(name) { return String(8 + _hash(name) % 80); }
|
||||
function _fmtDl(name) { const n = 200 + _hash(name) % 1800; return n >= 1000 ? (n / 1000).toFixed(1) + 'k' : String(n); }
|
||||
|
||||
function open(card) {
|
||||
const name = card.dataset.name || '资产';
|
||||
let kind = 'asset', info = [];
|
||||
if (card.dataset.gender) { // 人物
|
||||
kind = '人物 · 模特';
|
||||
info = [
|
||||
['类别', '人物'],
|
||||
['性别', card.dataset.gender || '—'],
|
||||
['年龄段', card.dataset.age || '—'],
|
||||
['角色', card.dataset.role || '—'],
|
||||
['来源', card.dataset.source || '—'],
|
||||
['使用次数', (card.dataset.used || '0') + ' 次'],
|
||||
const used = card.dataset.used || '0';
|
||||
const source = card.dataset.source || '平台预设';
|
||||
let tagText = 'AI 素材', intro = '', tags = [], props = [], hasTri = false, isActor = false;
|
||||
|
||||
if (card.dataset.gender) {
|
||||
tagText = '人物 · 模特';
|
||||
isActor = true; hasTri = true;
|
||||
intro = (card.dataset.role || '人物模特') + ' · 可用于本项目人物资产生成';
|
||||
tags = [card.dataset.gender, card.dataset.age, card.dataset.role].filter(Boolean);
|
||||
props = [
|
||||
['性别', card.dataset.gender || '-'], ['种族', '东亚'], ['作品ID', _fmtAssetId(name, 'person')],
|
||||
['年龄段', card.dataset.age || '-'], ['角色', card.dataset.role || '-'], ['创作人', '流·Studio'],
|
||||
['身高', '中等'], ['来源', source], ['文件大小', _fmtSize(name)],
|
||||
['使用次数', used + ' 次'], ['授权', '商用'], ['发布时间', '2024-05-20'],
|
||||
];
|
||||
} else if (card.dataset.sceneType) {
|
||||
kind = '场景';
|
||||
info = [
|
||||
['类别', '场景'],
|
||||
['场景类型', card.dataset.sceneType],
|
||||
['来源', card.dataset.source || '—'],
|
||||
['使用次数', (card.dataset.used || '0') + ' 次'],
|
||||
tagText = '场景 · ' + card.dataset.sceneType;
|
||||
isActor = false; hasTri = false;
|
||||
intro = card.dataset.sceneType + ' 场景资产';
|
||||
tags = [card.dataset.sceneType, source].filter(Boolean);
|
||||
props = [
|
||||
['场景类型', card.dataset.sceneType], ['来源', source], ['作品ID', _fmtAssetId(name, 'scene')],
|
||||
['镜头', '通用'], ['光线', '自然光'], ['创作人', '流·Studio'],
|
||||
['用途', '本项目场景资产'], ['使用次数', used + ' 次'], ['文件大小', _fmtSize(name)],
|
||||
];
|
||||
} else if (card.dataset.product) {
|
||||
kind = '商品资产';
|
||||
info = [
|
||||
['类别', '商品'],
|
||||
['关联商品', card.dataset.product],
|
||||
['来源', card.dataset.source || '—'],
|
||||
['使用次数', (card.dataset.used || '0') + ' 次'],
|
||||
tagText = '商品资产';
|
||||
isActor = false;
|
||||
intro = '关联商品: ' + card.dataset.product;
|
||||
tags = ['商品', source].filter(Boolean);
|
||||
props = [
|
||||
['关联商品', card.dataset.product], ['来源', source], ['作品ID', _fmtAssetId(name, 'product')],
|
||||
['用途', '商品资产'], ['使用次数', used + ' 次'], ['创作人', '流·Studio'],
|
||||
['授权', '商用'], ['文件大小', _fmtSize(name)], ['发布时间', '2024-05-20'],
|
||||
];
|
||||
} else {
|
||||
kind = 'AI 素材';
|
||||
info = [
|
||||
['名称', name],
|
||||
['来源', card.dataset.source || '—'],
|
||||
tagText = 'AI 素材';
|
||||
isActor = false;
|
||||
intro = name;
|
||||
tags = [source].filter(Boolean);
|
||||
props = [
|
||||
['名称', name], ['来源', source], ['作品ID', _fmtAssetId(name, 'asset')],
|
||||
['创作人', '流·Studio'], ['文件大小', _fmtSize(name)], ['发布时间', '2024-05-20'],
|
||||
];
|
||||
}
|
||||
|
||||
titleEl.textContent = name;
|
||||
kindEl.textContent = '// ' + kind;
|
||||
kindEl.textContent = '/ ' + tagText;
|
||||
leadImg.innerHTML = '<span class="ph-frame">' + name + '</span>';
|
||||
// 三视图:人物显示,其他默认提示
|
||||
if (card.dataset.gender) {
|
||||
triEl.style.display = 'grid';
|
||||
|
||||
thumbsEl.innerHTML = ['v1','v2','v3'].map((t, i) => `<div class="thumb placeholder${i === 0 ? ' active' : ''}"><span class="ph-frame">${t}</span></div>`).join('');
|
||||
thumbsEl.querySelectorAll('.thumb').forEach(t => t.addEventListener('click', () => {
|
||||
thumbsEl.querySelectorAll('.thumb').forEach(x => x.classList.remove('active'));
|
||||
t.classList.add('active');
|
||||
}));
|
||||
|
||||
if (card.dataset.sceneType) {
|
||||
triSection.style.display = 'none';
|
||||
} else if (isActor) {
|
||||
triSection.style.display = '';
|
||||
triEl.classList.remove('actor');
|
||||
triEl.innerHTML = '<div class="placeholder"><span class="ph-frame">' + name + ' · 三视图 (正/侧/背)</span></div>';
|
||||
ratioChip.textContent = '16:9';
|
||||
tipEl.style.display = 'none';
|
||||
} else if (card.dataset.product) {
|
||||
// 商品:若未上传则提示 AI 生成
|
||||
triEl.style.display = 'grid';
|
||||
tipEl.style.display = 'flex';
|
||||
} else {
|
||||
triEl.style.display = 'none';
|
||||
tipEl.style.display = 'none';
|
||||
triSection.style.display = '';
|
||||
triEl.classList.remove('actor');
|
||||
ratioChip.textContent = '16:9';
|
||||
if (hasTri) {
|
||||
triEl.innerHTML = '<div class="placeholder"><span class="ph-frame">' + name + ' · 三视图</span></div>';
|
||||
tipEl.style.display = 'none';
|
||||
} else {
|
||||
triEl.innerHTML = '<div class="placeholder missing" data-tri="0"><svg width="22" height="22" 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 8v4M12 16h.01"/></svg><span>暂未生成三视图(16:9 单图)</span></div>';
|
||||
tipEl.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
infoEl.innerHTML = info.map(([k, v]) => '<div style="display:flex; justify-content:space-between; align-items:baseline; padding:8px 0; border-bottom:1px solid var(--border-faint); font-size:12.5px;"><span style="color:var(--black-alpha-56); font-family:var(--font-mono); font-size:11px;">' + k + '</span><span style="color:var(--accent-black);">' + v + '</span></div>').join('');
|
||||
bg.style.display = 'flex';
|
||||
|
||||
introEl.textContent = intro || '暂无简介';
|
||||
tagsEl.innerHTML = tags.map(t => '<span class="ad-tag-chip">' + t + '</span>').join('') +
|
||||
'<button class="ad-tag-add" type="button" title="添加标签"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M8 3v10M3 8h10"/></svg></button>';
|
||||
propsEl.innerHTML = props.map(([k, v]) => '<div class="ad-prop"><span class="k">' + k + '</span><span class="v">' + v + '</span></div>').join('');
|
||||
|
||||
bg.classList.add('show');
|
||||
}
|
||||
function close() { bg.style.display = 'none'; }
|
||||
function close() { bg.classList.remove('show'); }
|
||||
|
||||
bg.addEventListener('click', e => { if (e.target === bg) close(); });
|
||||
document.getElementById('lib-detail-x').addEventListener('click', close);
|
||||
document.getElementById('lib-detail-close').addEventListener('click', close);
|
||||
document.getElementById('lib-detail-apply').addEventListener('click', () => {
|
||||
Shell.toast('已应用「' + titleEl.textContent + '」', '已加入当前项目');
|
||||
close();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -167,6 +167,18 @@
|
||||
}
|
||||
.prod-row .x:hover { background: var(--black-alpha-8); color: var(--accent-crimson); }
|
||||
.prod-row .x svg { width: 12px; height: 12px; }
|
||||
.prod-row .swap {
|
||||
width: 22px; height: 22px;
|
||||
display: grid; place-items: center;
|
||||
background: transparent; border: 0;
|
||||
border-radius: var(--r-sm);
|
||||
cursor: pointer;
|
||||
color: var(--black-alpha-48);
|
||||
flex-shrink: 0;
|
||||
transition: background var(--t-base), color var(--t-base);
|
||||
}
|
||||
.prod-row .swap:hover { background: var(--heat-12); color: var(--heat); }
|
||||
.prod-row .swap svg { width: 13px; height: 13px; }
|
||||
.prod-empty {
|
||||
padding: 14px 10px;
|
||||
text-align: center;
|
||||
@ -197,6 +209,7 @@
|
||||
}
|
||||
.prod-add:hover { background: var(--heat-12); }
|
||||
.prod-add svg { width: 12px; height: 12px; }
|
||||
.prod-add[hidden] { display: none; }
|
||||
|
||||
/* 商品库全屏弹窗 */
|
||||
.pl-modal-bg {
|
||||
@ -505,14 +518,51 @@
|
||||
display: flex; flex-direction: column;
|
||||
overflow-y: auto;
|
||||
}
|
||||
/* prompt-style summary 卡片 (引号 icon + 灰底 + 右上 meta) */
|
||||
.pc-pv-h {
|
||||
display: flex; flex-direction: column; gap: 8px;
|
||||
padding-bottom: 14px; margin-bottom: 16px;
|
||||
border-bottom: 1px solid var(--border-faint);
|
||||
position: relative;
|
||||
background: var(--background-lighter);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-md);
|
||||
padding: 14px 18px 14px 44px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.pc-pv-h .row { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--black-alpha-72); }
|
||||
.pc-pv-h .row .k { font-family: var(--font-mono); font-size: 11.5px; color: var(--black-alpha-48); letter-spacing: .02em; }
|
||||
.pc-pv-h .row .v { color: var(--accent-black); font-weight: 500; }
|
||||
.pc-pv-h .quote-icon {
|
||||
position: absolute;
|
||||
top: 13px; left: 16px;
|
||||
width: 18px; height: 18px;
|
||||
color: var(--black-alpha-24);
|
||||
}
|
||||
.pc-pv-h .pv-meta {
|
||||
float: right;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
color: var(--black-alpha-48);
|
||||
letter-spacing: .04em;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.pc-pv-h .pv-meta b { color: var(--accent-black); font-weight: 600; }
|
||||
.pc-pv-h .pv-line {
|
||||
font-size: 13px;
|
||||
color: var(--accent-black);
|
||||
line-height: 1.6;
|
||||
display: flex; align-items: center;
|
||||
}
|
||||
.pc-pv-h .pv-line .k {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
color: var(--black-alpha-48);
|
||||
letter-spacing: .04em;
|
||||
margin-right: 8px;
|
||||
min-width: 36px;
|
||||
}
|
||||
.pc-pv-h .pv-line .v { font-weight: 500; }
|
||||
.pc-pv-h .pv-line .swap {
|
||||
margin-left: 10px;
|
||||
font-size: 11.5px; color: var(--heat);
|
||||
cursor: pointer;
|
||||
}
|
||||
.pc-pv-h .pv-line .swap:hover { text-decoration: underline; }
|
||||
|
||||
.pv-platform-section { margin-bottom: 24px; }
|
||||
.pv-platform-section:last-child { margin-bottom: 0; }
|
||||
@ -525,10 +575,20 @@
|
||||
margin-left: auto; font-family: var(--font-mono); font-size: 11px;
|
||||
color: var(--black-alpha-48); letter-spacing: .02em; font-weight: 400;
|
||||
}
|
||||
.pv-platform-section .ps-h .batch-lab {
|
||||
font-family: var(--font-mono); font-size: 10.5px; font-weight: 600;
|
||||
padding: 2px 8px; border-radius: var(--r-pill); letter-spacing: .04em;
|
||||
}
|
||||
.pv-platform-section .ps-h .batch-lab.gen { background: var(--background-lighter); color: var(--black-alpha-56); }
|
||||
.pv-platform-section .ps-h .batch-lab.rerun { background: var(--heat-12); color: var(--heat); }
|
||||
.pv-platform-section .ps-h .sep { color: var(--black-alpha-32); font-weight: 400; }
|
||||
.pv-platform-section .ps-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: 8px;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
@media (max-width: 1400px) {
|
||||
.pv-platform-section .ps-grid { gap: 10px; }
|
||||
}
|
||||
.pv-platform-section .ps-grid .mp-result {
|
||||
position: relative;
|
||||
@ -539,30 +599,117 @@
|
||||
border: 1px solid var(--border-faint);
|
||||
display: flex; flex-direction: column;
|
||||
}
|
||||
.pv-platform-section .ps-grid .mp-result .mp-r-thumb { flex: 1; }
|
||||
.pv-platform-section .ps-grid .mp-result .mp-r-act {
|
||||
padding: 4px 6px;
|
||||
background: rgba(255,255,255,.95);
|
||||
.pv-platform-section .ps-grid .mp-result .mp-r-thumb { flex: 1; position: relative; }
|
||||
/* hover 浮层 (重跑 + 采用) — 无遮罩,右下角横向小 icon */
|
||||
.pv-platform-section .ps-grid .mp-r-overlay {
|
||||
position: absolute; right: 8px; bottom: 8px;
|
||||
display: flex; align-items: center;
|
||||
gap: 6px;
|
||||
opacity: 0; pointer-events: none;
|
||||
transition: opacity var(--t-base);
|
||||
z-index: 2;
|
||||
}
|
||||
.pv-platform-section .ps-grid .mp-result .add-lib {
|
||||
width: 100%;
|
||||
display: inline-flex; align-items: center; justify-content: center; gap: 4px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--heat-40);
|
||||
border-radius: var(--r-sm);
|
||||
color: var(--heat);
|
||||
height: 22px;
|
||||
cursor: pointer; font-family: inherit;
|
||||
.pv-platform-section .ps-grid .mp-result:hover .mp-r-overlay { opacity: 1; pointer-events: auto; }
|
||||
.pv-platform-section .ps-grid .mp-r-overlay button {
|
||||
width: 26px; height: 26px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255,255,255,.96);
|
||||
border: 1px solid var(--border-faint);
|
||||
cursor: pointer;
|
||||
display: grid; place-items: center;
|
||||
color: var(--accent-black);
|
||||
box-shadow: 0 2px 6px rgba(21,20,15,.16);
|
||||
transition: background var(--t-base), color var(--t-base), transform var(--t-base);
|
||||
}
|
||||
.pv-platform-section .ps-grid .mp-r-overlay button:hover { background: var(--heat); color: #fff; border-color: var(--heat); transform: scale(1.06); }
|
||||
.pv-platform-section .ps-grid .mp-r-overlay button svg { width: 12px; height: 12px; }
|
||||
/* 已采用角标 */
|
||||
.pv-platform-section .ps-grid .mp-result .adopt-badge {
|
||||
position: absolute; top: 6px; left: 6px;
|
||||
background: var(--accent-forest, #42c366);
|
||||
color: #fff;
|
||||
font-family: var(--font-mono); font-size: 10px; font-weight: 600;
|
||||
letter-spacing: .04em;
|
||||
padding: 1px 6px;
|
||||
border-radius: var(--r-pill);
|
||||
z-index: 3;
|
||||
display: none;
|
||||
}
|
||||
.pv-platform-section .ps-grid .mp-result.adopted .adopt-badge { display: inline-block; }
|
||||
.pv-platform-section .ps-grid .mp-result.adopted .mp-r-overlay { display: none; }
|
||||
.pv-platform-section .ps-grid .mp-result.regenerating .mp-r-thumb::before {
|
||||
content: '生成中…';
|
||||
position: absolute; inset: 0;
|
||||
background: rgba(255,255,255,.7);
|
||||
display: grid; place-items: center;
|
||||
font-family: var(--font-mono); font-size: 11px;
|
||||
color: var(--heat); letter-spacing: .04em;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 每批次下方的批量操作 (胶囊按钮组 · 左对齐) */
|
||||
.pc-pv-batch {
|
||||
margin-top: 10px;
|
||||
display: flex; gap: 10px; align-items: center; justify-content: flex-start;
|
||||
}
|
||||
.pc-pv-batch[hidden] { display: none; }
|
||||
.pv-platform-section .pc-pv-batch.batch-foot { margin-top: 10px; }
|
||||
|
||||
/* 预览区空态 (新任务且未生成) */
|
||||
.pc-pv-empty-state {
|
||||
flex: 1;
|
||||
display: flex; flex-direction: column;
|
||||
align-items: center; justify-content: center;
|
||||
text-align: center;
|
||||
padding: 40px 24px;
|
||||
gap: 6px;
|
||||
}
|
||||
.pc-pv-empty-state[hidden] { display: none; }
|
||||
.pc-pv-empty-state .mono {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10.5px;
|
||||
transition: background var(--t-base);
|
||||
color: var(--black-alpha-48);
|
||||
letter-spacing: .06em;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.pv-platform-section .ps-grid .mp-result .add-lib:hover { background: var(--heat-12); }
|
||||
.pv-platform-section .ps-grid .mp-result .add-lib.added {
|
||||
background: var(--accent-emerald-bg, #e6f4ec);
|
||||
color: var(--accent-emerald, #1f8a51);
|
||||
border-color: var(--accent-emerald-bd, #c4e3d1);
|
||||
.pc-pv-empty-state .title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--accent-black);
|
||||
}
|
||||
.pv-platform-section .ps-grid .mp-result .add-lib svg { width: 10px; height: 10px; }
|
||||
.pc-pv-empty-state .hint {
|
||||
font-size: 12.5px;
|
||||
color: var(--black-alpha-48);
|
||||
line-height: 1.6;
|
||||
max-width: 320px;
|
||||
}
|
||||
.pc-pv-empty-state .hint b { color: var(--heat); font-weight: 600; }
|
||||
/* 预览区 hidden 时收起所有内容元素 */
|
||||
#pv-summary[hidden], #pv-results[hidden], #pv-foot[hidden] { display: none; }
|
||||
.pc-pv-batch .summary {
|
||||
margin-right: auto;
|
||||
font-family: var(--font-mono); font-size: 11px;
|
||||
color: var(--black-alpha-48); letter-spacing: .02em;
|
||||
}
|
||||
.pc-pv-batch .summary b { color: var(--heat); font-weight: 700; }
|
||||
.pc-pv-batch .pill-btn {
|
||||
height: 34px;
|
||||
padding: 0 18px;
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--black-alpha-12);
|
||||
border-radius: var(--r-pill);
|
||||
color: var(--accent-black);
|
||||
font-family: inherit; font-size: 12.5px;
|
||||
cursor: pointer;
|
||||
transition: background var(--t-base), border-color var(--t-base), color var(--t-base);
|
||||
}
|
||||
.pc-pv-batch .pill-btn:hover { background: var(--background-lighter); border-color: var(--black-alpha-24); }
|
||||
.pc-pv-batch .pill-btn.primary {
|
||||
background: var(--heat); color: #fff; border-color: var(--heat);
|
||||
}
|
||||
.pc-pv-batch .pill-btn.primary:hover { filter: brightness(.94); }
|
||||
.pc-pv-batch .pill-btn svg { width: 13px; height: 13px; }
|
||||
|
||||
.pc-pv-foot {
|
||||
margin-top: 14px; padding-top: 12px;
|
||||
@ -610,10 +757,10 @@
|
||||
|
||||
<div class="pc-layout">
|
||||
|
||||
<!-- ===== 最左栏 · 任务历史 ===== -->
|
||||
<!-- ===== 最左栏 · 任务中心 ===== -->
|
||||
<div class="pc-tasks-panel" id="tasks-panel">
|
||||
<div class="pc-tasks-h">
|
||||
历史任务
|
||||
任务中心
|
||||
<span class="ct" id="tasks-count">0</span>
|
||||
<button class="new" type="button" id="tasks-new-btn" title="新建任务">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M8 3v10M3 8h10"/></svg>
|
||||
@ -634,9 +781,8 @@
|
||||
<div class="prod-list" id="prod-list">
|
||||
<!-- 已选商品 chip · JS 动态渲染 -->
|
||||
</div>
|
||||
<button class="prod-add" type="button" id="prod-add-btn">
|
||||
<button class="prod-add" type="button" id="prod-add-btn" title="添加商品" aria-label="添加商品">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
|
||||
去商品库添加
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -647,7 +793,7 @@
|
||||
<span class="title">选择平台</span>
|
||||
</div>
|
||||
<div class="platform-grid" id="platform-grid">
|
||||
<div class="platform-card selected" data-id="dy" data-name="抖音电商">
|
||||
<div class="platform-card" data-id="dy" data-name="抖音电商">
|
||||
<div class="p-check"></div>
|
||||
<div class="p-logo dy">抖</div>
|
||||
<div class="p-name">抖音电商</div>
|
||||
@ -729,16 +875,25 @@
|
||||
|
||||
<!-- ===== 右栏 · 预览 ===== -->
|
||||
<div class="pc-preview" id="pc-preview">
|
||||
<div class="pc-pv-h">
|
||||
<div class="row"><span class="k">已选商品</span><span class="v" id="pv-prod">补水保湿精华液</span></div>
|
||||
<div class="row"><span class="k">已选平台</span><span class="v" id="pv-platforms">抖音电商 / 淘宝</span></div>
|
||||
<div class="row"><span class="k">生成</span><span class="v" id="pv-count">4</span></div>
|
||||
<!-- 空态(新任务态 & 还没立即生成时显示) -->
|
||||
<div class="pc-pv-empty-state" id="pv-empty">
|
||||
<div class="mono">// EMPTY STATE</div>
|
||||
<div class="title">还没有生成结果</div>
|
||||
<div class="hint">先选商品、选平台,点击 <b>立即生成</b> 后,效果图会出现在这里</div>
|
||||
</div>
|
||||
|
||||
<div class="pc-pv-h" id="pv-summary">
|
||||
<svg class="quote-icon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M9 7c-2.76 0-5 2.24-5 5 0 1.84 1 3.45 2.48 4.32C5.99 17.36 4.99 18 4 18v2c3.31 0 6-2.69 6-6V8c0-.55-.45-1-1-1zm9 0c-2.76 0-5 2.24-5 5 0 1.84 1 3.45 2.48 4.32-.49 1.04-1.49 1.68-2.48 1.68v2c3.31 0 6-2.69 6-6V8c0-.55-.45-1-1-1z"/></svg>
|
||||
<div class="pv-meta"><b id="pv-count">4 张</b></div>
|
||||
<div class="pv-line"><span class="k">商品</span><span class="v" id="pv-prod">未选择</span></div>
|
||||
<div class="pv-line"><span class="k">平台</span><span class="v" id="pv-platforms">未选择</span></div>
|
||||
</div>
|
||||
<div id="pv-results">
|
||||
<!-- 默认占位 -->
|
||||
</div>
|
||||
<div class="pc-pv-foot">
|
||||
// 生成结果默认不入资产库,满意后点 [入资产库] 才扣费并保存
|
||||
|
||||
<div class="pc-pv-foot" id="pv-foot">
|
||||
// 采用即扣费并入对应商品的 <a href="products.html">AI 素材库 →</a>;未采用的图不扣费、不保存
|
||||
<br>// 任务进度可在 <a href="asset-factory.html">任务中心 →</a> 查看
|
||||
</div>
|
||||
</div>
|
||||
@ -868,10 +1023,10 @@ const PRODUCTS = [
|
||||
{ id: 'p7', name: '露露同款裸感瑜伽裤', cat: '运动户外', meta: '健身房 · 42 素材' },
|
||||
];
|
||||
|
||||
// State (单选)
|
||||
// State (单选 · 默认全空)
|
||||
const state = {
|
||||
selectedProd: 'p1', // string | null
|
||||
selectedPlatform: 'dy', // string | null
|
||||
selectedProd: null, // string | null
|
||||
selectedPlatform: null, // string | null
|
||||
count: 4,
|
||||
};
|
||||
const UNIT_PRICE = 0.50;
|
||||
@ -879,16 +1034,21 @@ const UNIT_PRICE = 0.50;
|
||||
// ─── 已选商品 渲染 (单选) ───
|
||||
function renderSelectedProds() {
|
||||
const list = document.getElementById('prod-list');
|
||||
const addBtn = document.getElementById('prod-add-btn');
|
||||
const id = state.selectedProd;
|
||||
const p = id ? PRODUCTS.find(x => x.id === id) : null;
|
||||
if (!p) {
|
||||
list.innerHTML = '<div class="prod-empty">未选择商品<div class="mono">// 点击下方按钮去商品库选</div></div>';
|
||||
list.innerHTML = ''; // 不显示占位文字, 由 + 按钮提示
|
||||
if (addBtn) addBtn.hidden = false;
|
||||
document.getElementById('pv-prod').textContent = '未选择';
|
||||
} else {
|
||||
list.innerHTML = `
|
||||
<div class="prod-row" data-id="${p.id}">
|
||||
<div class="placeholder thumb"></div>
|
||||
<div class="info"><div class="nm">${p.name}</div><div class="meta">${p.cat}</div></div>
|
||||
<button class="swap" type="button" data-swap="${p.id}" title="切换商品" aria-label="切换商品">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polyline points="17 1 21 5 17 9"/><polyline points="7 23 3 19 7 15"/><path d="M21 5H9a4 4 0 0 0-4 4M3 19h12a4 4 0 0 0 4-4"/></svg>
|
||||
</button>
|
||||
<button class="x" type="button" data-rm="${p.id}" aria-label="移除">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 6l12 12M6 18L18 6"/></svg>
|
||||
</button>
|
||||
@ -898,6 +1058,10 @@ function renderSelectedProds() {
|
||||
state.selectedProd = null;
|
||||
renderSelectedProds();
|
||||
});
|
||||
list.querySelector('button.swap[data-swap]').addEventListener('click', () => {
|
||||
addBtn.click(); // 复用 + 按钮的逻辑: 打开商品库
|
||||
});
|
||||
if (addBtn) addBtn.hidden = true;
|
||||
document.getElementById('pv-prod').textContent = p.name;
|
||||
}
|
||||
updateCost();
|
||||
@ -1155,7 +1319,30 @@ document.querySelectorAll('.pill-row').forEach(row => {
|
||||
});
|
||||
});
|
||||
|
||||
// 渲染右侧预览 (单平台)
|
||||
// ─── 预览区空态 / 内容 切换 ───
|
||||
function showPreviewEmpty() {
|
||||
const empty = document.getElementById('pv-empty');
|
||||
const sum = document.getElementById('pv-summary');
|
||||
const results = document.getElementById('pv-results');
|
||||
const foot = document.getElementById('pv-foot');
|
||||
if (empty) empty.hidden = false;
|
||||
if (sum) sum.hidden = true;
|
||||
if (results) results.hidden = true;
|
||||
if (foot) foot.hidden = true;
|
||||
}
|
||||
function showPreviewContent() {
|
||||
const empty = document.getElementById('pv-empty');
|
||||
const sum = document.getElementById('pv-summary');
|
||||
const results = document.getElementById('pv-results');
|
||||
const foot = document.getElementById('pv-foot');
|
||||
if (empty) empty.hidden = true;
|
||||
if (sum) sum.hidden = false;
|
||||
if (results) results.hidden = false;
|
||||
if (foot) foot.hidden = false;
|
||||
// pv-batch 由 renderResultCards 单独控制
|
||||
}
|
||||
|
||||
// 渲染右侧预览 (占位 — 切平台/数量时显示)
|
||||
function renderPreviewSections() {
|
||||
const container = document.getElementById('pv-results');
|
||||
const id = state.selectedPlatform;
|
||||
@ -1172,37 +1359,136 @@ function renderPreviewSections() {
|
||||
</div>
|
||||
<div class="ps-grid">
|
||||
${Array(state.count).fill(0).map(() => `
|
||||
<div class="mp-result">
|
||||
<div class="placeholder mp-r-thumb"><span class="ph-frame">${c.dataset.name}</span></div>
|
||||
<div class="mp-r-act">
|
||||
<button class="add-lib" type="button" data-cost="${UNIT_PRICE}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"><path d="M12 5v14M5 12h14"/></svg>
|
||||
入资产库 ¥${UNIT_PRICE.toFixed(2)}
|
||||
</button>
|
||||
</div>
|
||||
<div class="mp-result placeholder-only">
|
||||
<div class="placeholder mp-r-thumb"><span class="ph-frame">待生成</span></div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.querySelectorAll('.add-lib').forEach(b => {
|
||||
b.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
if (b.classList.contains('added')) return;
|
||||
b.classList.add('added');
|
||||
b.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12l5 5L20 7"/></svg> 已入库';
|
||||
Shell.toast('已入库', '已扣 ¥' + (+b.dataset.cost).toFixed(2));
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染生成结果 (点立即生成时调,带 hover overlay + 批量 bar)
|
||||
const RERUN_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>';
|
||||
const ADOPT_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>';
|
||||
|
||||
let _batchSeq = 0;
|
||||
function appendBatch(n, kind) {
|
||||
const container = document.getElementById('pv-results');
|
||||
const id = state.selectedPlatform;
|
||||
const c = id ? document.querySelector('.platform-card[data-id="' + id + '"]') : null;
|
||||
if (!c) return;
|
||||
_batchSeq += 1;
|
||||
const ts = new Date();
|
||||
const tsStr = `${String(ts.getHours()).padStart(2,'0')}:${String(ts.getMinutes()).padStart(2,'0')}:${String(ts.getSeconds()).padStart(2,'0')}`;
|
||||
const labCls = kind === 'gen' ? 'gen' : 'rerun';
|
||||
const labTxt = kind === 'gen' ? `批次 ${_batchSeq} · 初始生成` : (kind === 'rerun-all' ? `批次 ${_batchSeq} · 全部重跑` : `批次 ${_batchSeq} · 单张重跑`);
|
||||
const section = document.createElement('div');
|
||||
section.className = 'pv-platform-section';
|
||||
section.dataset.kind = kind;
|
||||
section.innerHTML = `
|
||||
<div class="ps-h">
|
||||
<span class="batch-lab ${labCls}">${labTxt}</span>
|
||||
<span class="sep">·</span>
|
||||
<span>${c.dataset.name} 套图</span>
|
||||
<span class="ct">${n} 张 · ${tsStr}</span>
|
||||
</div>
|
||||
<div class="ps-grid">
|
||||
${Array(n).fill(0).map((_, i) => `
|
||||
<div class="mp-result regenerating" data-idx="${i}">
|
||||
<div class="placeholder mp-r-thumb"><span class="ph-frame">${c.dataset.name}</span></div>
|
||||
<div class="mp-r-overlay">
|
||||
<button class="r-rerun" type="button" title="重跑这一张" aria-label="重跑">${RERUN_SVG}</button>
|
||||
<button class="r-adopt" type="button" title="采用这一张" aria-label="采用">${ADOPT_SVG}</button>
|
||||
</div>
|
||||
<span class="adopt-badge">已采用</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
<div class="pc-pv-batch batch-foot">
|
||||
<button class="pill-btn rerun-batch" type="button" title="重跑这一批">
|
||||
${RERUN_SVG}
|
||||
<span>全部重跑</span>
|
||||
</button>
|
||||
<button class="pill-btn primary adopt-batch" type="button" title="采用这一批">
|
||||
${ADOPT_SVG}
|
||||
<span>全部采用 · <span class="adopted">0</span>/<span class="total">${n}</span></span>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(section);
|
||||
section.querySelectorAll('.mp-result.regenerating').forEach(card => {
|
||||
setTimeout(() => card.classList.remove('regenerating'), 1200);
|
||||
});
|
||||
section.querySelectorAll('.r-rerun').forEach(b => b.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
rerunOne(b.closest('.mp-result'));
|
||||
}));
|
||||
section.querySelectorAll('.r-adopt').forEach(b => b.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
adoptOne(b.closest('.mp-result'));
|
||||
}));
|
||||
section.querySelector('.rerun-batch').addEventListener('click', () => {
|
||||
appendBatch(n, 'rerun-all');
|
||||
Shell.toast('全部重跑', n + ' 张图重新生成中 · 新批次已追加');
|
||||
});
|
||||
section.querySelector('.adopt-batch').addEventListener('click', () => {
|
||||
const cards = section.querySelectorAll('.mp-result:not(.adopted)');
|
||||
if (!cards.length) { Shell.toast('该批次已全部采用'); return; }
|
||||
cards.forEach(c => { c.classList.remove('regenerating'); c.classList.add('adopted'); });
|
||||
updateBatchSummary();
|
||||
Shell.toast('已全部采用', cards.length + ' 张图入对应商品的 AI 素材 · 扣 ¥' + (cards.length * UNIT_PRICE).toFixed(2));
|
||||
});
|
||||
section.scrollIntoView({ behavior: 'smooth', block: 'end' });
|
||||
updateBatchSummary();
|
||||
}
|
||||
function renderResultCards() {
|
||||
const container = document.getElementById('pv-results');
|
||||
// 首次生成清掉占位 placeholder-batch,保留已有真实批次(再次「立即生成」追加新批次)
|
||||
container.querySelectorAll('.placeholder-batch').forEach(el => el.remove());
|
||||
appendBatch(state.count, 'gen');
|
||||
}
|
||||
|
||||
function rerunOne(card) {
|
||||
if (!card) return;
|
||||
appendBatch(1, 'rerun-one');
|
||||
}
|
||||
function adoptOne(card) {
|
||||
if (!card || card.classList.contains('adopted')) return;
|
||||
card.classList.remove('regenerating');
|
||||
card.classList.add('adopted');
|
||||
Shell.toast('已采用', '入对应商品的 AI 素材 · 扣 ¥' + UNIT_PRICE.toFixed(2));
|
||||
updateBatchSummary();
|
||||
}
|
||||
function updateBatchSummary() {
|
||||
document.querySelectorAll('#pv-results .pv-platform-section').forEach(section => {
|
||||
const cards = section.querySelectorAll('.mp-result:not(.placeholder-only)');
|
||||
const adopted = section.querySelectorAll('.mp-result.adopted').length;
|
||||
const adoptedEl = section.querySelector('.adopt-batch .adopted');
|
||||
const totalEl = section.querySelector('.adopt-batch .total');
|
||||
if (adoptedEl) adoptedEl.textContent = adopted;
|
||||
if (totalEl) totalEl.textContent = cards.length;
|
||||
});
|
||||
}
|
||||
|
||||
// 立即生成
|
||||
document.getElementById('pc-go-btn').addEventListener('click', () => {
|
||||
if (!state.selectedPlatform || !state.selectedProd) return;
|
||||
Shell.toast('已提交任务', '可在 [任务中心] 查看进度');
|
||||
renderPreviewSections();
|
||||
// 已有真实批次时,语义是追加新批次(用户换了设置再次生成)
|
||||
const container = document.getElementById('pv-results');
|
||||
const hasReal = container && container.querySelector('.mp-result-batch:not(.placeholder-batch)');
|
||||
if (hasReal) {
|
||||
Shell.toast('已追加批次', state.count + ' 张图新增到下方 · 旧批次保留');
|
||||
} else {
|
||||
Shell.toast('已提交任务', '可在 [任务中心] 查看进度');
|
||||
}
|
||||
showPreviewContent();
|
||||
renderResultCards();
|
||||
});
|
||||
|
||||
// 批量按钮已下沉到每个批次内部 (.rerun-batch / .adopt-batch)
|
||||
// 不再有全局 #pv-rerun-all / #pv-adopt-all
|
||||
|
||||
// URL ?product=商品名 → 替换默认选中(从 products.html 跳过来时携带)
|
||||
(function applyUrlProduct() {
|
||||
const q = new URLSearchParams(location.search);
|
||||
@ -1219,6 +1505,7 @@ document.getElementById('pc-go-btn').addEventListener('click', () => {
|
||||
// 初始
|
||||
renderSelectedProds();
|
||||
updatePlatforms();
|
||||
showPreviewEmpty(); // 默认新任务态 → 右侧显示空态
|
||||
|
||||
/* ============================================================
|
||||
任务历史 (localStorage) · 仅在「立即生成」时创建任务
|
||||
@ -1230,6 +1517,32 @@ updatePlatforms();
|
||||
|
||||
let tasks = [];
|
||||
let currentId = null;
|
||||
let _draftSnap = null; // 临时新任务的状态缓存 (切到真实任务前保存, 切回时恢复)
|
||||
|
||||
function saveDraftSnap() {
|
||||
_draftSnap = {
|
||||
selectedProd: state.selectedProd,
|
||||
selectedPlatform: state.selectedPlatform,
|
||||
count: state.count,
|
||||
};
|
||||
}
|
||||
function applyDraftToForm() {
|
||||
const snap = _draftSnap || { selectedProd: null, selectedPlatform: null, count: 4 };
|
||||
state.selectedProd = snap.selectedProd;
|
||||
state.selectedPlatform = snap.selectedPlatform;
|
||||
state.count = snap.count;
|
||||
document.querySelectorAll('.pill-row[data-key="count"] .opt').forEach(b => b.classList.toggle('active', +b.dataset.val === state.count));
|
||||
document.querySelectorAll('#platform-grid .platform-card').forEach(c => c.classList.toggle('selected', c.dataset.id === state.selectedPlatform));
|
||||
renderSelectedProds();
|
||||
updatePlatforms(); // updatePlatforms 内会调 renderPreviewSections, 重置占位 + 隐藏 batch
|
||||
showPreviewEmpty(); // 临时任务态: 右侧空态
|
||||
}
|
||||
function backToDraft() {
|
||||
if (currentId === null) return;
|
||||
applyDraftToForm();
|
||||
currentId = null;
|
||||
renderTasksList();
|
||||
}
|
||||
|
||||
const PLAT_NAME = {};
|
||||
document.querySelectorAll('#platform-grid .platform-card').forEach(c => {
|
||||
@ -1267,13 +1580,23 @@ updatePlatforms();
|
||||
function renderTasksList() {
|
||||
const root = document.getElementById('tasks-list');
|
||||
document.getElementById('tasks-count').textContent = tasks.length;
|
||||
if (!tasks.length) {
|
||||
root.innerHTML = `<div class="pc-tasks-empty">还没有历史任务<span class="mono">// 调整商品/平台/参数后生成</span></div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
// 临时新任务卡 — 仅在 draft 态 (currentId=null) 或有暂存的 _draftSnap 时显示
|
||||
// 立即生成后 currentId 指向真实任务,_draftSnap 也未设置 → 不自动追加新草稿
|
||||
const isDraft = (currentId === null);
|
||||
const hasDraft = isDraft || _draftSnap !== null;
|
||||
const draftHtml = hasDraft ? `
|
||||
<div class="pc-task-card draft${isDraft ? ' active' : ''}" data-draft="1">
|
||||
<div class="nm">新任务</div>
|
||||
<div class="meta">
|
||||
<span class="pill-mini gen">编辑中</span>
|
||||
</div>
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
const STATUS_LABEL = { gen: '生成中', ok: '已完成', err: '失败' };
|
||||
const sorted = [...tasks].sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));
|
||||
root.innerHTML = sorted.map(t => `
|
||||
const savedHtml = sorted.map(t => `
|
||||
<div class="pc-task-card${t.id === currentId ? ' active' : ''}" data-id="${t.id}">
|
||||
<div class="nm">${escapeHtml(t.name)}</div>
|
||||
<div class="meta">
|
||||
@ -1285,7 +1608,12 @@ updatePlatforms();
|
||||
</button>
|
||||
</div>
|
||||
`).join('');
|
||||
root.querySelectorAll('.pc-task-card').forEach(card => {
|
||||
root.innerHTML = draftHtml + savedHtml;
|
||||
// draft 卡点击 → 切回临时态
|
||||
const draftCard = root.querySelector('.pc-task-card.draft');
|
||||
if (draftCard) draftCard.addEventListener('click', backToDraft);
|
||||
// 真实任务卡点击 → loadTaskIntoForm
|
||||
root.querySelectorAll('.pc-task-card:not(.draft)').forEach(card => {
|
||||
card.addEventListener('click', e => {
|
||||
if (e.target.closest('button.x')) return;
|
||||
loadTaskIntoForm(card.dataset.id);
|
||||
@ -1310,6 +1638,7 @@ updatePlatforms();
|
||||
function loadTaskIntoForm(id) {
|
||||
const t = tasks.find(x => x.id === id);
|
||||
if (!t) return;
|
||||
if (currentId === null) saveDraftSnap(); // 切走 draft 前保存当前编辑状态
|
||||
state.selectedProd = t.snap.selectedProd || null;
|
||||
state.selectedPlatform = t.snap.selectedPlatform || null;
|
||||
state.count = t.snap.count;
|
||||
@ -1317,11 +1646,14 @@ updatePlatforms();
|
||||
document.querySelectorAll('#platform-grid .platform-card').forEach(c => c.classList.toggle('selected', c.dataset.id === state.selectedPlatform));
|
||||
renderSelectedProds();
|
||||
updatePlatforms();
|
||||
showPreviewContent(); // 真实任务: 显示预览内容
|
||||
renderResultCards(); // 重新渲染结果卡 (hover overlay + 批量 bar)
|
||||
currentId = id;
|
||||
renderTasksList();
|
||||
}
|
||||
|
||||
function newBlankTask() {
|
||||
_draftSnap = null; // 清空之前的 draft 缓存
|
||||
state.selectedProd = null;
|
||||
state.selectedPlatform = null;
|
||||
state.count = 4;
|
||||
@ -1329,6 +1661,7 @@ updatePlatforms();
|
||||
document.querySelectorAll('#platform-grid .platform-card').forEach(c => c.classList.remove('selected'));
|
||||
renderSelectedProds();
|
||||
updatePlatforms();
|
||||
showPreviewEmpty(); // 新任务态: 右侧空态
|
||||
currentId = null;
|
||||
renderTasksList();
|
||||
}
|
||||
@ -1354,6 +1687,13 @@ updatePlatforms();
|
||||
|
||||
tasks = load();
|
||||
renderTasksList();
|
||||
|
||||
// URL ?taskId=xxx → 从任务中心跳过来时自动载入
|
||||
(function applyUrlTaskId() {
|
||||
const q = new URLSearchParams(location.search);
|
||||
const tid = q.get('taskId');
|
||||
if (tid && tasks.some(t => t.id === tid)) loadTaskIntoForm(tid);
|
||||
})();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@ -557,9 +557,6 @@
|
||||
}
|
||||
.asset-card .meta .pill {
|
||||
padding: 2px 8px;
|
||||
background: var(--accent-emerald-bg, #e6f4ec);
|
||||
color: var(--accent-emerald, #1f8a51);
|
||||
border: 1px solid var(--accent-emerald-bd, #c4e3d1);
|
||||
border-radius: var(--r-sm);
|
||||
font-size: 10.5px;
|
||||
font-weight: 500;
|
||||
@ -912,7 +909,7 @@
|
||||
<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="filter" type="button" data-key="status">
|
||||
全部状态
|
||||
通过
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg>
|
||||
</button>
|
||||
<div class="right">
|
||||
@ -953,10 +950,6 @@
|
||||
<div class="tab-pane" data-pane="videos">
|
||||
<div class="pd-toolbar">
|
||||
<div class="total">该商品视频项目 <span class="ct">(4)</span></div>
|
||||
<button class="filter" type="button" data-key="status">
|
||||
全部状态
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg>
|
||||
</button>
|
||||
<div class="right">
|
||||
<button class="filter" type="button" data-key="sort">
|
||||
最新导出
|
||||
@ -966,10 +959,10 @@
|
||||
</div>
|
||||
|
||||
<div class="asset-grid">
|
||||
<div class="asset-card"><div class="thumb placeholder" style="aspect-ratio: 9/16;"><span class="type-pill">视频 · 9:16</span><span class="ph-frame">补水面膜 · v3</span></div><div class="meta"><span class="pill pass" data-status="pass" title="点击切换状态">通过</span><span class="date">2026-05-20 12:08</span></div></div>
|
||||
<div class="asset-card"><div class="thumb placeholder" style="aspect-ratio: 9/16;"><span class="type-pill">视频 · 9:16</span><span class="ph-frame">补水面膜 · v2</span></div><div class="meta"><span class="pill pass" data-status="pass" title="点击切换状态">通过</span><span class="date">2026-05-19 10:24</span></div></div>
|
||||
<div class="asset-card"><div class="thumb placeholder" style="aspect-ratio: 9/16;"><span class="type-pill">视频 · 9:16</span><span class="ph-frame">熬夜急救 · v1</span></div><div class="meta"><span class="pill archive" data-status="archive" title="点击切换状态">归档</span><span class="date">2026-05-18 21:42</span></div></div>
|
||||
<div class="asset-card"><div class="thumb placeholder" style="aspect-ratio: 9/16;"><span class="type-pill">视频 · 9:16</span><span class="ph-frame">补水面膜 · v1</span></div><div class="meta"><span class="pill fail" data-status="fail" title="点击切换状态">不通过</span><span class="date">2026-05-17 16:00</span></div></div>
|
||||
<div class="asset-card" data-proj-status="done"><div class="thumb placeholder" style="aspect-ratio: 9/16;"><span class="type-pill">视频 · 9:16</span><span class="ph-frame">补水面膜 · v3</span></div><div class="meta"><span class="pill ok"><span class="dot"></span>已完成</span><span class="date">2026-05-20 12:08</span></div></div>
|
||||
<div class="asset-card" data-proj-status="wip"><div class="thumb placeholder" style="aspect-ratio: 9/16;"><span class="type-pill">视频 · 9:16</span><span class="ph-frame">补水面膜 · v2</span></div><div class="meta"><span class="pill info"><span class="dot"></span>视频生成 4/6</span><span class="date">2026-05-19 10:24</span></div></div>
|
||||
<div class="asset-card" data-proj-status="archived"><div class="thumb placeholder" style="aspect-ratio: 9/16;"><span class="type-pill">视频 · 9:16</span><span class="ph-frame">熬夜急救 · v1</span></div><div class="meta"><span class="pill neutral"><span class="dot"></span>已归档</span><span class="date">2026-05-18 21:42</span></div></div>
|
||||
<div class="asset-card" data-proj-status="fail"><div class="thumb placeholder" style="aspect-ratio: 9/16;"><span class="type-pill">视频 · 9:16</span><span class="ph-frame">补水面膜 · v1</span></div><div class="meta"><span class="pill err"><span class="dot"></span>故事板失败</span><span class="date">2026-05-17 16:00</span></div></div>
|
||||
</div>
|
||||
|
||||
<div class="pd-more"><button type="button">加载更多</button></div>
|
||||
@ -1273,15 +1266,17 @@
|
||||
// 每个 (pane, key) 的可选项 + 默认值(数组首项)
|
||||
const OPTIONS = {
|
||||
'assets:type': ['全部类型', '模特上身图', '平台套图', '三视图'],
|
||||
'assets:status': ['全部状态', '通过', '不通过', '归档'],
|
||||
// 状态:默认 通过 · 没有「全部状态」选项,只能切换 通过 / 不通过 / 归档
|
||||
'assets:status': ['通过', '不通过', '归档'],
|
||||
'assets:sort': ['最新生成', '最早生成'],
|
||||
'videos:status': ['全部状态', '通过', '不通过', '归档'],
|
||||
'videos:sort': ['最新导出', '最早导出'],
|
||||
'tasks:type': ['全部类型', '模特上身图', '平台套图', '视频素材', '三视图'],
|
||||
'tasks:status': ['全部状态', '已完成', '生成中', '排队中', '失败'],
|
||||
'tasks:date': ['全部', '今天', '近 7 天', '近 30 天'],
|
||||
'tasks:sort': ['提交时间倒序', '提交时间正序'],
|
||||
};
|
||||
// 「状态」永远视为已生效筛选(没有"全部"选项),即便选了默认值也走过滤逻辑
|
||||
const ALWAYS_APPLY_KEYS = new Set(['status']);
|
||||
// 任务行 status pill 的 class → 中文标签
|
||||
const TASK_STATUS_MAP = { ok: '已完成', gen: '生成中', wait: '排队中', err: '失败', fail: '失败' };
|
||||
// assets 卡片 data-status → 中文标签
|
||||
@ -1385,6 +1380,7 @@
|
||||
btn.dataset.value = val;
|
||||
[...btn.childNodes].forEach(n => { if (n.nodeType === 3) n.remove(); });
|
||||
btn.insertBefore(document.createTextNode(val + ' '), btn.firstChild);
|
||||
// 仅在切到非默认值时才高亮(状态 chip 即便永远过滤,视觉也保持中性,与「全部类型」一致)
|
||||
btn.classList.toggle('filtered', val !== btn.dataset.default);
|
||||
}
|
||||
|
||||
@ -1396,7 +1392,9 @@
|
||||
});
|
||||
const isDefault = (key) => {
|
||||
const btn = paneEl.querySelector('.pd-toolbar .filter[data-key="' + key + '"]');
|
||||
return !btn || btn.dataset.value === btn.dataset.default;
|
||||
if (!btn) return true; // 该 pane 没这个 filter → 等同默认,不过滤
|
||||
if (ALWAYS_APPLY_KEYS.has(key)) return false; // 状态有按钮时,永远走过滤
|
||||
return btn.dataset.value === btn.dataset.default;
|
||||
};
|
||||
|
||||
if (paneId === 'assets' || paneId === 'videos') {
|
||||
@ -1429,6 +1427,15 @@
|
||||
}
|
||||
updateCount(paneEl, visible);
|
||||
toggleEmpty(paneEl, visible === 0);
|
||||
// 不足 2 行时不显示「加载更多」按钮(布局后用 offsetTop 统计行数)
|
||||
const moreEl = paneEl.querySelector('.pd-more');
|
||||
if (moreEl) {
|
||||
requestAnimationFrame(() => {
|
||||
const visCards = [...paneEl.querySelectorAll('.asset-card')].filter(c => c.style.display !== 'none');
|
||||
const rows = new Set(visCards.map(c => c.offsetTop)).size;
|
||||
moreEl.style.display = rows >= 2 ? '' : 'none';
|
||||
});
|
||||
}
|
||||
} else if (paneId === 'tasks') {
|
||||
const rows = [...paneEl.querySelectorAll('.task-table .task-row:not(.head)')];
|
||||
let visible = 0;
|
||||
|
||||
@ -734,7 +734,7 @@
|
||||
<span class="corner-tr"></span><span class="corner-bl"></span>
|
||||
<div class="picker-h">
|
||||
<div class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 3v18"/></svg></div>
|
||||
<div class="ti"><div class="t">生成白底三视图</div><div class="d">// 一次出 3 张 · 正面 / 侧面 / 背面</div></div>
|
||||
<div class="ti"><div class="t">生成白底三视图</div><div class="d">// 单张 16:9 · 正 / 侧 / 背 合一</div></div>
|
||||
<button class="x" onclick="closePicker('white')"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M18 6L6 18M6 6l12 12"/></svg></button>
|
||||
</div>
|
||||
<div class="picker-b">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user