Polish static UI flows
Some checks failed
Build and Deploy / build-and-deploy (push) Failing after 4m39s

This commit is contained in:
iye 2026-05-28 12:29:12 +08:00
parent 5edfa05369
commit bbe29622c2
45 changed files with 2187 additions and 813 deletions

99
AGENTS.md Normal file
View File

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

View File

@ -0,0 +1,21 @@
# Root Next.js Archive · 2026-05-28
This folder preserves the earlier root-level Next.js scaffold.
It was archived because the current product surface is the static HTML app in
`电商AI平台/*.html`, while this Next.js implementation only contained a partial
route set and could confuse future edits.
Archived paths:
- `app/`
- `components/`
- `k8s/`
- `package.json`
- `package-lock.json`
- `next.config.mjs`
- `next-env.d.ts`
- `postcss.config.mjs`
- `tsconfig.json`
- `deployment-guide.md`
To restore it later, move these files back to the repository root.

View File

@ -1,21 +1,27 @@
# 待归档/清理的 HTML 文件
# 已归档/保留的 HTML 文件说明
> 2026-05-21 · 设计稿对接前清理清单
> 2026-05-28 · 主流程收口记录
下列文件是早期版本或独立实验页,**不在主流程上**,对接给团队前建议归档/删除。
保留它们不会影响主流程渲染(没有 sidebar / 内部 link 指向),但会让 repo 文件目录显得混乱
下列早期版本或独立实验页**不在主流程上**,已归档到
[`_archive/deprecated-pages-20260528/`](_archive/deprecated-pages-20260528/)
## ✅ 可以直接删除(无任何入口)
根目录半成品 Next.js scaffold 已归档到
[`../_archive/root-next-20260528/`](../_archive/root-next-20260528/)。
保留本文件用于说明历史路径,避免后续误改旧页面。
## ✅ 已归档(无主流程入口)
| 文件 | 原用途 | 现状 |
|------|--------|------|
| `product-create.legacy.html` | 旧版「新建商品」全屏页(3883 行 drawer) | 已被 `product-create.html`(stub 重定向)+ drawer 模式替代,无任何链接 |
| `product-create-v2.html` | 备选「新建商品」实验版 | 仅 `product-studio.html` 单向关联,product-studio 本身已无入口 |
| `studio.html` | 早期工作室页占位 | 已被 `pipeline.html` 替代,sidebar 已切换,无 link |
| `studio-v2.html` | 早期工作室 V2 实验 | 同上 |
| `product-studio.html` | 商品 + 工作室合并实验 | 主流程已拆为 `product-detail.html` + `pipeline.html` |
| `_archive/deprecated-pages-20260528/product-create.legacy.html` | 旧版「新建商品」全屏页(3883 行 drawer) | 已被 `product-create.html` stub + drawer 模式替代 |
| `_archive/deprecated-pages-20260528/product-create-v2.html` | 备选「新建商品」实验版 | 仅供参考 |
| `_archive/deprecated-pages-20260528/studio.html` | 早期工作室页占位 | 已被 `pipeline.html` 替代 |
| `_archive/deprecated-pages-20260528/studio-v2.html` | 早期工作室 V2 实验 | 已被 `pipeline.html` 替代 |
| `_archive/deprecated-pages-20260528/product-studio.html` | 商品 + 工作室合并实验 | 主流程已拆为 `product-detail.html` + `pipeline.html` |
| `../_archive/root-next-20260528/` | 根目录 Next.js 半成品 | 静态 HTML 才是当前 source of truth |
## ⚠️ 保留但要清理 sidebar 入口判断
## ⚠️ 仍保留在主流程/辅助流程
| 文件 | 决定 |
|------|------|
@ -25,17 +31,6 @@
| `asset-factory.html` | **保留** · sidebar 入口「图片生成」 |
| `design-system.html` | **保留** · 设计系统参考,非用户路径 |
## 🚀 清理命令(对接前)
如果决定执行,可在 `电商AI平台/` 目录下:
```powershell
# Windows PowerShell
Remove-Item product-create.legacy.html, product-create-v2.html, studio.html, studio-v2.html, product-studio.html
```
清理后总文件数:**21 → 16**(含 design-system),核心 15 个。
## 📌 sidebar NAV 当前入口对照(assets/shell.js:10-43)
```

View File

@ -0,0 +1,16 @@
# Deprecated Static Pages Archive · 2026-05-28
These pages were moved out of the active static app because the current
navigation and production prototype use `products.html`, `product-detail.html`,
`pipeline.html`, `asset-factory.html`, `model-photo.html`, and
`platform-cover.html` instead.
Archived pages:
- `product-create.legacy.html`
- `product-create-v2.html`
- `product-studio.html`
- `studio.html`
- `studio-v2.html`
Keep them only as reference material. They should not be linked from the active
Airshelf static flow.

View File

@ -143,10 +143,9 @@
.quota-rules .step .formula { font-family: var(--font-mono); font-size: 11.5px; color: var(--heat); background: var(--heat-12); padding: 0 4px; border-radius: var(--r-sm); }
/* ─── 表格通用 ─── */
.billing-table { width: 100%; border-collapse: separate; border-spacing: 0; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); overflow: hidden; }
.billing-table th, .billing-table td { padding: 11px 14px; text-align: left; font-size: 12.5px; border-bottom: 1px solid var(--border-faint); }
.billing-table thead th { background: var(--background-lighter); font-family: var(--font-mono); font-size: 10.5px; font-weight: 500; color: var(--black-alpha-48); letter-spacing: .04em; text-transform: uppercase; }
.billing-table tbody tr:last-child td { border-bottom: 0; }
.billing-table { width: 100%; border-collapse: separate; border-spacing: 0; background: var(--surface); border: 1px solid var(--border-muted); border-radius: var(--r-md); overflow: hidden; }
.billing-table th, .billing-table td { padding: 11px 14px; text-align: left; font-size: 12.5px; border-bottom: 0; }
.billing-table thead th { background: var(--background-lighter); border-bottom: 1px solid var(--border-muted); font-family: var(--font-mono); font-size: 10.5px; font-weight: 500; color: var(--black-alpha-48); letter-spacing: .04em; text-transform: uppercase; }
.billing-table tbody tr:hover { background: var(--background-lighter); }
.billing-table .ts { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .02em; }
.billing-table .neg { font-variant-numeric: tabular-nums; font-weight: 500; color: var(--accent-black); text-align: right; }

View File

@ -157,7 +157,25 @@
.history-card .placeholder { width: 78px; height: 78px; }
/* ─── 列表视图 · 表格 ─── */
#task-list-view {
background: var(--surface);
border: 1px solid var(--border-muted);
border-radius: var(--r-md);
overflow: hidden;
}
#task-list-view[hidden] { display: none; }
#task-list-view table.t {
border: 0;
border-radius: 0;
background: transparent;
}
#task-list-view table.t thead th {
background: var(--background-lighter);
border-bottom-color: var(--border-muted);
}
#task-list-view table.t tbody td {
border-bottom: 0;
}
.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; }

View File

@ -1482,9 +1482,9 @@ select.duration-select:focus,
/* ─── Table ─── */
table.t {
width: 100%; border-collapse: collapse;
width: 100%; border-collapse: separate; border-spacing: 0;
background: var(--surface);
border: 1px solid var(--border-faint);
border: 1px solid var(--border-muted);
border-radius: var(--r-md);
overflow: hidden;
}
@ -1495,19 +1495,18 @@ table.t thead th {
color: var(--black-alpha-48);
padding: 14px 16px;
background: var(--black-alpha-3);
border-bottom: 1px solid var(--border-faint);
border-bottom: 1px solid var(--border-muted);
letter-spacing: .04em;
text-transform: uppercase;
font-family: var(--font-mono);
}
table.t tbody td {
padding: 16px;
border-bottom: 1px solid var(--border-faint);
border-bottom: 0;
font-size: 13px;
vertical-align: middle;
color: var(--accent-black);
}
table.t tbody tr:last-child td { border-bottom: 0; }
table.t tbody tr { cursor: pointer; transition: background var(--t-base); }
table.t tbody tr:hover { background: var(--black-alpha-4); }
@ -1589,6 +1588,255 @@ table.t tbody tr:hover { background: var(--black-alpha-4); }
letter-spacing: .04em; font-weight: 400;
}
/* ─── Global command palette + account menu ─── */
.shell-command-bg {
position: fixed;
inset: 0;
z-index: 1800;
display: none;
align-items: flex-start;
justify-content: center;
padding: 12vh 16px 24px;
background: var(--black-alpha-48);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
.shell-command-bg.show { display: flex; }
.shell-command {
width: min(640px, 100%);
max-height: min(680px, 76vh);
display: flex;
flex-direction: column;
background: var(--surface-raised);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
box-shadow: var(--shadow-floating);
overflow: hidden;
}
.shell-command-h {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 16px;
border-bottom: 1px solid var(--border-faint);
}
.shell-command-h .ic {
width: 30px;
height: 30px;
display: grid;
place-items: center;
background: var(--heat-12);
color: var(--heat);
border: 1px solid var(--heat-20);
border-radius: var(--r-md);
flex-shrink: 0;
}
.shell-command-h .ic svg { width: 14px; height: 14px; }
.shell-command-h input {
flex: 1;
min-width: 0;
height: 34px;
border: 0;
background: transparent;
color: var(--accent-black);
font-family: inherit;
font-size: 15px;
outline: none;
}
.shell-command-h input::placeholder { color: var(--black-alpha-48); }
.shell-command-close {
height: 26px;
padding: 0 8px;
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
background: var(--background-lighter);
color: var(--black-alpha-48);
font-family: var(--font-inter);
font-size: 11px;
font-weight: 700;
letter-spacing: .02em;
cursor: pointer;
}
.shell-command-close:hover { color: var(--accent-black); border-color: var(--black-alpha-24); }
.shell-command-list {
padding: 8px;
overflow-y: auto;
}
.shell-command-section {
padding: 8px 10px 6px;
font-family: var(--font-mono);
font-size: 10.5px;
color: var(--black-alpha-48);
letter-spacing: .06em;
}
.shell-command-item {
width: 100%;
min-height: 48px;
display: flex;
align-items: center;
gap: 12px;
padding: 8px 10px;
border: 0;
border-radius: var(--r-md);
background: transparent;
color: var(--accent-black);
font-family: inherit;
text-align: left;
cursor: pointer;
}
.shell-command-item:hover,
.shell-command-item.active {
background: var(--heat-12);
color: var(--heat);
}
.shell-command-item .cmd-ic {
width: 30px;
height: 30px;
display: grid;
place-items: center;
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
background: var(--surface);
color: currentColor;
flex-shrink: 0;
}
.shell-command-item .cmd-ic svg { width: 14px; height: 14px; }
.shell-command-item .cmd-main {
flex: 1;
min-width: 0;
}
.shell-command-item .cmd-title {
display: block;
font-size: 13px;
font-weight: 600;
color: currentColor;
}
.shell-command-item .cmd-sub {
display: block;
margin-top: 2px;
font-size: 12px;
line-height: 1.45;
color: var(--black-alpha-56);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.shell-command-item:hover .cmd-sub,
.shell-command-item.active .cmd-sub { color: var(--black-alpha-72); }
.shell-command-item .cmd-key {
min-width: 26px;
height: 22px;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0 7px;
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
background: var(--surface);
color: var(--black-alpha-48);
font-family: var(--font-inter);
font-size: 10px;
font-weight: 700;
letter-spacing: .02em;
}
.shell-command-empty {
padding: 48px 20px;
display: grid;
place-items: center;
gap: 8px;
color: var(--black-alpha-48);
font-size: 13px;
}
.shell-command-foot {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 16px;
border-top: 1px solid var(--border-faint);
color: var(--black-alpha-48);
font-family: var(--font-mono);
font-size: 10.5px;
letter-spacing: .04em;
}
.shell-command-foot .spacer { flex: 1; }
.shell-account-menu {
position: fixed;
z-index: 1700;
min-width: 232px;
display: none;
padding: 8px;
background: var(--surface-raised);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
box-shadow: var(--shadow-floating);
}
.shell-account-menu.show { display: block; }
.shell-account-head {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 8px 10px;
border-bottom: 1px solid var(--border-faint);
margin-bottom: 6px;
}
.shell-account-head .av {
width: 30px;
height: 30px;
display: grid;
place-items: center;
border-radius: 6px;
background: var(--accent-black);
color: var(--accent-white);
font-size: 12px;
font-weight: 600;
}
.shell-account-head .nm {
display: block;
font-size: 13px;
font-weight: 600;
color: var(--accent-black);
}
.shell-account-head .mail {
display: block;
margin-top: 2px;
font-family: var(--font-mono);
font-size: 10.5px;
color: var(--black-alpha-48);
letter-spacing: .02em;
}
.shell-account-menu button {
width: 100%;
min-height: 34px;
display: flex;
align-items: center;
gap: 9px;
padding: 0 9px;
border: 0;
border-radius: var(--r-sm);
background: transparent;
color: var(--accent-black);
font-family: inherit;
font-size: 13px;
text-align: left;
cursor: pointer;
}
.shell-account-menu button svg {
width: 14px;
height: 14px;
color: var(--black-alpha-56);
}
.shell-account-menu button:hover {
background: var(--black-alpha-4);
color: var(--heat);
}
.shell-account-menu button:hover svg { color: var(--heat); }
.shell-account-menu .sep {
height: 1px;
background: var(--border-faint);
margin: 6px 4px;
}
/* ─── Modal ─── */
.modal-bg {
position: fixed; inset: 0;

View File

@ -44,6 +44,23 @@ const NAV = [
}
];
const SHELL_COMMANDS = [
{ id: 'dashboard', group: '导航', label: '工作台', sub: '任务队列、今日消耗、项目进度', href: 'index.html', icon: 'dashboard', key: 'D' },
{ id: 'products', group: '导航', label: '商品库', sub: '管理 SKU、商品图册、卖点信息', href: 'products.html', icon: 'package', key: 'P' },
{ id: 'projects', group: '导航', label: '视频项目', sub: '查看五阶段短视频流水线', href: 'projects.html', icon: 'clapperboard', key: 'V' },
{ id: 'asset-factory', group: '导航', label: '图片生成', sub: '模特上身图、平台套图、图片创作', href: 'asset-factory.html', icon: 'sparkles', key: 'I' },
{ id: 'library', group: '导航', label: '资产库', sub: '素材、人物、场景、成片统一管理', href: 'library.html', icon: 'folder', key: 'A' },
{ id: 'team', group: '导航', label: '团队', sub: '成员、权限、额度、协作记录', href: 'team.html', icon: 'users' },
{ id: 'account', group: '导航', label: '消费', sub: '余额、充值、账单流水', href: 'account.html', icon: 'creditCard' },
{ id: 'settings', group: '导航', label: '设置', sub: '个人信息、通知、安全、偏好', href: 'settings.html', icon: 'settings' },
{ id: 'messages', group: '常用动作', label: '消息中心', sub: '任务提醒、协作评论、系统通知', href: 'messages.html', icon: 'bell', key: 'M' },
{ id: 'new-product', group: '常用动作', label: '新建商品', sub: '从商品信息开始生成素材与视频', href: 'product-create.html', icon: 'productPlus' },
{ id: 'new-project', group: '常用动作', label: '新建视频项目', sub: '选择商品并进入脚本配置', href: 'projects-new.html', icon: 'plus' },
{ id: 'model-photo', group: '常用动作', label: '生成模特上身图', sub: '快速生成 3:4 商品展示素材', href: 'model-photo.html', icon: 'users' },
{ id: 'platform-cover', group: '常用动作', label: '生成平台套图', sub: '适配电商平台封面与详情图', href: 'platform-cover.html', icon: 'images' },
{ id: 'image-optimize', group: '常用动作', label: '图片创作', sub: '对话式生成、编辑、加入资产库', href: 'image-optimize.html', icon: 'image' },
];
window.Shell = {
render({ active = '', crumbs = [], balance = '¥327.40', topActions = '' } = {}) {
const navHtml = NAV.map(n => `
@ -59,15 +76,15 @@ window.Shell = {
<div class="brand">
<img class="brand-logo" src="assets/logo.png" alt="Airshelf">
</div>
<div class="search-box" onclick="document.getElementById('global-search').focus()">
<div class="search-box" onclick="Shell.openCommandPalette()">
${ShellIcon('search')}
<input id="global-search" placeholder="搜索"/>
<input id="global-search" placeholder="搜索" readonly aria-label="打开全局搜索"/>
<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="user" onclick="Shell.toggleAccountMenu(event)">
<div class="av"></div>
<div class="em">小李的店</div>
</div>
@ -103,7 +120,7 @@ window.Shell = {
${ShellIcon('bell')}
<span class="count-noti">12</span>
</button>
<div class="topbar-avatar" onclick="Shell.toast('账户菜单', '李 · li@shop.com')" title="账户">
<div class="topbar-avatar" onclick="Shell.toggleAccountMenu(event)" title="账户">
<span></span>
</div>
${topActions}
@ -126,6 +143,43 @@ window.Shell = {
</div>
`;
const commandHtml = `
<div class="shell-command-bg" id="shell-command-bg" aria-hidden="true">
<div class="shell-command" role="dialog" aria-modal="true" aria-labelledby="shell-command-title">
<div class="shell-command-h">
<span class="ic">${ShellIcon('search')}</span>
<input id="shell-command-input" autocomplete="off" placeholder="搜索页面、动作、项目入口" aria-label="搜索命令">
<button class="shell-command-close" type="button" id="shell-command-close">ESC</button>
</div>
<div class="shell-command-list" id="shell-command-list"></div>
<div class="shell-command-foot">
<span id="shell-command-title">COMMAND</span>
<span>// Enter 打开 · Ctrl K 唤起</span>
<span class="spacer"></span>
<span id="shell-command-count">0 </span>
</div>
</div>
</div>
`;
const accountMenuHtml = `
<div class="shell-account-menu" id="shell-account-menu" aria-hidden="true">
<div class="shell-account-head">
<span class="av"></span>
<span>
<span class="nm">小李的店</span>
<span class="mail">li@shop.com</span>
</span>
</div>
<button type="button" data-account-act="settings">${ShellIcon('settings')}个人设置</button>
<button type="button" data-account-act="messages">${ShellIcon('bell')}消息中心</button>
<button type="button" data-account-act="team">${ShellIcon('users')}团队管理</button>
<button type="button" data-account-act="account">${ShellIcon('creditCard')}消费与余额</button>
<div class="sep"></div>
<button type="button" data-account-act="logout">${ShellIcon('arrowRight')}退出登录</button>
</div>
`;
// ─── 全局 Lightbox · 任意页面可用 Shell._openLightbox(src, name) ──
const lightboxHtml = `
<div class="np-lightbox" id="np-lightbox" onclick="Shell._closeLightbox()">
@ -284,15 +338,203 @@ window.Shell = {
}
document.body.insertAdjacentHTML('beforeend', toastHtml);
document.body.insertAdjacentHTML('beforeend', lightboxHtml);
document.body.insertAdjacentHTML('beforeend', commandHtml);
document.body.insertAdjacentHTML('beforeend', accountMenuHtml);
document.addEventListener('keydown', e => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault();
document.getElementById('global-search')?.focus();
Shell.openCommandPalette();
}
});
this.enhanceSelects(document);
this._bindGlobalChrome();
},
_esc(s) {
return String(s ?? '').replace(/[&<>"']/g, c => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
})[c]);
},
_bindGlobalChrome() {
const input = document.getElementById('global-search');
if (input && !input.dataset.shellBound) {
input.dataset.shellBound = '1';
input.addEventListener('focus', () => {
input.blur();
Shell.openCommandPalette();
});
input.addEventListener('keydown', e => {
e.preventDefault();
Shell.openCommandPalette();
});
}
const bg = document.getElementById('shell-command-bg');
const closeBtn = document.getElementById('shell-command-close');
const cmdInput = document.getElementById('shell-command-input');
if (bg && !bg.dataset.shellBound) {
bg.dataset.shellBound = '1';
bg.addEventListener('click', e => {
if (e.target === bg) Shell.closeCommandPalette();
});
closeBtn?.addEventListener('click', () => Shell.closeCommandPalette());
cmdInput?.addEventListener('input', () => Shell.renderCommandList(cmdInput.value));
cmdInput?.addEventListener('keydown', e => {
if (e.key === 'Escape') {
e.preventDefault();
Shell.closeCommandPalette();
return;
}
if (e.key !== 'Enter') return;
const first = document.querySelector('#shell-command-list .shell-command-item');
if (first) {
e.preventDefault();
Shell.runCommand(first.dataset.command);
}
});
}
const accountMenu = document.getElementById('shell-account-menu');
if (accountMenu && !accountMenu.dataset.shellBound) {
accountMenu.dataset.shellBound = '1';
accountMenu.addEventListener('click', e => {
const item = e.target.closest('[data-account-act]');
if (!item) return;
Shell.closeAccountMenu();
const act = item.dataset.accountAct;
const hrefs = {
settings: 'settings.html',
messages: 'messages.html',
team: 'team.html',
account: 'account.html',
logout: 'login.html',
};
if (act === 'logout') {
Shell.toast('已退出登录', '返回登录页');
setTimeout(() => { location.href = hrefs.logout; }, 220);
return;
}
if (hrefs[act]) location.href = hrefs[act];
});
}
if (!this._globalChromeDocBound) {
this._globalChromeDocBound = true;
document.addEventListener('click', e => {
if (!e.target.closest('.shell-account-menu') && !e.target.closest('.topbar-avatar') && !e.target.closest('.user')) {
Shell.closeAccountMenu();
}
});
document.addEventListener('keydown', e => {
if (e.key === 'Escape') {
Shell.closeCommandPalette();
Shell.closeAccountMenu();
}
});
}
this.renderCommandList('');
},
openCommandPalette(query = '') {
const bg = document.getElementById('shell-command-bg');
const input = document.getElementById('shell-command-input');
if (!bg || !input) return;
this.closeAccountMenu();
const wasOpen = bg.classList.contains('show');
bg.classList.add('show');
bg.setAttribute('aria-hidden', 'false');
input.value = query;
this.renderCommandList(query);
if (!wasOpen) this.lockScroll();
requestAnimationFrame(() => input.focus());
},
closeCommandPalette() {
const bg = document.getElementById('shell-command-bg');
if (!bg || !bg.classList.contains('show')) return;
bg.classList.remove('show');
bg.setAttribute('aria-hidden', 'true');
this.unlockScroll();
},
renderCommandList(query = '') {
const list = document.getElementById('shell-command-list');
const count = document.getElementById('shell-command-count');
if (!list) return;
const q = String(query || '').trim().toLowerCase();
const items = SHELL_COMMANDS.filter(cmd => {
if (!q) return true;
return [cmd.label, cmd.sub, cmd.group, cmd.id].join(' ').toLowerCase().includes(q);
});
if (count) count.textContent = items.length + ' 项';
if (!items.length) {
list.innerHTML = `<div class="shell-command-empty">${ShellIcon('search')}<span>没有匹配的入口</span><span class="shell-command-section">// 换个关键词试试</span></div>`;
return;
}
let lastGroup = '';
list.innerHTML = items.map((cmd, i) => {
const section = cmd.group !== lastGroup ? `<div class="shell-command-section">${this._esc(cmd.group)}</div>` : '';
lastGroup = cmd.group;
return section + `
<button class="shell-command-item${i === 0 ? ' active' : ''}" type="button" data-command="${this._esc(cmd.id)}">
<span class="cmd-ic">${ShellIcon(cmd.icon)}</span>
<span class="cmd-main">
<span class="cmd-title">${this._esc(cmd.label)}</span>
<span class="cmd-sub">${this._esc(cmd.sub)}</span>
</span>
${cmd.key ? `<span class="cmd-key">${this._esc(cmd.key)}</span>` : ''}
</button>
`;
}).join('');
list.querySelectorAll('.shell-command-item').forEach(btn => {
btn.addEventListener('click', () => Shell.runCommand(btn.dataset.command));
});
},
runCommand(id) {
const cmd = SHELL_COMMANDS.find(item => item.id === id);
if (!cmd) return;
this.closeCommandPalette();
if (cmd.href) {
location.href = cmd.href;
return;
}
Shell.toast('已执行', cmd.label);
},
toggleAccountMenu(event) {
event?.stopPropagation?.();
const menu = document.getElementById('shell-account-menu');
const anchor = event?.currentTarget;
if (!menu || !anchor) return;
const shouldOpen = !menu.classList.contains('show');
this.closeCommandPalette();
if (!shouldOpen) {
this.closeAccountMenu();
return;
}
const rect = anchor.getBoundingClientRect();
menu.classList.add('show');
menu.setAttribute('aria-hidden', 'false');
const width = menu.offsetWidth || 232;
const height = menu.offsetHeight || 260;
let left = rect.right - width;
if (left < 12) left = rect.left;
left = Math.min(Math.max(12, left), window.innerWidth - width - 12);
let top = rect.bottom + 8;
if (top + height > window.innerHeight - 12) top = Math.max(12, rect.top - height - 8);
menu.style.left = left + 'px';
menu.style.top = top + 'px';
},
closeAccountMenu() {
const menu = document.getElementById('shell-account-menu');
if (!menu) return;
menu.classList.remove('show');
menu.setAttribute('aria-hidden', 'true');
},
enhanceSelects(root = document) {

View File

@ -540,25 +540,25 @@ code.tk.heat { background: var(--heat-12); color: var(--heat); border-color: var
/* === Audit table (§7 待统一清单) === */
.audit-table {
width: 100%; border-collapse: collapse;
width: 100%; border-collapse: separate; border-spacing: 0;
background: var(--surface);
border: 1px solid var(--border-faint);
border: 1px solid var(--border-muted);
border-radius: var(--r-md); overflow: hidden;
margin-bottom: 22px;
}
.audit-table th, .audit-table td {
padding: 12px 16px; text-align: left;
font-size: 12.5px; color: var(--accent-black);
border-bottom: 1px solid var(--border-faint);
border-bottom: 0;
vertical-align: top;
}
.audit-table th {
border-bottom: 1px solid var(--border-muted);
font-family: var(--font-mono); font-size: 11px;
font-weight: 500; color: var(--black-alpha-56);
letter-spacing: .04em; text-transform: uppercase;
background: var(--black-alpha-3);
}
.audit-table tbody tr:last-child td { border-bottom: 0; }
.audit-table td:first-child { font-family: var(--font-mono); font-size: 12px; color: var(--heat); width: 32px; }
.audit-table td.problem { color: var(--accent-crimson); font-weight: 500; }
.audit-table td.now { font-family: var(--font-mono); font-size: 11.5px; color: var(--black-alpha-64); }
@ -2779,7 +2779,7 @@ code.tk.heat { background: var(--heat-12); color: var(--heat); border-color: var
<div class="desc">锁视口高度 · 多栏内部滚 · 大量页面级独有样式。<strong style="color:var(--accent-crimson)">⚠️ 这类定制最多 · 改之前先确认 restraint.css 没现成组件</strong></div>
<pre class="ascii">[ 左栏 · 资产/导航 sticky ] [ 中央 · 画布/参数 ] [ 右栏 · 预览/AI 助手 ]
└───────── 整页 viewport 高度锁定 · 不滚 main ────────┘</pre>
<div class="pages">代表页:<strong>pipeline.html · model-photo.html · image-optimize.html · studio.html</strong></div>
<div class="pages">代表页:<strong>pipeline.html · model-photo.html · platform-cover.html · image-optimize.html</strong></div>
</div>
<div class="layout-card">

View File

@ -467,6 +467,18 @@ Disabled: 底 `--black-alpha-5` + 边 `--black-alpha-12` + 半透明。
- 行间 1 px 分隔(`--border-faint`),最后行去
- Hover: `--black-alpha-4` 底色
#### §4.7.1 表格(`table.t` / 数据表)
**适用:** 项目列表 / 任务中心 / 成员表 / 消费流水等需要横向扫描的数据表。
- 外层只保留 1 px `--border-muted` + 8 px 圆角,形成清楚容器边界
- 表头保留 1 px `--border-muted` 下分割线,表头底色用 `--background-lighter`
- **tbody 行与行之间不画横向分割线**;用 `padding`、留白、hover 底色区分行
- 行 hover 只用 `--black-alpha-4`,不加阴影、不换 hue
- 表格内状态优先用 `.pill-l2` / `.pill`,行末操作用 `.btn-sm` 或 icon button
**禁:** 表格 tbody 每行密集 `border-bottom`、双层外框、白色/裸 hex 边框、用强阴影分隔行。
### §4.8 Tabs(主 / 副)
**主 Tab(下划线激活):**

View File

@ -16,8 +16,13 @@
flex: 1; min-height: 0;
display: grid;
grid-template-columns: 240px 1fr;
transition: grid-template-columns var(--t-base);
}
.io-app.side-collapsed { grid-template-columns: 0 1fr; }
@media (max-width: 1100px) {
.io-app { grid-template-columns: 200px 1fr; }
.io-app.side-collapsed { grid-template-columns: 0 1fr; }
}
@media (max-width: 1100px) { .io-app { grid-template-columns: 200px 1fr; } }
/* ========== 左 · 会话栏 ========== */
.io-side {
@ -25,6 +30,12 @@
background: var(--surface);
display: flex; flex-direction: column;
min-height: 0; overflow: hidden;
transition: opacity var(--t-base), transform var(--t-base);
}
.io-app.side-collapsed .io-side {
opacity: 0;
transform: translateX(-8px);
pointer-events: none;
}
.io-side-h {
display: flex; align-items: center; gap: 8px;
@ -147,6 +158,74 @@
background: var(--surface);
}
.io-toolbar .spacer { flex: 1; }
.io-toolbar .side-restore-btn {
height: 32px;
padding: 0 10px;
display: inline-flex;
align-items: center;
gap: 6px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
color: var(--black-alpha-72);
font-family: inherit;
font-size: 12.5px;
cursor: pointer;
}
.io-toolbar .side-restore-btn:hover { border-color: var(--heat-20); color: var(--heat); }
.io-toolbar .side-restore-btn[hidden] { display: none; }
.io-toolbar .side-restore-btn svg { width: 14px; height: 14px; }
.io-toolbar-search {
position: relative;
width: min(320px, 32vw);
min-width: 220px;
}
.io-toolbar-search[hidden] { display: none; }
.io-toolbar-search svg {
position: absolute;
left: 10px;
top: 50%;
transform: translateY(-50%);
width: 13px;
height: 13px;
color: var(--black-alpha-48);
pointer-events: none;
}
.io-toolbar-search input {
width: 100%;
height: 32px;
padding: 0 32px 0 30px;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
color: var(--accent-black);
font-family: inherit;
font-size: 12.5px;
outline: none;
}
.io-toolbar-search input:focus { border-color: var(--heat-40); background: var(--surface); }
.io-toolbar-search .clear-search {
position: absolute;
right: 4px;
top: 50%;
transform: translateY(-50%);
width: 24px;
height: 24px;
border: 0;
border-radius: var(--r-sm);
background: transparent;
color: var(--black-alpha-48);
cursor: pointer;
display: grid;
place-items: center;
}
.io-toolbar-search .clear-search:hover { background: var(--black-alpha-4); color: var(--accent-black); }
.io-toolbar-search .clear-search svg {
position: static;
transform: none;
width: 12px;
height: 12px;
}
.io-toolbar .search-btn {
width: 32px; height: 32px;
background: var(--surface);
@ -157,7 +236,9 @@
display: grid; place-items: center;
}
.io-toolbar .search-btn:hover { border-color: var(--heat-20); color: var(--heat); }
.io-toolbar .search-btn.active { background: var(--heat-12); border-color: var(--heat-20); color: var(--heat); }
.io-toolbar .search-btn svg { width: 14px; height: 14px; }
.io-tool-filter { position: relative; display: inline-flex; }
.io-toolbar .tb-chip {
display: inline-flex; align-items: center; gap: 6px;
height: 32px; padding: 0 10px;
@ -169,7 +250,48 @@
transition: border-color var(--t-base), color var(--t-base);
}
.io-toolbar .tb-chip:hover { border-color: var(--heat-20); color: var(--heat); }
.io-toolbar .tb-chip.active { background: var(--heat-12); border-color: var(--heat-20); color: var(--heat); }
.io-toolbar .tb-chip svg { width: 10px; height: 10px; opacity: .6; }
.io-tool-filter.open .tb-chip svg { transform: rotate(180deg); }
.io-tool-menu {
position: absolute;
top: calc(100% + 4px);
right: 0;
min-width: 156px;
display: none;
padding: 4px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
box-shadow: var(--shadow-floating);
z-index: 30;
}
.io-tool-filter.open .io-tool-menu { display: block; }
.io-tool-menu .mi {
width: 100%;
min-height: 32px;
display: flex;
align-items: center;
gap: 8px;
padding: 0 10px;
border: 0;
border-radius: var(--r-sm);
background: transparent;
color: var(--accent-black);
font-family: inherit;
font-size: 12.5px;
text-align: left;
cursor: pointer;
}
.io-tool-menu .mi:hover { background: var(--black-alpha-4); }
.io-tool-menu .mi.selected { background: var(--heat-12); color: var(--heat); font-weight: 500; }
.io-tool-menu .mi svg {
width: 12px;
height: 12px;
opacity: 0;
color: var(--heat);
}
.io-tool-menu .mi.selected svg { opacity: 1; }
/* 对话流主体 */
.io-stream {
@ -711,22 +833,33 @@
<section class="io-main">
<div class="io-toolbar">
<button class="side-restore-btn" type="button" id="io-side-restore" hidden title="展开会话栏">
<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="18" height="18" rx="2"/><path d="M9 3v18"/></svg>
会话
</button>
<span class="spacer"></span>
<button class="search-btn" type="button" title="搜索">
<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>
</button>
<button class="tb-chip" type="button">
时间
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg>
</button>
<button class="tb-chip" type="button">
生成模式
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg>
</button>
<button class="tb-chip" type="button">
操作类型
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg>
</button>
<div class="io-toolbar-search" id="io-toolbar-search" hidden>
<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="io-toolbar-search-input" type="text" placeholder="搜索当前对话结果">
<button class="clear-search" type="button" id="io-toolbar-search-clear" title="清空搜索">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg>
</button>
</div>
<div class="io-tool-filter" data-filter="time">
<button class="tb-chip" type="button"><span data-filter-label>时间</span><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg></button>
<div class="io-tool-menu" data-filter-menu></div>
</div>
<div class="io-tool-filter" data-filter="mode">
<button class="tb-chip" type="button"><span data-filter-label>生成模式</span><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg></button>
<div class="io-tool-menu" data-filter-menu></div>
</div>
<div class="io-tool-filter" data-filter="action">
<button class="tb-chip" type="button"><span data-filter-label>操作类型</span><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg></button>
<div class="io-tool-menu" data-filter-menu></div>
</div>
</div>
<div class="io-stream" id="io-stream">
@ -893,6 +1026,13 @@ Shell.render({
const convList = $('#io-conv-list');
const newConvBtn = $('#io-new-conv');
const jumpBtn = $('#io-jump-bottom');
const ioApp = $('.io-app');
const foldBtn = $('.io-side-h .fold');
const sideRestore = $('#io-side-restore');
const toolbarSearchBtn = $('.io-toolbar .search-btn');
const toolbarSearch = $('#io-toolbar-search');
const toolbarSearchInput = $('#io-toolbar-search-input');
const toolbarSearchClear = $('#io-toolbar-search-clear');
function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c])); }
function uid() { return 'm-' + Date.now().toString(36) + Math.random().toString(36).slice(2, 5); }
@ -913,6 +1053,12 @@ Shell.render({
messages: [], // 当前激活会话的对话流(从 convMessages 镜像出来)
convMessages: { default: [] }, // 所有会话的 messages 按 convId 持久化
refImages: [],
toolbar: {
q: '',
time: 'all',
mode: 'all',
action: 'all',
},
param: {
model: 'studio-v2',
ratio: '1:1',
@ -921,6 +1067,27 @@ Shell.render({
},
};
const TOOL_FILTERS = {
time: [
{ id: 'all', label: '时间' },
{ id: 'today', label: '今天' },
{ id: 'week', label: '近 7 天' },
],
mode: [
{ id: 'all', label: '生成模式' },
{ id: 'studio-v2', label: 'Airshelf v2' },
{ id: 'studio-v2-pro', label: 'v2 Pro' },
{ id: 'realistic', label: '写实增强' },
{ id: 'anime', label: '国风动漫' },
],
action: [
{ id: 'all', label: '操作类型' },
{ id: 'ok', label: '已完成' },
{ id: 'gen', label: '生成中' },
{ id: 'err', label: '失败' },
],
};
function slimMessages(msgs) {
// dataUrl 体积大,持久化时剥离;只保留元数据
return (msgs || []).map(m => ({
@ -1074,6 +1241,29 @@ Shell.render({
}
/* ---------- 渲染:中央对话流 ---------- */
function getVisibleMessages() {
const t = state.toolbar;
const q = (t.q || '').trim().toLowerCase();
const now = Date.now();
return state.messages.filter(m => {
if (q) {
const hay = [
m.prompt,
m.ratio,
modelLabel(m.model),
styleLabel(m.style),
...(m.results || []).map(r => r.label || r.status),
].join(' ').toLowerCase();
if (!hay.includes(q)) return false;
}
if (t.time === 'today' && now - (m.createdAt || now) > 86400000) return false;
if (t.time === 'week' && now - (m.createdAt || now) > 86400000 * 7) return false;
if (t.mode !== 'all' && m.model !== t.mode) return false;
if (t.action !== 'all' && !(m.results || []).some(r => r.status === t.action)) return false;
return true;
});
}
function renderStream() {
if (!state.messages.length) {
streamInner.innerHTML = `
@ -1099,7 +1289,27 @@ Shell.render({
});
return;
}
streamInner.innerHTML = state.messages.map(messageHTML).join('');
const visible = getVisibleMessages();
if (!visible.length) {
streamInner.innerHTML = `
<div class="io-empty">
<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="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
</div>
<div class="badge">// NO MATCH</div>
<h2>没有符合筛选的结果</h2>
<p>当前对话里没有匹配的批次,可以清空搜索或切回全部筛选。</p>
<div class="examples"><button class="ex" type="button" id="io-clear-toolbar-filters">清空筛选</button></div>
</div>`;
$('#io-clear-toolbar-filters')?.addEventListener('click', () => {
state.toolbar = { q: '', time: 'all', mode: 'all', action: 'all' };
if (toolbarSearchInput) toolbarSearchInput.value = '';
syncToolbarFilters();
renderStream();
});
return;
}
streamInner.innerHTML = visible.map(messageHTML).join('');
bindMessageEvents();
}
@ -1608,6 +1818,9 @@ Shell.render({
if (!e.target.closest('.io-input-bottom .param')) {
document.querySelectorAll('.io-input-bottom .param.open').forEach(x => x.classList.remove('open'));
}
if (!e.target.closest('.io-tool-filter')) {
document.querySelectorAll('.io-tool-filter.open').forEach(x => x.classList.remove('open'));
}
if (!e.target.closest('.cell-more-wrap')) {
document.querySelectorAll('.cell-more-wrap.open').forEach(x => x.classList.remove('open'));
}
@ -1616,6 +1829,73 @@ Shell.render({
}
});
/* ---------- 顶部工具栏:折叠 / 搜索 / 筛选 ---------- */
function setSideCollapsed(collapsed) {
ioApp?.classList.toggle('side-collapsed', collapsed);
if (sideRestore) sideRestore.hidden = !collapsed;
try { localStorage.setItem('fs-io-side-collapsed', collapsed ? '1' : '0'); } catch (e) {}
}
foldBtn?.addEventListener('click', () => setSideCollapsed(true));
sideRestore?.addEventListener('click', () => setSideCollapsed(false));
toolbarSearchBtn?.addEventListener('click', () => {
const open = toolbarSearch?.hidden;
if (toolbarSearch) toolbarSearch.hidden = !open;
toolbarSearchBtn.classList.toggle('active', !!open || !!state.toolbar.q);
if (open) requestAnimationFrame(() => toolbarSearchInput?.focus());
});
toolbarSearchInput?.addEventListener('input', e => {
state.toolbar.q = e.target.value.trim();
toolbarSearchBtn?.classList.toggle('active', !!state.toolbar.q || !toolbarSearch?.hidden);
renderStream();
});
toolbarSearchClear?.addEventListener('click', () => {
state.toolbar.q = '';
if (toolbarSearchInput) toolbarSearchInput.value = '';
toolbarSearchBtn?.classList.toggle('active', !toolbarSearch?.hidden);
renderStream();
toolbarSearchInput?.focus();
});
function syncToolbarFilters() {
document.querySelectorAll('.io-tool-filter').forEach(wrap => {
const key = wrap.dataset.filter;
const value = state.toolbar[key] || 'all';
const items = TOOL_FILTERS[key] || [];
const selected = items.find(x => x.id === value) || items[0];
wrap.querySelector('[data-filter-label]').textContent = selected?.label || '';
wrap.querySelector('.tb-chip')?.classList.toggle('active', value !== 'all');
const menu = wrap.querySelector('[data-filter-menu]');
if (!menu) return;
menu.innerHTML = items.map(it => `
<button class="mi${it.id === value ? ' selected' : ''}" type="button" data-val="${esc(it.id)}">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg>
<span>${esc(it.label)}</span>
</button>
`).join('');
});
}
document.querySelectorAll('.io-tool-filter').forEach(wrap => {
const btn = wrap.querySelector('.tb-chip');
const menu = wrap.querySelector('[data-filter-menu]');
btn?.addEventListener('click', e => {
e.stopPropagation();
const open = wrap.classList.contains('open');
document.querySelectorAll('.io-tool-filter.open').forEach(x => x.classList.remove('open'));
if (!open) wrap.classList.add('open');
});
menu?.addEventListener('click', e => {
const item = e.target.closest('.mi');
if (!item) return;
e.stopPropagation();
state.toolbar[wrap.dataset.filter] = item.dataset.val;
wrap.classList.remove('open');
syncToolbarFilters();
renderStream();
});
});
/* ---------- 新对话 ---------- */
newConvBtn.addEventListener('click', () => {
// 若 default 上有内容,把它归档到「最近」(新 id + 转存 messages),然后清空 default
@ -1671,6 +1951,8 @@ Shell.render({
} catch (e) {}
buildParamMenus();
syncParamLabels();
syncToolbarFilters();
try { setSideCollapsed(localStorage.getItem('fs-io-side-collapsed') === '1'); } catch (e) {}
updateCost();
renderRefs();
syncSendDisabled();

View File

@ -2370,6 +2370,17 @@ document.querySelectorAll('.asset-card').forEach(card => {
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="1.5" stroke-linecap="round"><path d="M8 3v10M3 8h10"/></svg></button>';
const tagAddBtn = tagsEl.querySelector('.ad-tag-add');
tagAddBtn?.addEventListener('click', e => {
e.stopPropagation();
const existing = [...tagsEl.querySelectorAll('.ad-tag-chip')].map(el => el.textContent);
const next = ['精选素材', '常用资产', '待复用', '已核准'].find(t => !existing.includes(t)) || '新标签';
const chip = document.createElement('span');
chip.className = 'ad-tag-chip';
chip.textContent = next;
tagsEl.insertBefore(chip, tagAddBtn);
Shell.toast('已添加标签', next);
});
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');
@ -2426,6 +2437,13 @@ document.querySelectorAll('.asset-card').forEach(card => {
close(true);
});
bg.querySelectorAll('.ad-icon-btn, .ad-stat-btn').forEach(btn => {
btn.addEventListener('click', e => {
e.stopPropagation();
Shell.toast('已加入下载', (_curName || '资产') + ' · 原图 / 三视图');
});
});
// 弹窗按钮 · 主按钮「保存并退出」/ 次按钮「退出」
confirmSaveBtn.addEventListener('click', () => {
if (_versions.length) _doSave();

File diff suppressed because it is too large Load Diff

View File

@ -15,9 +15,12 @@
flex: 1; min-height: 0;
display: grid;
grid-template-columns: 260px 1fr;
transition: grid-template-columns var(--t-base);
}
.mp-layout.side-collapsed { grid-template-columns: 0 1fr; }
@media (max-width: 1280px) {
.mp-layout { grid-template-columns: 240px 1fr; }
.mp-layout.side-collapsed { grid-template-columns: 0 1fr; }
}
/* ─── 主区: flat 双区 (头部 + body) · 无 card · 用 border 分隔 ─── */
@ -55,6 +58,23 @@
color: var(--black-alpha-48);
}
.mp-main-h .spacer { flex: 1; }
.mp-main-h .side-restore-btn {
height: 32px;
padding: 0 10px;
display: inline-flex;
align-items: center;
gap: 6px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
color: var(--black-alpha-72);
font-family: inherit;
font-size: 12.5px;
cursor: pointer;
}
.mp-main-h .side-restore-btn:hover { border-color: var(--heat-20); color: var(--heat); }
.mp-main-h .side-restore-btn[hidden] { display: none; }
.mp-main-h .side-restore-btn svg { width: 14px; height: 14px; }
.mp-main-h .search-btn {
width: 32px; height: 32px;
background: var(--surface);
@ -177,6 +197,12 @@
display: flex; flex-direction: column;
min-height: 0;
overflow: hidden;
transition: opacity var(--t-base), transform var(--t-base);
}
.mp-layout.side-collapsed .mp-prod-space {
opacity: 0;
transform: translateX(-8px);
pointer-events: none;
}
/* 顶部工具栏: 返回 + 折叠 (跟 image-optimize 视觉一致) */
.mp-side-top {
@ -2224,6 +2250,10 @@
<!-- 主区顶部 · toolbar (商品标题 + 搜索 + 筛选 · 跟图片创作一致) -->
<div class="mp-main-h">
<button class="side-restore-btn" type="button" id="mp-side-restore" hidden title="展开商品空间">
<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="18" height="18" rx="2"/><path d="M9 3v18"/></svg>
商品
</button>
<div class="cur-title">
<span class="crumb">// 商品空间</span>
<span class="nm placeholder" id="cur-prod-nm">未选择 · 请在左侧商品空间选一个</span>
@ -4737,6 +4767,22 @@ document.getElementById('pv-swap').addEventListener('click', () => {
tasks = load();
})();
/* ---------- 商品空间折叠 / 展开 ---------- */
(function () {
const layout = document.querySelector('.mp-layout');
const foldBtn = document.querySelector('.mp-side-top .fold');
const restoreBtn = document.getElementById('mp-side-restore');
function setSideCollapsed(collapsed) {
layout?.classList.toggle('side-collapsed', collapsed);
if (restoreBtn) restoreBtn.hidden = !collapsed;
if (foldBtn) foldBtn.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
try { localStorage.setItem('fs-mp-side-collapsed', collapsed ? '1' : '0'); } catch (e) {}
}
foldBtn?.addEventListener('click', () => setSideCollapsed(true));
restoreBtn?.addEventListener('click', () => setSideCollapsed(false));
try { setSideCollapsed(localStorage.getItem('fs-mp-side-collapsed') === '1'); } catch (e) {}
})();
// 初始化
renderProdSpace();
renderSelectedProds();

View File

@ -14,9 +14,12 @@
flex: 1; min-height: 0;
display: grid;
grid-template-columns: 260px 1fr;
transition: grid-template-columns var(--t-base);
}
.pc-layout.side-collapsed { grid-template-columns: 0 1fr; }
@media (max-width: 1280px) {
.pc-layout { grid-template-columns: 240px 1fr; }
.pc-layout.side-collapsed { grid-template-columns: 0 1fr; }
}
/* ─── 主区: flat 双区 · 用 border 分隔 ─── */
@ -53,6 +56,23 @@
color: var(--black-alpha-48);
}
.pc-main-h .spacer { flex: 1; }
.pc-main-h .side-restore-btn {
height: 32px;
padding: 0 10px;
display: inline-flex;
align-items: center;
gap: 6px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
color: var(--black-alpha-72);
font-family: inherit;
font-size: 12.5px;
cursor: pointer;
}
.pc-main-h .side-restore-btn:hover { border-color: var(--heat-20); color: var(--heat); }
.pc-main-h .side-restore-btn[hidden] { display: none; }
.pc-main-h .side-restore-btn svg { width: 14px; height: 14px; }
.pc-main-h .search-btn {
width: 32px; height: 32px;
background: var(--surface);
@ -170,6 +190,12 @@
display: flex; flex-direction: column;
min-height: 0;
overflow: hidden;
transition: opacity var(--t-base), transform var(--t-base);
}
.pc-layout.side-collapsed .pc-prod-space {
opacity: 0;
transform: translateX(-8px);
pointer-events: none;
}
/* 顶部工具栏: 返回 + 折叠 (跟 image-optimize 视觉一致) */
.pc-side-top {
@ -1159,6 +1185,10 @@
<!-- 主区顶部 · toolbar (商品标题 + 搜索 + 筛选 · 跟图片创作一致) -->
<div class="pc-main-h">
<button class="side-restore-btn" type="button" id="pc-side-restore" hidden title="展开商品空间">
<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="18" height="18" rx="2"/><path d="M9 3v18"/></svg>
商品
</button>
<div class="cur-title">
<span class="crumb">// 商品空间</span>
<span class="nm placeholder" id="cur-prod-nm">未选择 · 请在左侧商品空间选一个</span>
@ -2297,6 +2327,22 @@ document.addEventListener('click', e => {
tasks = load();
})();
/* ---------- 商品空间折叠 / 展开 ---------- */
(function () {
const layout = document.querySelector('.pc-layout');
const foldBtn = document.querySelector('.pc-side-top .fold');
const restoreBtn = document.getElementById('pc-side-restore');
function setSideCollapsed(collapsed) {
layout?.classList.toggle('side-collapsed', collapsed);
if (restoreBtn) restoreBtn.hidden = !collapsed;
if (foldBtn) foldBtn.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
try { localStorage.setItem('fs-pc-side-collapsed', collapsed ? '1' : '0'); } catch (e) {}
}
foldBtn?.addEventListener('click', () => setSideCollapsed(true));
restoreBtn?.addEventListener('click', () => setSideCollapsed(false));
try { setSideCollapsed(localStorage.getItem('fs-pc-side-collapsed') === '1'); } catch (e) {}
})();
// 初始
renderProdSpace();
renderSelectedProds();

View File

@ -24,7 +24,7 @@
/* ============================================================
新建商品 · 重定向 stub
----------------------------------------------------------
旧版本是一个 3883 行的全屏 drawer 页(legacy 备份在 product-create.legacy.html)。
旧版本是一个 3883 行的全屏 drawer 页(legacy 已归档到 _archive/deprecated-pages-20260528/)。
现在「新建商品」改为 drawer 形态在原页面弹出,直接访问此 URL 没有意义。
行为:

View File

@ -147,7 +147,6 @@
<div class="tab" data-filter="wip">进行中 <span class="count">0</span></div>
<div class="tab" data-filter="done">已完成 <span class="count">0</span></div>
<div class="tab" data-filter="fail">失败 <span class="count">0</span></div>
<div class="tab" data-filter="archived">已归档 <span class="count">0</span></div>
</div>
<div class="toolbar">
@ -216,7 +215,7 @@
<span class="muted-2 mono" style="font-size:11px;">3/5</span>
</div>
</td>
<td><span class="pill info"><span class="dot"></span>故事板 待确认</span></td>
<td><span class="pill info"><span class="dot"></span>故事板生成中</span></td>
<td class="muted-2">12 分钟前</td>
<td>
<div class="row-action">
@ -339,25 +338,6 @@
<td class="muted-2">5 月 4 日</td>
<td></td>
</tr>
<tr data-status="archived" data-name="补水面膜 痛点种草 v1" onclick="location.href='pipeline.html#stage-5'">
<td>
<div class="proj-name-cell">
<div class="placeholder proj-thumb"><span class="ph-frame">9:16</span></div>
<div><div class="proj-name">补水面膜 · 痛点种草 · v1</div><div class="proj-sub">6 镜 · 0-15s</div></div>
</div>
</td>
<td>透真补水面膜</td>
<td><span class="muted">AI 全生</span></td>
<td>
<div class="hstack">
<div class="prog"><span class="done"></span><span class="done"></span><span class="done"></span><span class="done"></span><span class="done"></span></div>
<span class="muted-2 mono" style="font-size:11px;">5/5</span>
</div>
</td>
<td><span class="pill neutral"><span class="dot"></span>已归档</span></td>
<td class="muted-2">4 月 28 日</td>
<td></td>
</tr>
</tbody>
</table>
</div>
@ -379,7 +359,7 @@
<span class="muted-2 mono" style="font-size:10.5px;">3/5</span>
</div>
<div class="card-foot">
<span class="pill info"><span class="dot"></span>故事板 待确认</span>
<span class="pill info"><span class="dot"></span>故事板生成中</span>
<span class="card-time">12 分钟前</span>
</div>
</div>
@ -505,25 +485,6 @@
</div>
</div>
<div class="proj-card" data-status="archived" data-name="补水面膜 痛点种草 v1" onclick="location.href='pipeline.html#stage-5'">
<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-project"><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 card-thumb"><span class="ph-frame">9:16 · 5/5 ✓</span></div>
<div class="card-body">
<div>
<div class="card-name">补水面膜 · 痛点种草 · v1</div>
<div class="card-sub" style="margin-top:4px;">透真补水面膜 · 6 镜</div>
</div>
<div class="hstack">
<div class="prog"><span class="done"></span><span class="done"></span><span class="done"></span><span class="done"></span><span class="done"></span></div>
<span class="muted-2 mono" style="font-size:10.5px;">5/5</span>
</div>
<div class="card-foot">
<span class="pill neutral"><span class="dot"></span>已归档</span>
<span class="card-time">4 月 28 日</span>
</div>
</div>
</div>
</div>
</div>
@ -665,7 +626,7 @@ Shell.render({ active: 'projects', crumbs: [{ label: '工作台', href: 'index.h
// ============== 从 DOM 实算项目计数,同步副标题 + tab 角标 ==============
const allRows = document.querySelectorAll('#list-tbody tr');
const counts = { total: allRows.length, wip: 0, done: 0, fail: 0, archived: 0 };
const counts = { total: allRows.length, wip: 0, done: 0, fail: 0 };
allRows.forEach(r => { const s = r.dataset.status; if (counts[s] !== undefined) counts[s]++; });
document.getElementById('sub-total').textContent = counts.total;
@ -901,13 +862,13 @@ document.getElementById('search-input').addEventListener('input', e => {
applyFilter();
});
// 从 URL ?filter=wip|done|fail|archived|all 接收外部跳转 (例如工作台 stat 卡)
// 从 URL ?filter=wip|done|fail|all 接收外部跳转 (例如工作台 stat 卡)
(function applyUrlFilter() {
try {
const q = new URLSearchParams(location.search);
const f = q.get('filter');
if (!f) return;
const valid = ['all', 'wip', 'done', 'fail', 'archived'];
const valid = ['all', 'wip', 'done', 'fail'];
if (!valid.includes(f)) return;
state.filter = f;
document.querySelectorAll('#status-tabs .tab').forEach(t =>

View File

@ -111,9 +111,9 @@
.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 { width: 100%; border-collapse: separate; border-spacing: 0; background: var(--surface); border: 1px solid var(--border-muted); border-radius: var(--r-md); overflow: hidden; font-size: 12.5px; }
.perm-table th, .perm-table td { padding: 8px 10px; border-bottom: 0; }
.perm-table th { border-bottom: 1px solid var(--border-muted); background: var(--background-lighter); 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; }

View File

@ -0,0 +1,400 @@
# Airshelf 页面流程定稿记录
> 本文件用于记录和用户逐页确认后的产品页面流程。
> PRD 只作为功能边界参考,页面布局与交互以本文件后续定稿为准。
维护日期2026-05-28
---
## 0. 总体原则
- 创作视频是产品主线任务。
- 当前版本单条生成视频的时长上限是 15s。
- AI 图片创作是附加能力,用于辅助商品图、人物、场景等素材生产,不抢视频主线。
- 页面跳转尽量少,能在当前页面完成的操作尽量不跳转。
- 操作尽量简单,第一次使用 AI 产品的用户也应该能理解。
- 5 个 Stage 是步骤引导,不是强制锁定流程。
- 用户可以在脚本、基础资产、故事板、视频、拼接导出之间自由跳转。
- 如果某个 Stage 缺少前置内容,页面给提示和快捷补全入口,不做硬锁死。
- 暂不做复制项目、归档、审核、待审核、待我确认等复杂项目管理概念。
---
## 1. 视频项目页 `/projects`
### 页面定位
视频项目页是视频生产任务列表,帮助用户看清每个视频项目做到哪一步,以及下一步能点什么。
### 不做内容
- 不做复制项目。
- 不做归档。
- 不做审核 / 待审核 / 待我确认。
- 不做草稿分类。
- 不做复杂批量操作。
- AI 图片创作不作为顶部主按钮。
### 状态分类
只保留:
- 全部
- 进行中
- 生成中
- 已完成
- 失败
### 页面结构
1. Page Head
- 标题:视频项目
- 主按钮:新建视频项目
2. 状态概览 / Tab
- 全部
- 进行中
- 生成中
- 已完成
- 失败
3. 工具栏
- 搜索项目 / 商品名
- 商品筛选
- 状态筛选
- 最近更新排序
- 可选:列表 / 卡片视图切换
4. 项目列表
- 默认列表行,不默认大卡片。
- 每行展示:
- 9:16 缩略图
- 项目名
- 关联商品
- 当前阶段
- 5 段进度:脚本 / 资产 / 故事板 / 视频 / 导出
- 更新时间
- 状态
- 主操作:继续 / 查看进度 / 查看成片 / 重试
5. 空状态
- 没有商品:引导新建商品。
- 有商品但没有项目:引导新建视频项目。
---
## 2. 新建视频项目页 `/projects/new`
### 页面定位
新建项目页只负责创建项目壳子,并绑定商品。
脚本怎么生成、脚本风格、任务设定等全部放到 Stage 1 脚本页。
### 交互原则
- 一个页面内完成,不做多步跳转。
- 只问用户:为哪个商品创建视频。
- 创建后直接进入项目流水线的 Stage 1 脚本页。
### 字段
必选:
- 商品
可选:
- 项目名称
- 默认自动生成,例如:商品名 + 短视频 + 日期。
不展示:
- 创作方式
- 脚本风格
- 任务设定
- 商品卖点选择
- 预估消耗
### 卖点选择归属
商品卖点不放在新建项目页。
商品卖点选择放到 Stage 1 脚本页,作为“本条视频重点”配置项,从商品库自动带出。
---
## 3. Stage 1 脚本页
### 页面定位
脚本页是 AI 脚本工作台。
用户通过 AI 输入和启动卡片完成脚本来源选择、卖点选择、风格设定、任务设定,并在中间区域查看完整读秒流分镜脚本。
### 核心原则
- 脚本是用户最关注的内容,必须放在中间最大区域。
- 脚本不按 15s 大段展示。
- 脚本按完整读秒流分镜展示,适合短视频细看。
- AI 聊天不是主内容,而是脚本修改和生成记录。
- AI 输入框放页面底部固定,作为当前 Stage 的全局指令入口。
### 页面布局
顶部:
- 项目面包屑
- 商品名 / 项目名
- 5 Stage 自由切换导航:脚本 / 基础资产 / 故事板 / 视频 / 拼接导出
主体:
- 左侧窄栏:商品信息 / 卖点 / 人物 / 场景等辅助信息
- 中间主栏:读秒流分镜脚本
- 右侧窄栏AI 对话记录 / 操作历史
底部固定栏:
- AI 输入框
- 页面操作按钮,例如重新生成全部、进入下一步
### 空状态引导
脚本来源不做 Tab。
脚本未生成时,中间主区域展示启动卡片:
- AI 帮我写
根据商品信息自动生成完整读秒流分镜。
- 我有脚本
粘贴已有口播稿AI 帮你整理成分镜结构。
- 一句话生成
输入一句视频方向AI 生成完整分镜。
- 复刻爆款
暂未开放或后续版本处理。
用户选择后进入对应模式。生成脚本后,中间区域切换为完整读秒流分镜。
### 读秒流分镜结构
每个分镜以卡片 / 行卡展示:
- 镜头编号
- 时间段,例如 0-5s、5-10s
- 画面描述
- 对白 / 旁白
- 可选:镜头类型、场景、人物
用户可以:
- 直接编辑某个分镜文字。
- 用底部 AI 输入框修改某个分镜。
- 重新生成全部。
- 调整整体时长、口吻、卖点重点等。
### 商品卖点
- 从商品库自动带出。
- 在脚本页展示为“本条视频重点”。
- 默认选中前 2-3 个,或默认使用全部卖点。
- 用户可点选 / 取消。
- 用户也可以直接在 AI 输入框里说重点突出哪些卖点。
---
## 4. Stage 2 基础资产页
### 页面定位
基础资产页用于为当前视频准备视觉资产。
它不是资产库,也不是完整 AI 图片创作页。
### 总体原则
- 默认 AI 自动生成,用户只在效果不好时调整。
- 脚本确定后AI 自动识别需要的商品、人物、场景,并生成图片提示词。
- 页面优先让用户看默认生成效果。
- 不满意时再修改提示词、上传参考图、从资产库选择或重新生成。
- 商品是最重要的,优先展示商品资产。
### 页面布局
- 左侧:资产清单
- 中间:当前资产工作台 / 生成结果
- 右侧AI 操作记录 / 生成记录
- 底部AI 输入框
### 资产顺序
1. 商品资产
2. 人物
3. 场景
### 商品资产
商品三视图是一张 16:9 图,包含正面、侧面、背面。
它不是三张图,也不拆分成三个槽位。
不提供:
- 上传三视图
- 使用现有商品图
原因:
- 用户一般不会有适合生成视频的白底三视图。
- 如果不生成三视图,系统本来就只能默认使用商品图继续,不需要多给一个“使用现有商品图”的操作。
推荐交互:
- 展示当前商品图。
- 展示商品三视图状态:未生成 / 生成中 / 已生成。
- 说明:生成 16:9 商品三视图可以提升商品角度稳定性。
- 主操作:生成商品三视图。
- 不生成三视图也可以继续,只做弱提示。
生成规则:
- 商品三视图属于结构转换型。
- 默认一次生成 1 张。
- 不满意可重新生成或用 AI 输入框调整提示词。
- 商品三视图支持保留历史版本,用户可以在历史版本中选择满意的一版采用。
### 人物资产
命名为“人物”,不叫“模特”。
人物指脚本中需要出现的角色,用户要确定这个角色的形象。
默认流程:
1. AI 根据脚本自动识别人物。
2. AI 为每个人物自动生成图片提示词。
3. 默认生成 4 张人物立绘候选。
4. 用户选择 1 张满意的立绘。
5. AI 基于选中的立绘生成 1 张 16:9 人物三视图。
6. 用户采用此人物资产。
不满意时,用户可以:
- 修改描述。
- 上传自己的模特 / 参考图。
- 从人物库选择。
- 重新生成立绘。
- 从历史立绘里再选一张生成三视图。
- 重新生成人物三视图。
- 从人物三视图历史版本中选择满意的一版采用。
人物调整不放抽屉。
点击人物后,中间区域直接切换为人物工作台。
人物工作台需要一步到位承载:
- 人物库选择
- 上传参考图
- 描述生成
- 直接使用自己的模特
- AI 生成多张立绘
- 从立绘生成三视图
- 从历史立绘再选再生成三视图
### 场景资产
默认流程:
1. AI 根据脚本识别场景。
2. AI 自动生成场景图提示词。
3. 默认一次生成 4 张候选。
4. 用户选择 1 张。
不满意时,用户可以:
- 修改描述。
- 重新生成。
- 上传替换。
### 候选生成规则
创意选择型:一次生成 4 张。
- 人物立绘
- 场景图
结构转换型:一次生成 1 张。
- 商品三视图
- 人物三视图
三视图版本规则:
- 商品三视图和人物三视图都可以重跑。
- 每次重跑产生一个历史版本。
- 用户可以在历史版本中回选满意的一版。
- 采用某一版后,该版本成为当前项目使用版本。
### 资产复用
人物默认是项目内资产,不自动进入团队人物库,避免资产库变乱。
采用人物时可以勾选:
- 保存到人物库
保存后:
- 进入团队共享人物库。
- 后续项目可复用。
- 记录来源:项目内生成 / AI 生成 / 上传参考图等。
---
## 5. 待继续确认页面
- Stage 4 视频页
- Stage 5 拼接导出页
- 商品库 / 商品详情
- 资产库
- AI 图片创作
- 人物库
- 工作台
- 团队
- 消费
- 设置
---
## 5. Stage 3 故事板页
### 页面定位
故事板页用于在生成视频前,让用户清晰看到接下来这条 15s 视频的大致内容和画面走向。
### 核心原则
- 不做分镜级故事板。
- 不按每个镜头单独生成故事板。
- 故事板按照“一条视频”的完整时长来展示。
- 当前版本单条视频上限是 15s因此故事板就是这条 15s 视频的整体视觉预览。
- 故事板需要让用户一眼明白:等下这条视频会讲什么、画面如何推进、商品如何出现。
### 与脚本页的关系
- Stage 1 脚本页仍然可以按读秒流分镜展示,方便用户细看脚本。
- Stage 3 故事板页不继承“每个分镜一张图”的结构。
- 故事板应总结整条 15s 视频,而不是把脚本分镜逐张拆开。
### 初步页面方向
主体重点应放在一张完整故事板 / 故事板预览上。
页面需要展示:
- 当前视频脚本摘要
- 使用的商品资产、人物资产、场景资产
- 故事板整体预览
- 生成状态
- 重新生成 / 调整提示词 / 采用当前版本
- 历史版本回选
故事板可重跑。
每次重跑保留历史版本,用户可以在历史版本中选择满意的一版。