Files
dsh_shenxian/dsh-server-docs/06-工作台UI规范.md
T

331 lines
20 KiB
Markdown
Raw Normal View History

# 短视频工作台 UI 规范
> **【本工作区约定 · 2026-09-10 入库 · 适用范围 2026-09-10 明确】**
> **地位**:**关键文档 / 强制基线**——**本工作区内我开发的一切前端页面必须遵循**本文档的 Token、组件与交互约定;冲突时以本文档为准。
> **适用范围(用户明确)**:**除「dsh 自带功能」与「dsh 自带插件页面」外,其余所有功能插件开发(含其 client 面 section)、门户插件、门户页面(desktop / admin / skills / plugins 等)一律参考本规范 + 使用 UI skill。** 即:dsh 官方 UI 与官方插件渲染的部分不在约束内(不改它们),我自己写的页面/插件全部在约束内。
> **权威源**:`E:\ProgramData\AI技能\mcn-short-video\project\短视频脚本创作\V1.0\mcn-work-shop\docs\工作台UI规范.md`(源侧更新后回拷本文件)
> ⚠️ **该路径 2026-09-12 实测已不存在**(glob 无结果;`E:\ProgramData\AI技能\mcn-short-video\` 目录仍在,其下已无 `工作台UI规范.md`)→ **本副本当前即唯一有效版本**。源侧若恢复,需重新建立回拷关系并补同步。
> **UI skill(按场景选对,用错场景等于没用)**:开发/改前端页面时**必须加载并使用对应技能**做视觉判断 ——
> - **`impeccable`** → **本工作区绝大多数页面**(工具类后台、设置面板、管理页、表单、空态)。**用 Operate 模式**。它的描述明确覆盖 settings / empty states / a11y / 信息层级 / 动效,与本规范同向。
> ⚠️ 它自带的机械检测器**未随包提供**(`scripts/detect.mjs` 会报 `bundled detector not found`)→ 改为**人工逐条过 `reference/craft-floor.md`** 的 Verify 清单与 Refuse 禁令(2026-09-12 实测的确抓出了真问题,见下)。
> - **`taste-skill`**(frontmatter name 为 `design-taste-frontend`)→ **仅用于落地页 / 作品集 / 整站重设计**。
> ⚠️ 它**自述「Not dashboards, not data tables, not multi-step product UI」** → **本平台的管理页 / 设置面板不在其射程内**,不要拿它做这类页面。
> - **冲突时以本文档的实测 Token 数值为准**(本文档是实测基线,技能是通用审美)。
> - 位置:`E:\ProgramData\.workbuddy\skills\<name>\`(2026-09-12 起两个技能已装入全局技能目录,并已过安全审计)。Skill 工具加载不到时**直接读文件**即可。
> **改文档规则**:UI 决策若伴随"为什么这样改",回写源侧文件并回拷,保持两端一致。
> **同步时间戳与回拷流程(档案 19 §D8)**:本文档是**回拷副本**,权威源见上文"权威源"路径。
> - 最近同步:**2026-09-10**(入库)→ 之后如有改动请在此追加一行日期;
> - **2026-09-12**:修订「UI skill」条款 —— 原表述要求"必须同时使用 impeccable + taste-skill",但 taste-skill 自述**不覆盖** dashboards / data tables / 多步产品 UI,与本工作区页面类型不符 → 改为**按场景分流**(impeccable 管本工作区绝大多数页面;taste-skill 仅限落地页 / 作品集 / 重设计)。
> - ⚠️ **本次未回写源侧**:权威源路径(`E:\ProgramData\AI技能\mcn-short-video\...\工作台UI规范.md`)在本机**不存在**(2026-09-12 实测 glob 无结果)→ 修订**只落在本副本**;源侧恢复后需补回拷。
> - 回拷流程:① 改源侧 → ② 回拷到本文件 → ③ 在上一行追加同步日期 → ④ 走文档库对账脚本确认双端一致
> (`bash scripts/docs-sync-check.sh`,0 = 全绿)。
> **定位**:本文件是前端 UI 的**设计规范基线**(配色 / 字体 / 圆角 / 布局 / 组件 / 交互),供新增页面、调整样式时对照,避免重复踩坑。
---
## 1. 设计语言
亮色主题、工具类后台风格。三条主线:
1. **信息密度优先**——表格/列表是主体,行高紧凑(表格 td `7px 10px`,字号 15px)。
2. **克制配色**——蓝(`#2f6fed`)作主色承担交互反馈,橙(`#e8590c`)只用于强调数值/警示,其余靠灰度拉开层次。
3. **轻量质感**——圆角 8/12px、阴影 `0 1px 3px rgba(0,0,0,.08)`,不用重投影和渐变。
---
## 2. 设计 Token
### 2.1 色彩
| 变量 | 值 | 用途 |
|---|---|---|
| `--bg` | `#f5f6f8` | 页面底色 |
| `--panel` | `#ffffff` | 卡片/表格/弹窗面板 |
| `--border` | `#e3e6ea` | 统一描边 |
| `--text` | `#24292f` | 主文本 |
| `--dim` | `#6b7280` | 次要文本、说明、占位 |
| `--primary` | `#2f6fed` | 主色(按钮/激活态/链接 hover) |
| `--primary-soft` | `#eaf1fe` | 主色浅底(hover 行、激活 tab 底) |
| `--accent` | `#e8590c` | 强调(榜单得分、警示) |
| `--danger` | `#d93026` | 危险操作(删除) |
| `--shadow` | `0 1px 3px rgba(0,0,0,.08)` | 卡片/顶栏阴影 |
**扩展色(未进变量,直接写在规则里)**
| 场景 | 值 |
|---|---|
| 表头底色 | `#f6f8fa` |
| 链接蓝 | `#4a90e2` |
| 主色 hover 深一档 | `#2a5fd0` |
| 图标垂直微调 | `vertical-align: -0.18em` |
| 文件类型色 | md `#8250df` / json `#cf222e` / xlsx `#1a7f37` / txt·csv `#57606a` |
| 半透明主色底 | `rgba(47,111,237,.07)`(首页统计条)、`.16`(分隔线) |
**功能图标亮色映射**(未列出的跟随 currentColor):
`trending-up` 橙 · `users` 蓝 · `pen` 粉 `#ec4899` · `flask` 青 `#0ea5e9` · `lightbulb` 黄 `#d99a2b` · `refresh` 青绿 `#0f7f8f` · `chart` 玫红 `#d6336c` · `wrench` 灰 `#5b6470`
### 2.2 字体
```css
font-family: "Microsoft YaHei", "PingFang SC", "Segoe UI", sans-serif;
font-size: 16px; /* body 基准 */
```
代码/JSON 预览:`Consolas, monospace`。
**字号阶梯**(实测归纳,不是硬 token):
| 级别 | 字号 | 用例 |
|---|---|---|
| 特大 | 26px / 21px | 首页统计数字、面包屑主标题 |
| 标题 | 18px / 17px | 弹窗标题、区块标题、卡片标题 |
| 正文 | 16px / 15px | 正文、按钮、表格、输入框 |
| 次要 | 14px / 13px | 说明文字、数值列、徽章、tab(小) |
### 2.3 圆角 / 阴影 / 层级
| 类型 | 值 | 用例 |
|---|---|---|
| 圆角 | 6px | 小控件(btn-sm、侧栏切换组、树节点) |
| | **8px** | 默认(按钮、输入框、表格容器、信息卡、评分块) |
| | 10px | 徽章、榜单容器、卡片 |
| | **12px** | 大卡片(首页 data-card、func-card、统计条) |
| | 14px | 弹窗 |
| 阴影 | `var(--shadow)` | 卡片、顶栏 |
| | `0 8px 30px rgba(0,0,0,.18)` | 弹窗 |
| | `0 4px 12px rgba(47,111,237,.15)` | 卡片 hover(主色扩散) |
| z-index | 10 顶栏 / 100 弹窗遮罩 / 200 Toast | 表头 sticky 为 1 |
### 2.4 间距
没有 token 化,实际使用序列:**2 / 4 / 6 / 8 / 10 / 12 / 14 / 16 / 18 / 20 / 24px**。常用组合:
- 卡片内边距 `16px 18px`;信息卡 `14px 16px`;表格单元 `7px 10px`;表头 `8px 10px`
- 页面外边距 `20px 24px`;首页/详情内容 `24px`;区块间距 14~24px
- 工具条内部 gap 8~10px;表单行 gap 10px
---
## 3. 布局框架
```
┌─ header.topbar(55px,sticky,白底 + 底部 1px 边框 + shadow,z-index 10)
│ brand(logo) · crumbs(面包屑) · top-actions(首页/帮助/设置,右侧)
├─ main#view height: calc(100vh - 55px) overflow: auto ← 唯一滚动容器
│ .home(max-width 960) / .page(max-width 1440)
└─ 弹窗层 .modal-mask(fixed inset 0,z-index 100)
```
**滚动(坑点,勿改)**
| 规则 | 原因 |
|---|---|
| `html, body { overflow: hidden }` | 消除顶栏 1px 边框引发的第二层滚动条 |
| `main#view { height: calc(100vh - 55px) }` | **55px 是实测值**(padding 20 + 行高 34 + 边框 1)。写成 54px 底部会溢出 1px,触发第二层滚动条 |
| 长内容滚动交给内部容器(`.rw-sec-body`、`.rw-content`、`.file-tree`) | 保持页面级不滚 |
**内容宽度**:首页 `.home` max-width **960px**;功能页 `.page` max-width **1440px**(padding 20 24);弹窗 `.rw-modal` 宽 `min(1600px, 100% - 100px)`、高 `calc(100vh - 60px)`。
---
## 4. 组件规范
### 4.1 按钮
| 类 | 外观 | 用例 |
|---|---|---|
| `.btn` | 白底 + border + `6px 14px` + r8 + 15px;hover 边框/文字变 primary;`:disabled` opacity .5 | 默认 |
| `.btn-primary` | primary 底白字;hover `#2a5fd0` | 主行动 |
| `.btn-ghost` | 透明底 + border | 次要/弹窗内 |
| `.btn-danger` | danger 系 | 删除 |
| `.btn-sm` | `3px 10px` / 14px / r6 | 表格功能列 |
| `.btn-icon` | `5px 9px`,纯图标 | 图标按钮 |
| `.btn-busy` | primary-soft 底 + primary 字 + opacity .8,内含图标旋转动画 | 异步进行中 |
| `.btn-import` | 白底 + `#d0d4da` 边 + `#495057` 字 | **保存类**(浅灰底深字) |
| `.btn-view` | 白底 + primary 边 + primary 字 | **查看类** |
> 功能列成对出现时:**保存=浅灰底深字(.btn-import),查看=白底蓝字(.btn-view)**,靠底色深浅区分而非都用蓝。
顶栏三按钮用 `.ghost`(白底 + border,`6px 14px`,r8)。
### 4.2 输入框
`.input`(`6px 10px`,min-width 200px)· `.input-select`(`6px 8px`,筛选下拉更窄)。均 1px border + r8 + 15px + 白底。
### 4.3 表格
```css
.table-wrap overflow auto + 1px border + r8 + 白底
.table width 100%, border-collapse collapse, 15px
.table th sticky top 0, 背景 #f6f8fa, 8px 10px, 600, nowrap, z-index 1
.table td 7px 10px, nowrap, 底部 1px border
tbody tr:hover 背景 var(--primary-soft)
```
| 修饰类 | 作用 |
|---|---|
| `.num`(td/th) | **右对齐** + `font-size: 13px` + `font-variant-numeric: tabular-nums`(等宽数字,必须加,否则数字跳动) |
| `.th-wide` | 数值列表头 min-width 90px + 右对齐 |
| `.sortable` | cursor pointer,hover 变 primary |
| `.cell-content` | max-width 220px + 省略号 |
| `.cell-loc` | 账号定位列缩窄 110px |
| `.th-act` / `.cell-act` | 功能列居中 |
| `.title-plain` | 视频标题:默认同正文色,**hover 才变蓝+下划线**(避免整列花掉) |
| `.link` / `.link.strong` | `#4a90e2`;`.strong` 加粗 |
**列宽策略(关键)**
| 场景 | 做法 |
|---|---|
| 榜单表格 `.table-rank` | **必须 `table-layout: fixed`**——auto 布局下 100% 表格的富余空间会按比例摊薄,**min-width 压不住**;fixed 下 width 才是硬约束,账号列自动吃剩余宽度 |
| 普通数据表格 | auto 布局,靠 `.cell-content` max-width + 省略号收口 |
**行高对齐(关键)**:榜单三个 tab 的行高必须统一,否则切换时整表跳动——
- 表格 `.table-rank td { height: 42px }`(功能列 `.btn-sm` 撑到 42px,其他 tab 原本只有 34.7px)
- 首页预览 `.rank-preview-list .rank-row:not(.rank-head) { min-height: 40px }`
### 4.4 Tab(两套并存,注意覆盖)
| 位置 | 样式 |
|---|---|
| 顶栏 | 胶囊式:r8,hover 浅蓝底蓝字,active **蓝底白字 + 600** |
| 数据页(**重新定义同名 `.tabs/.tab`**) | 下划线式:无背景无圆角,`border-bottom: 2px transparent` + `margin-bottom: -1px`(压住容器分隔线),active **蓝字 + 蓝色下划线** |
> ⚠️ 同名类二次定义,**数据页规则在后会覆盖顶栏规则**。新增页面若在下划线式区域用 `.tab`,拿到的是下划线式;需要胶囊式请另起类名。
榜单 tab `.rank-tab`:r8 8px 0 0(上圆角),active 浅蓝底 + 蓝字 + 底部 2px 蓝线。
首页预览里需 `align-self: center`(全局 `.rank-tabs` 用 stretch,会让 tab 组相对兄弟控件偏上约 5px)。
### 4.5 卡片
| 类 | 规格 |
|---|---|
| `.data-card`(首页入口) | r12,`16px 18px`,shadow,hover **上浮** `translateY(-2px)` + 蓝边 + `0 4px 12px rgba(47,111,237,.15)`,transition .15s |
| `.info-card`(信息区) | r8,`14px 16px`,margin-bottom 14px;标题 15px/600 + 底部 1px 分隔线 |
| `.recent-card`(脚本快捷卡) | r10,`14px 16px`,min-height **172px**(保证网格等高),hover 蓝边 + 淡影 |
| `.func-card` | r12,`16px 18px` |
### 4.6 徽章 / 标签
`.badge`:`2px 8px`,r10,13px。
- `.badge-signed` 浅蓝底蓝字(签约/内部)
- `.badge-ext` `#fff4e5` 底 + accent 橙字
- `.rw-ver-new` primary 底白字 r9 12px(最新版本标记,附在版本条最右)
- `.recent-card-score` 12px/600 r10
### 4.7 弹窗
```css
.modal-mask fixed inset 0, rgba(0,0,0,.35), flex 居中, z-index 100
.modal 白底 r14, 22px 26px, width 520px, max-width 92vw, shadow 0 8px 30px
.req-modal 760px × min(840px, 92vh) /* AI创作 */
.rw-modal min(1600px, 100% - 100px) × calc(100vh - 60px) /* 脚本对比/查看 */
```
> ⚠️ `.modal-mask[hidden] { display: none }` **必须显式写**——`.modal-mask` 是 `display:flex`,会覆盖 HTML `hidden` 属性导致弹窗关不掉。
**对比弹窗三段式(`.rw-*`)**:`.rw-stack`(flex row, gap 12)→ `.rw-sec`(flex 1, min-width 0, r8, overflow hidden)→ 内分 `.rw-sec-head`(8px 12px,白底,底部边框)/ `.rw-sec-body`(12px 14px,overflow auto)/ `.rw-sec-foot`(**min-height 46px**,右对齐,顶部边框)。
> `.rw-sec-foot` 的 min-height 是为了**空 foot(源脚本列占位)与其他列等高**,不能删。
### 4.8 Toast
固定在底部居中(`bottom: 32px`,`translateX(-50%)`),深底 `#24292f` 白字,`10px 18px` r8,z-index 200,**显示 1.8 秒后自动消失**。用于复制成功、保存结果等轻量反馈;失败重试类提示走行内 `.toolbar-note` / `.empty`。
### 4.9 空状态
`.empty-state`:纵向居中,`padding: 56px 20px`,gap 10px——图标(dim + opacity .45)→ 标题(17px/600)→ 说明(14px/dim)→ 行动按钮(margin-top 10px)。
轻量占位可用 `.empty`(`padding: 30px`,居中,dim)。
### 4.10 分页 / 批量
`.pager` 右对齐(gap 12,`padding: 12px 4px`);`.list-footer` 用 space-between 把批量操作条放左、分页放右;批量条 `.list-batchbar` 出现时与分页同行。
### 4.11 评分条
`.score-row`:名称 `.score-name` 固定 **88px** → 进度条 `.score-bar`(flex 1,height 6px,r3,底 `#eef1f5`)→ 分值 `.score-val` 固定 **36px** 右对齐。三档宽度固定,保证多行对齐。
### 4.12 图标
```js
svgIcon(name, size = 16, color) ;生成 class="ic" 的 SVG
```
- Lucide 风格:`viewBox 0 0 24 24`,`stroke-width: 2`,`fill: none`,`stroke: currentColor`
- 静态 HTML 用 `<i data-icon="home">` 占位,`initIcons()` 统一替换
- `.ic { vertical-align: -0.18em; flex-shrink: 0 }` ——**垂直微调 -0.18em,去掉会和文字基线错开**
---
## 5. 关键交互约定
| 约定 | 说明 |
|---|---|
| **数值一律右对齐 + 等宽数字** | `text-align: right` + `font-variant-numeric: tabular-nums`,列表头同步右对齐 |
| **字段对齐用 grid 列轨道共享** | 账号信息/视频详情的多行字段,用同一个 grid 让列跨行对齐;每行独立 flex 会因内容长短把后续字段顶偏(R44/R45 教训) |
| **超宽字段移出共享网格** | 如「账号定位」整行留在网格内会把 4 条轨道撑成等宽(各 232.5px),右侧大片留白 |
| **滚动联动单向** | AI 脚本列滚动驱动分镜列对齐,**不做双向耦合**(双向会互相抖动) |
| **长文本截断三件套** | `overflow: hidden` + `text-overflow: ellipsis` + `white-space: nowrap`,并挂 `title` 显示全文 |
| **hover 才提示可点** | 列表中的标题类文本用 `.title-plain`(默认同正文色,hover 变蓝+下划线),避免整列都是蓝字 |
---
## 6. 已知坑与规避
| # | 坑 | 规避 |
|---|---|---|
| 1 | `main#view` 写成 `calc(100vh - 54px)` → 底部溢出 1px,出现第二层滚动条 | 用 **55px**(padding 20 + 行高 34 + 边框 1) |
| 2 | 榜单表格用 auto 布局 → 列宽被富余空间摊薄,`min-width` 失效 | 加 `.table-rank { table-layout: fixed }` + 显式列宽 |
| 3 | 榜单三 tab 行高不一致 → 切换时整表跳动 | 表格 td 固定 42px;首页预览行 min-height 40px |
| 4 | `.modal-mask` 的 `display:flex` 覆盖 `hidden` → 弹窗关不掉 | 显式写 `.modal-mask[hidden] { display: none }` |
| 5 | 数值列没加 `tabular-nums` → 滚动/翻页时数字左右跳 | 数值列统一加 |
| 6 | 图标与文字基线错开 | `.ic` 的 `vertical-align: -0.18em` 不能删 |
| 7 | 首页 `.rank-tabs` 相对兄弟控件偏上 ~5px | 预览场景加 `align-self: center; margin-bottom: 0` |
---
## 7. UI 改动的生效链路与验收(2026-09-12 实战确立,**所有 UI 改动必走**)
> **起因**:v0.2.6 把「功能管理」列表改卡片后,**磁盘、服务端、实例三处都验过是新的,用户却一直看到旧 UI** —— 排查耗了 40+ 步。
> 根因不是"改错地方",而是**链路上有两道时序/缓存**:client bundle 只在**实例启动时**加载;而浏览器按 URL 里的 `rev` 决定要不要重新拉。
> ⇒ 为让**所有** UI 改动能被稳定验收,确立以下机制。
### 7.1 生效链路(改完到"用户看见",要过四关)
| 关 | 环节 | 关键条件 |
|---|---|---|
| ① | 改源码 → `npm pack` → 打包 | **必须升版本号** —— 同版本改内容会让 pnpm 判定"无需更新",白改 |
| ② | 铺到用户 profile(`ensure-biz-plugins.cjs` 等) | 改完立刻生效(**磁盘层**) |
| ③ | **实例加载 bundle** | **必须重启实例** —— client bundle 在**实例启动时**加载;不重启则实例内存里仍是旧代码 |
| ④ | 浏览器取到新代码 | 由 URL 里的 `rev`(**内容 hash**)决定;平台已加 `no-cache` 兜底 |
### 7.2 平台侧机制(2026-09-12 落地,**勿删**)
- **`Cache-Control: no-cache`**:平台代理层对实例的 `/plugins/` 响应统一加此头 —— **允许缓存但每次回源校验**,bundle 一变浏览器就能拿到新的。
- 实现位置:`src/supervisor/proxy.ts`,搜索 `平台缓存治理`
- ⚠️ `/assets/` 另有 nginx `location ~* ^/assets/` 的 `expires 30d` 会覆盖此头 —— 但 `/assets/` 是 dsh 构建产物(**文件名带 hash**),30d 合理,**无需处理**
- **`rev` 由 dsh 官方按内容计算**(`dsh-client-modules`:`framedHash("combo", [sourceBytes, sourceMap])`),平台**不得改**(红线 R2);且 dsh **会拒绝 `rev` 不匹配的请求**(官方注释:*mismatched revisions are rejected instead of serving newer bytes*)—— 这恰好成了"实例是否已换新"的**判据**。
### 7.3 验收口径(三段式,缺一不可)
| 查什么 | 怎么查 | 期望 |
|---|---|---|
| **磁盘** | `grep -c <新代码特征> <profile>/node_modules/<pkg>/lib/*.js` | ≥ 1 |
| **实例已加载** | 抓实例页 HTML,比对 URL 里的 **`rev`** | **与改前不同**(变了 = 实例加载的是新内容) |
| **服务端返回** | 用 HTML 里那条**完整** bundle URL 请求 | `HTTP 200` + 含新代码特征 |
### 7.4 四个常见误判(都是本次踩过的)
1. **拿单包 URL 请求** → 404(`/plugins/??` 只认**完整包列表**,不是单包)
2. **去服务器上 `/opt/dshs/poc/**` 的源码副本里 grep** → 那是**源码**、不是实例读取路径(实例读 `profile/node_modules`)→ 会误判成"根本没改"
3. **只看"实例是否重启过"** → 还要看 HTML 里 `rev` 是否真的变了
4. **取实例页 HTML 漏参数** → 必须 `curl -L --compressed -c jar -b jar -H "Accept: text/html"`(缺一即被 gzip 拦截,拿到空内容)
5. **client bundle 的 `inject` 写成数组** → **必须是函数**(原包写法 `inject: () => ({})`,返回「要注入的服务映射」对象)。写成数组 `["@deepseek-ai/dsh-client-runtime", …]` 会被 dsh 理解为「**在等这些服务**」⇒ 条目**永久 pending**:
```
web boot: 1 entry did not activate
<pkg>: pending (waiting for services: @deepseek-ai/dsh-client-runtime, …)
```
⚠️ **这个错只在浏览器里暴露**——服务端日志显示 `[xxx] loaded` 一切正常,`inject` 类型错误**完全看不出来**(2026-09-12 实测)。
6. **整合包的 client bundle 必须注册「包名自己」** → 拼接多个原包时,它们各自 `load({id: "<原包名>"})`,**包名本身不会自动注册**;dsh 按包名找 ⇒ 报 `loaded without registering "<包名>" via __ModuleLoader__.load`。
正确做法:子包的 `load` 改为「收集」,再由本包 `load({id: 包名, factory})` 统一注册,**并在 factory 内驱动子包 factory**(否则子包 UI 永不注册)。