1) dsh-server-docs/ 从工作区(原 E:\...\aliyun-dsh-server\dsh-server-docs)**整体并入本仓**,
保留目录名 ⇒ 仓库内 dsh-server-docs/... 的相对引用天然继续有效;旧目录(含其 .git)已归档到
工作区 _中间产物_待清理/,未随本提交带入。
2) .gitattributes:新增 `dsh-server-docs/** -text` —— 原文档库是 `* -text` + autocrlf=false,
必须保持纯 LF,否则会被本仓的 CRLF 规则翻掉。
3) 活引用里的绝对路径已全部改到新位置(docs 的 INDEX / README / scripts / skills + 用户级 skills
+ ~/.workbuddy/settings.json 的 hooks);历史档案(04-调整方案/、archive/)按「只增不改」未动。
⚠️ hooks 路径改动需「完全重启会话」才生效(配置是会话启动快照)。
4) 交接单/T08:新增 §16「生产整体切换执行记录」(形态 / 落地动作 / **4 个只有真上线才暴露的真 bug** /
验收证据 / 回滚命令 / 残留项);台账 T08 行 → 已完成并归档;03-路线图 §二 登记 T08 收尾项。
5) 统一称谓:**「本机」只指跑 WorkBuddy 的开发机**,47 / 106 一律写「远程服务器」。
20 KiB
短视频工作台 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. 设计语言
亮色主题、工具类后台风格。三条主线:
- 信息密度优先——表格/列表是主体,行高紧凑(表格 td
7px 10px,字号 15px)。 - 克制配色——蓝(
#2f6fed)作主色承担交互反馈,橙(#e8590c)只用于强调数值/警示,其余靠灰度拉开层次。 - 轻量质感——圆角 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 字体
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 表格
.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-newprimary 底白字 r9 12px(最新版本标记,附在版本条最右).recent-card-score12px/600 r10
4.7 弹窗
.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,会覆盖 HTMLhidden属性导致弹窗关不掉。
对比弹窗三段式(.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 图标
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/另有 nginxlocation ~* ^/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 四个常见误判(都是本次踩过的)
- 拿单包 URL 请求 → 404(
/plugins/??只认完整包列表,不是单包) - 去服务器上
/opt/dshs/poc/**的源码副本里 grep → 那是源码、不是实例读取路径(实例读profile/node_modules)→ 会误判成"根本没改" - 只看"实例是否重启过" → 还要看 HTML 里
rev是否真的变了 - 取实例页 HTML 漏参数 → 必须
curl -L --compressed -c jar -b jar -H "Accept: text/html"(缺一即被 gzip 拦截,拿到空内容) - 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 实测)。 - 整合包的 client bundle 必须注册「包名自己」 → 拼接多个原包时,它们各自
load({id: "<原包名>"}),包名本身不会自动注册;dsh 按包名找 ⇒ 报loaded without registering "<包名>" via __ModuleLoader__.load。 正确做法:子包的load改为「收集」,再由本包load({id: 包名, factory})统一注册,并在 factory 内驱动子包 factory(否则子包 UI 永不注册)。