用与小程端相同的收敛手法,把 admin-web(Vue3 + Element Plus)的样式统一到一套 token 上:颜色、字号、字重、触摸目标、对比度,并补上 Element Plus 派生色阶联动。
范围:24 个 .vue 中的 style 块 + src/theme/index.css(重写为底座)+ 新增 src/theme/tokens.ts(TS 侧色值镜像)。另含复验中咬出并修复的 1 个既有功能性缺陷(见第六节)。
| 类别 | 改造前(实测) | 改造后 | 证据来源 |
|---|---|---|---|
| Element Plus 派生色阶残留 | light-3/5/7/8/9、dark-2 及 success/warning/danger/info 的 light-N 全是 EP 默认值;数据统计页实测 133 处元素仍在使用 EP 默认调色板(其中主色蓝 8 处、success 14、danger 10、warning 8、info 92) | 全部档位按品牌基调重算;4 个页面实测 0 处 | 渲染态 CDP 实测 |
| 硬编码色值 | 97 处 / 67 种色值 | 0 | 守卫 |
| 硬编码字号 | 88 处;其中 20 处 ≤11px(最低 9px,工作台实测 9px×27 / 10px×42) | 0 处越界;实测最低 11px | 守卫 + 渲染态 |
| 非法字重 | 6 处 650(缺字重平台会回退,渲染不一致) | 0;仅用 400/500/600/700 | 守卫 + 渲染态 |
| 对比度不达标 | text-tertiary #888 3.54:1、warning #E59A32 2.33:1、coral #F45F68 作文字 3.14:1 | 全部 ≥4.5:1(11 组实算判定) | 守卫(WCAG 实算) |
| 触摸目标偏小 | 17 处 <44px | 全部 ≥44px | 守卫 |
| 依赖 UA 默认字号 | 浏览器对 <small> 默认 font-size: smaller(0.833em),父级 11px→实际 9.17px、12px→10px | 统一继承父级字号 + 关键处显式声明,实测 ≥11px | 本轮渲染态新发现 |
Element Plus 的 --el-color-*-light-N 是 SCSS 编译期静态生成的:只改 --el-color-primary 不会带动它们,残留下来的就是 EP 默认蓝。
| token | 改造前 | 改造后 | 说明 |
|---|---|---|---|
--el-color-primary | #f45f68(只覆盖了基色) | #C84A53 | 实心底 + 白字 4.60:1 |
--el-color-primary-light-3 | #79bbff ← EP 默认蓝 | #d98087 | 按 mix(#fff,base,30%) 重算 |
--el-color-primary-light-5 | #a0cfff ← EP 默认蓝 | #e4a5a9 | mix(#fff,base,50%) |
--el-color-primary-light-7 | #c6e2ff ← EP 默认蓝 | #efc9cb | mix(#fff,base,70%) |
--el-color-primary-light-8 | #d9ecff ← EP 默认蓝 | #f4dbdd | mix(#fff,base,80%) |
--el-color-primary-light-9 | #ecf5ff ← EP 默认蓝 | #faedee | mix(#fff,base,90%) |
--el-color-primary-dark-2 | #337ecc ← EP 默认蓝 | #a03b42 | mix(#000,base,20%) |
--el-color-info | #909399 | #656C79 | info 语义是「中性/未知」,保持灰调 |
--color-text-tertiary | #888888(3.54:1) | #676E7B | 白底 5.13:1 / 浅底 4.74:1 |
--admin-coral-strong | (未定义) | #C0414A | 深一档,专供白底文字 5.13:1 |
--admin-coral-solid | (未定义) | #C84A53 | 实心底 + 白字 4.60:1 |
--tap-min | (未定义) | 44px | 触摸目标下限 |
--font-size-base | (未定义) | 14px | 正文基准 |
| 维度 | 改造前 | 改造后 |
|---|---|---|
| 字号分布 | 9px×27 · 10px×42 · 11px×35 · 12px×22 · 13px×9 · 14px×123 · 15px×3 · 16px×29 · 17px×73 · 20px×6 · 21px×6 · 22px×1 · 27px×1 · 30px×13 · 31px×3 | 11px×90 · 12px×37 · 13px×9 · 14px×123 · 15px×3 · 16px×102 · 20px×13 · 24px×1 · 28px×16 |
| 字重分布 | 400 / 500 / 600 / 650×6 / 700 | 仅 400 / 500 / 600 / 700 |
| 层级倒挂 | 指标数字 30px > 页面标题 24px(数字比标题更抢眼) | 标题 24px / 指标 24px / 区块 20px,层级归位 |
字号刻度收敛为:micro 11 / xs 12 / sm 13 / base 14 / md 15 / lg 16 / xl 18 / 2xl 20 / 3xl 24 / 4xl 28 / 5xl 32 / display 38。
系统自带 Edge(Chromium 内核)无头截图,视口 1440×900、缩放 1x,页面数据来自本地 mock。 已目视核对工作台与数据统计的前后对照;登录页与申诉管理页未逐张细看,请人工过目。 数值层面另以「计算样式实测 + 守卫断言」为准。
提示:本节截图的「改造后」列取自已修复漏斗缺陷的产物, 故数据统计页的漏斗卡显示的是真实数字;修复前后的专项对照见第六节。
| 项目 | 结果 | 命令 / 方式 |
|---|---|---|
| UI 契约门禁(全量) | 20 / 20 PASS,0 失败,10.4s | node scripts/run-ui-contracts.mjs |
| 新增守卫 admin-design-tokens | PASS:未定义 token 0 / 硬编码色值 0 / 越界字号 0 / 非法字重 0 / 对比度 11 组实算 / tokens.ts↔index.css 20 项一致 | node ui-tests/admin-design-tokens.mjs |
| 新增守卫 admin-reactivity-contracts | PASS:扫描 24 个 .vue、23 个 v-for 绑定点 → 非响应式状态 0、颜色助手遮蔽 0(静态常量清单 3 处正确豁免) | node ui-tests/admin-reactivity-contracts.mjs |
| 守卫判别力(负向注入 · 设计守卫) | 抽掉 .scene-footer small 的 font-size → 守卫 FAIL(exit 1),3b 计数 1 | 注入 → 复测 → 还原 |
| 守卫判别力(负向注入 · 响应式守卫) | 3 例注入全部咬住(exit 1):①普通数组+直接改元素+v-for ②汇总表改硬编码数字(同源契约)③颜色助手被同名局部变量遮蔽;还原后复跑 exit 0 | node .workbuddy/neg-inject-admin.mjs |
| 漏斗缺陷修复(行为验证) | 同一 mock、同一路由,只差代码:修复前漏斗卡/安全卡 全 0、比率全 - → 修复后 12846 / 9217 / 8462 / 3812 / 48210 / 51236 / 7420 / 4380 / 1926 / 1284,安全卡 17 / 214 / 23 / 9 | Edge 无头 + CDP 读渲染后 DOM 文本 |
| 类型检查 | exit 0,无错误 | vue-tsc --noEmit |
| 生产构建 | 成功,17.74s | vite build |
| 渲染态实测(CDP) | 登录 / 工作台 / 申诉 / 数据统计 4 页:EP 默认色 0 处、<11px 文本 0 处、字重仅 400/500/600/700 | Edge 无头 + CDP 取计算样式 |
现象:同一张卡片里,上方的漏斗数字全是 0,下方「累计注册 / 实名用户 / 审核通过 / 7日活跃」的汇总表却是真实数字(12846 / 9217 / 8462 / 3812),界面自相矛盾。
根因:这三组曾是普通数组,loadFunnel() 直接改元素属性,而模板读的就是同一个数组 ——
非响应式写入不触发重渲染,Vue 看不到变化:
const funnelStages = [ { value: 0, ... } ] // 普通数组
funnelStages[0].value = userData.totalRegistered // 直接改元素 → 渲染不更新
定位依据(渲染态实测,同一 mock、同一路由,只差代码)
| 取值来源 | 是否响应式 | 修复前渲染 | 修复后渲染 |
|---|---|---|---|
funnelStats.totalRegistered 等(汇总表) | 是(ref) | 正常 12846 / 9217 / 8462 / 3812 | 正常 |
漏斗卡 10 张(funnelStages / matchStages) | 否(普通数组) | 全 0 | 12846 / 9217 / 8462 / 3812 / 48210 / 51236 / 7420 / 4380 / 1926 / 1284 |
安全卡 4 张(safetyCards) | 否(普通数组) | 全 0 | 17 / 214 / 23 / 9 |
比率(funnel-rate) | 否 | 全 - | 71.7% / 65.9% / 29.7% / 106.3% / 15.4% / 9.1% / 2.7% |
已排除环境因素:CDP 抓取页面运行期 控制台错误 0 条、网络失败 0 条, 且 汇总表在同一页渲染出了 12846 ⇒ 数据确实已到达前端,纯粹是响应式问题。 改造前的构建产物表现完全相同 ⇒ 该缺陷先于本次改动存在,不是本次收敛引入的回归。
修法(按根因而非最小改动):三个集合改为 computed() 从既有 ref 派生,
卡片与汇总表因此同源 —— 结构上不可能再出现「同一份数据的两处展示对不上」。
另把 loadFunnel() 里遮蔽颜色助手的局部变量 const token 改名 authToken。
为何前 19 个守卫全绿也没发现它:既有守卫都是源码结构断言,而这一段代码完全合法
—— 无 TS 报错、无 lint 报错、构建通过。只有真跑起来看渲染结果才暴露。
故新增 ui-tests/admin-reactivity-contracts.mjs:
①非响应式状态(普通声明 + 被当状态写 + 出现在模板里 → 失败;未被写过的静态常量清单合法,按「是否被写」区分)
②数据统计页同源契约(卡片与汇总表必须取自同一变量、端点不得被删)
③颜色助手遮蔽。已用负向注入证明判别力:3 例注入全部 FAIL(exit 1),还原后复跑 PASS。
ref 后能否正确传导到 DOM」这一链路(修复前全 0、修复后为真实值,同一 mock 对照)。
真实后端的响应字段名未在本轮重新核对 —— 但这部分本次未改动,修复前后一致。matchesBySource / matchmakerRecommendations 等字段(这几个值同样走 ref,
若后端返回即会渲染)。请勿据此判定为同类问题。/api/v1/* 返回了按前端读取字段构造的 mock 数据,不是真实业务数据;本次只看版式/配色/字号/热区,不验证业务正确性。