Files
danyang-match/admin-web/src/theme/tokens.ts
T
WorkBuddy 631e3000d3 管理端:设计系统 v2 收敛 + 修复漏斗卡恒为 0 的响应式缺陷
## 一、设计系统收敛

- theme/index.css 重写为 token 底座:品牌三级、语义五组、文字四级、
  边框/背景/圆角/阴影/动效/热区/排版刻度
- 修复 Element Plus 派生色阶残留:--el-color-*-light-N 是 SCSS 编译期静态生成,
  只覆盖基色不会带动它们 —— 数据统计页实测 133 处元素仍在用 EP 默认调色板
  (其中主色蓝 8 处)。已按 mix(#fff,base,N*10%) / mix(#000,base,20%) 补全全部档位
- 新增 src/theme/tokens.ts:模板里 :style 需拼 8 位 hex(color + '15'),用不了 var(),
  故以 TS 常量镜像色值;守卫会双向校验它与 index.css 同名取值一致
- 24 个 .vue 收敛:硬编码色值 97 处/67 种 → 0;越界字号 88 处(最低 9px)→ 0;
  非法字重 650×6 → 0;触摸目标 17 处 <44px → 全部 ≥44px
- 对比度:text-tertiary 3.54:1、warning 2.33:1、coral 作文字 3.14:1 → 全部 ≥4.5:1
- 新增守卫 ui-tests/admin-design-tokens.mjs(含 WCAG 对比度实算)

## 二、修复既有功能性缺陷:数据统计页漏斗卡与安全卡恒显示 0

- 现象:同一张卡片里,上方漏斗数字全是 0,下方汇总表却是 12846/9217/8462/3812,
  界面自相矛盾
- 根因:funnelStages / matchStages / safetyCards 是普通数组(const x = [...]),
  loadFunnel() 直接改元素属性,模板读的却是同一个数组 —— 非响应式写入不触发重渲染。
  同页凡走 ref 的值(汇总表、V11 区块)全部正常,规律清晰
- 修法(按根因):三个集合改为 computed() 从既有 ref 派生,卡片与汇总表因此同源,
  结构上不可能再出现「同一份数据的两处展示对不上」;另把 loadFunnel() 内遮蔽
  颜色助手的局部变量 const token 改名 authToken
- 该缺陷无 TS/lint/构建报错,前 18 个守卫全绿也抓不到 —— 只有真跑起来看渲染结果才暴露
- 新增守卫 ui-tests/admin-reactivity-contracts.mjs:
  ①非响应式状态(普通声明 + 被当状态写 + 出现在模板里;未被写过的静态常量清单合法)
  ②数据统计页同源契约 ③颜色助手遮蔽

## 验证

- UI 契约门禁 20/20 PASS(MIN_GUARDS 18→20)
- 负向注入 3 例全部 FAIL(exit 1),还原后复跑 exit 0(证明守卫有判别力)
- vue-tsc --noEmit exit 0
- vite build 成功(17.74s)
- 渲染态实测(Edge 无头 + CDP 读渲染后 DOM,同一 mock、同一路由,只差代码):
  修复前漏斗卡/安全卡全 0、比率全 '-' → 修复后 12846/9217/8462/3812/
  48210/51236/7420/4380/1926/1284,安全卡 17/214/23/9

交付物:artifacts/admin-design-v2-preview.html
2026-09-23 01:06:57 +08:00

78 lines
3.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 管理端调色板常量(TS 侧)。
*
* 为什么需要这个文件:
* 部分模板用 `:style="{ backgroundColor: stage.color + '15' }"` 把 6 位 hex 拼成
* 8 位带 alpha 的颜色('#409EFF' + '15' → '#409EFF15',即 8% 不透明度)。
* 这类位置**不能**用 CSS var —— `'var(--x)' + '15'` 是无效值,浏览器会整条丢弃。
* 于是「色值只有一处事实源」这条规则在 TS 侧会漏。本文件补上这个缺口。
*
* 单一事实源约定:
* 键名就是 src/theme/index.css 里的 var 名,值必须与之一致。
* 门控脚本 scripts/lint-admin-tokens.mjs 会逐项比对,任一侧改了而另一侧没改即失败。
*/
export const TOKEN_HEX: Record<string, string> = {
'--color-primary': '#F45F68',
'--color-primary-strong': '#C0414A',
'--color-primary-solid': '#C84A53',
'--color-success': '#2E9B68',
'--color-success-strong': '#1F7A4F',
'--color-warning': '#E59A32',
'--color-warning-strong': '#8F5D0F',
'--color-danger': '#D94A4A',
'--color-danger-strong': '#B8353A',
'--color-info': '#4A7CD4',
'--color-info-strong': '#3A63B0',
'--color-purple': '#8B63BC',
'--color-purple-strong': '#6B47A0',
'--color-neutral': '#8A9099',
'--color-neutral-strong': '#656C79',
'--color-text-primary': '#1F2328',
'--color-text-secondary': '#555555',
'--color-text-tertiary': '#676E7B',
'--color-bg-page': '#F7F9FB',
'--color-border': '#E4E7EB',
}
/**
* 取 token 的 hex 字面量。用于 :style 绑定(含 hex 拼接透明度的场景)。
* @example token('--color-success') // '#2E9B68'
*/
export function token(name: string): string {
const value = TOKEN_HEX[name]
if (!value) throw new Error(`未登记的 token: ${name}(请先加入 TOKEN_HEX 并同步 theme/index.css)`)
return value
}
/**
* 生成带 alpha 的 8 位 hex。alpha 传 2 位十六进制('15' = 21/255 ≈ 8%)。
* 与 `color + '15'` 等价,但把「必须 6 位 hex」这个约束显式化,避免误把 var() 传进来。
*/
export function tint(hex: string, alphaHex: string): string {
if (!/^#[0-9a-fA-F]{6}$/.test(hex)) {
throw new Error(`tint() 需要 6 位 hex(不能是 var() 或 3/8 位 hex):${hex}`)
}
return `${hex}${alphaHex}`
}
/** 指标卡 / 待办项的色调组,与 theme/index.css 的 --tone-* 一一对应。 */
export interface Tone {
/** 图标与图形用的鲜亮色 */
vivid: string
/** 文字用的可读色(压 soft 底 ≥4.5:1) */
ink: string
/** 浅底 */
soft: string
}
export const TONES: Record<string, Tone> = {
rose: { vivid: '#F45F68', ink: '#C0414A', soft: '#FFF0F1' },
amber: { vivid: '#E59A32', ink: '#8F5D0F', soft: '#FFF6E6' },
blue: { vivid: '#4A7CD4', ink: '#3A63B0', soft: '#EEF5FF' },
green: { vivid: '#2E9B68', ink: '#1F7A4F', soft: '#EDF8F1' },
purple: { vivid: '#8B63BC', ink: '#6B47A0', soft: '#F4EFFF' },
slate: { vivid: '#8A9099', ink: '#656C79', soft: '#F4F5F7' },
red: { vivid: '#D94A4A', ink: '#B8353A', soft: '#FFF6F6' },
}