# 短视频工作台 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\\`(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 用 `` 占位,`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 <新代码特征> /node_modules//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 : 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 永不注册)。