Skip to content

Design System

KS-OTO edited this page Sep 21, 2026 · 1 revision

设计系统

权威规格在仓库里的 docs/design-baseline.md —— 那份文件与代码同步演进,本页只是导读。

设计系统的存在理由是:弹窗的不一致只是模型的投影。模型不统一,弹窗怎么调都会再漂。

结构

节 内容
0 硬约束:TDesign 原生,token 只用 --td-*
0.1 品牌主题层 src/assets/theme.css(实测取值来源、刻意偏离、对比度基准)
1 语义聚类(W / B / P)
2 卡型矩阵
3 屏幕策略
4 单位规范
5 精度规范
6 AccountDetail:所有平台同构的账号详情

0. 硬约束:TDesign 原生

不迁移组件库、不模仿参考站、不新增 --ui-* 平行 token 层。 自定义语义值只写在 src/assets/layout.css 的 :root 并注明理由。

0.1 品牌主题层:src/assets/theme.css

全站唯一的配色入口。亮 / 暗两套品牌主题通过覆盖 TDesign 的设计变量(--td-*)实现, 组件侧零硬编码颜色 —— 换配色只动那一个文件。

几条最值钱的经验:

  • 语义色必须钉在「消费级」上,不是随手挑一级。TDesign 的 --td-success-color 这类 token 是别名,指向色阶里的某一级(success → -5、error → -6、brand → 亮 -7 / 暗 -8)。 .t-progress__inner 的填充读的就是别名 —— 把亮绿钉在 -8 而不是 -5, 进度条实际拿到的是一个发闷的中绿,而当时所有门禁都是绿的。
  • 「填充档」和「文字档」是两个 token,不能混用。--td-brand-color 是填充档 (按钮底、进度条、开关),给的是「上面压白字的色块」。拿它当文字色, 亮色下侥幸可读,暗色下掉到 2.15:1。文字一律走 --td-text-color-*。
  • 同一个 token 同时当填充与文字时算术上无解:白字压在填充上需要亮度 ≤0.183, 它当字压在卡片面上需要 ≥0.231。本项目的解法是暗色品牌色换成另一个蓝(#4e7ef9), 这是全站唯一一处亮暗不同色,理由写在 theme.css 文件头。
  • --td-text-color-anti 必须保持白色 —— 它不只用于彩色填充上的字, 还被 tooltip 的文本读(tooltip 底色是 --td-gray-color-13,暗色下 #242424)。
  • 对比度阈值要拿库自己的官方基线当基准:TDesign 官方在浅色 -1 背景上的 success / warning 文字只有 2.86 / 2.82:1。要求自己 ≥4.5 会造出一整套不达标的假失败; 正确的判据是「不比自己替换掉的那套更差」。

4. 单位规范

数值          单位
─────────    ────────
110.00       ¥        ← 货币:卡面一律用**符号**(¥ / $)
8.42         万 token  ← 计数:数值 + 中文量纲
56.70        (无)    ← 币种未知:只显示数值,不猜
  • 币种代码 → 符号的映射只维护一份:src/format.ts 的 currencySymbol()。
  • 单位渲染成独立元素(不是 value 字符串的一部分),数值仍是纯数字,可复制、可断言。
  • 禁止在标签里重复币种:CNY 总余额 → 总余额。
  • 币种代码(CNY / USD)只保留在详情弹窗的「币种」字段。

5. 精度规范

全站数值只有三种语义,精度由语义决定,不由调用点决定。实现与推导见 src/format.ts:

类别(Metric.kind) 精度 舍入 适用
money 2 位 + 千分位 截断向零 一切货币读数
tokens 量级缩写(K 档 1 位 / M·B 档 2 位;不足 1K 退回计数精度) 四舍五入 token / CREDITS 总量
percent 1 位 四舍五入 百分比、比率、折算值
count 0 位 + 千分位 四舍五入 请求次数、天数、个数
text / duration 原样 — 枚举、日期文案、倒计时

三条容易踩的:

  • 金额必须先做浮点补偿:1.15 * 100 === 114.99999999999999,直接 Math.trunc 会得到 1.14(系统性少 1 分钱,实测 13 个样本里 7 个踩坑)。truncateMoney 内部已处理。
  • 「原样输出」是精度漏洞,任何一档都不能例外。formatTokens 不足 1K 的那一档曾写 String(value),于是 quota - used 算出的 206.34719999999652 原样印到卡面上。 同一类还有 -0(Math.round(-0.04) 是 -0)与非有限数(NaN 会印成 "NaN")。 判据:每个分支都必须落到某个精度上,没有分支允许原样返回输入数字。
  • <t-statistic> 只许出现在 components/ui/MetricTile.vue 且必须显式声明精度。 它不传 decimal-places 时不是取整,而是原样输出 0–20 位小数 —— 所以金额交给它之前必须已经截断。

6. AccountDetail:所有平台同构的账号详情

状态:已落地。 类型在 src/types.ts,构件库在 src/detail.ts, 10 个适配器与 10 个 Section 都已按这套模型改造完。

概念 在哪 规则
模型 src/types.ts 字段恒存在的扁平对象;失败取中性空值
构件库 src/detail.ts field / metric / table / notice / link / windowQuota / cardsOf
适配器 src/components/*Detail.ts 平台原始响应 → AccountDetail,平台差异只在这里
骨架组件 src/components/ui/ 卡面与弹窗各只此一套
cardFace 开关 适配器 「卡面 vs 弹窗」的唯一开关,别在组件里挑字段

cardFace 现有两种用法:同一账号的另一个订阅、只对部分模型生效的限额 (火山日额度就是后者 —— 它只对图片/视频/语音与 Harness 生效,因此卡面只留 5 小时/周/月)。

OverviewTab 不在该模型的范围内(它读原始切片),不要顺手改它。

布局与列策略

  • 布局只用 src/assets/layout.css 的语义化网格原语;禁 t-row / t-col (TDesign 栅格 12 列);断点只在 layout.css 声明。
  • 账号列表一律 .grid-cards grid-cards--wide。
  • 列策略由个数声明、App.vue 统一施加(组件不判断): shouldSpanFullRow(≥2 整行)、isCompactAccounts(≥5 单列)、metricGridClass(读数 2/3/auto)、 windowContainerClass(窗口 ≥3 等分)。改谓词要同时改 App.vue 绑定与 layout.css —— utils.test.ts 有接线守卫。
  • 每一层卡片都铺满上一层给它的高度,所以区块卡 → 账号卡 → 窗口块三层严格等高。
  • 弹窗居中必须 placement="center"(TDesign 默认 top = 视口 20vh 顶距)。

守卫测试

规格不是靠自觉守住的。src/ 下几组守卫测试盯着最容易静默失效的那几条:

文件 盯什么
theme.test.ts 正文档位过 AA、反白字下限、进度条填充亮度、圆角阶梯、字面颜色不得外流
format.test.ts t-statistic 必须声明精度、渲染层禁 toFixed / Math.round / toLocaleString
tdesign.test.ts 组件只从 src/tdesign.ts 的注册表来,禁 app.use(TDesign) 整库导入
utils.test.ts 列策略谓词 ↔ App.vue 绑定 ↔ layout.css 三处接线一致

改配色、改精度、改列策略前先跑 bun run test:unit。

Clone this wiki locally