管理端:设计系统 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
This commit is contained in:
WorkBuddy
2026-09-23 01:06:57 +08:00
parent 269adf1fed
commit 631e3000d3
20 changed files with 1765 additions and 381 deletions
@@ -60,7 +60,7 @@
<el-descriptions-item label="补充说明" :span="2">{{ detail.report?.description || '无' }}</el-descriptions-item>
</el-descriptions>
<h4 style="margin:20px 0 12px;color:#E96B6B">被举报人风险画像</h4>
<h4 style="margin:20px 0 12px;color:var(--admin-coral-strong)">被举报人风险画像</h4>
<el-descriptions :column="2" border v-if="detail.riskProfile">
<el-descriptions-item label="用户">{{ detail.riskProfile.displayName }}</el-descriptions-item>
<el-descriptions-item label="风险分数">{{ detail.riskProfile.totalRiskScore }}</el-descriptions-item>
@@ -72,7 +72,7 @@
<el-descriptions-item label="历史举报数">{{ detail.riskProfile.totalReportsCount }}</el-descriptions-item>
</el-descriptions>
<h4 style="margin:20px 0 12px;color:#E96B6B">处置操作</h4>
<h4 style="margin:20px 0 12px;color:var(--admin-coral-strong)">处置操作</h4>
<el-form label-width="100px">
<el-form-item label="处理结果">
<el-select v-model="processResult" style="width:100%">
@@ -100,6 +100,7 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { get, post } from '@/api/client'
import { token as tokenColor } from '@/theme/tokens'
const loading = ref(false)
const processing = ref(false)
@@ -137,10 +138,10 @@ async function loadStats() {
const res: any = await get('/operator/risk/reports/stats', {}, { Authorization: `Bearer ${token}` })
const data = res.data || {}
stats.value = [
{ key: 'pending', label: '待处理', value: data.pendingCount || 0, color: '#E96B6B' },
{ key: 'processing', label: '处理中', value: data.processingCount || 0, color: '#E59A32' },
{ key: 'resolved', label: '已处理', value: data.resolvedCount || 0, color: '#67C23A' },
{ key: 'dismissed', label: '已驳回', value: data.dismissedCount || 0, color: '#909399' }
{ key: 'pending', label: '待处理', value: data.pendingCount || 0, color: tokenColor('--color-primary') },
{ key: 'processing', label: '处理中', value: data.processingCount || 0, color: tokenColor('--color-warning') },
{ key: 'resolved', label: '已处理', value: data.resolvedCount || 0, color: tokenColor('--color-success') },
{ key: 'dismissed', label: '已驳回', value: data.dismissedCount || 0, color: tokenColor('--color-neutral') }
]
} catch {}
}
@@ -188,7 +189,7 @@ onMounted(loadData)
<style scoped>
.page-container { padding: 24px; }
.stat-card { text-align: center; }
.stat-value { font-size: 36px; font-weight: bold; }
.stat-label { font-size: 14px; color: #666; margin-top: 8px; }
.stat-value { font-size: var(--font-size-5xl); font-weight: var(--font-weight-bold); }
.stat-label { font-size: var(--font-size-base); color: var(--color-text-tertiary); margin-top: 8px; }
.card-header { display: flex; justify-content: space-between; align-items: center; }
</style>