Przeglądaj źródła

fix(admin): P1 返工 热力对比度死区/焦点环/五维智解耦/渐变令牌派生

Agnes Agent 2 dni temu
rodzic
commit
14f734cd7f

+ 5 - 1
cfc-web/src/styles/admin-tokens.css

@@ -21,7 +21,7 @@
 :root {
 :root {
   /* ---- 品牌主色与功能色(主色加深以满足 AA) ---- */
   /* ---- 品牌主色与功能色(主色加深以满足 AA) ---- */
   --color-primary: #0B5FCC;          /* 加深以满足 AA 对比度 */
   --color-primary: #0B5FCC;          /* 加深以满足 AA 对比度 */
-  --color-primary-light: #0958D9;    /* hover:比 default 更深 */
+  --color-primary-light: #0958D9;    /* hover 态(命名沿用 Ant,非亮度指示) */
   --color-primary-dark: #003EB3;     /* active:三态最深 */
   --color-primary-dark: #003EB3;     /* active:三态最深 */
   --color-success: #52C41A;          /* 原 #67C23A */
   --color-success: #52C41A;          /* 原 #67C23A */
   --color-warning: #FAAD14;          /* 原 #E6A23C */
   --color-warning: #FAAD14;          /* 原 #E6A23C */
@@ -138,6 +138,10 @@
 .el-button--primary { background-color: var(--adm-primary); border-color: var(--adm-primary); color: #fff; }
 .el-button--primary { background-color: var(--adm-primary); border-color: var(--adm-primary); color: #fff; }
 .el-button--primary:hover,
 .el-button--primary:hover,
 .el-button--primary:focus { background-color: var(--adm-primary-hover); border-color: var(--adm-primary-hover); color: #fff; }
 .el-button--primary:focus { background-color: var(--adm-primary-hover); border-color: var(--adm-primary-hover); color: #fff; }
+/* hover 与 default 色差极小,补一条非颜色线索;排除 is-disabled 以免改动禁用态表现 */
+.el-button--primary:not(.is-disabled):hover { box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.12); }
+/* 键盘可见焦点单独给 :focus-visible,不挂到 :hover 上 */
+.el-button--primary:focus-visible { outline: 2px solid var(--adm-primary-active); outline-offset: 2px; }
 .el-button--primary:active { background-color: var(--adm-primary-active); border-color: var(--adm-primary-active); color: #fff; }
 .el-button--primary:active { background-color: var(--adm-primary-active); border-color: var(--adm-primary-active); color: #fff; }
 .el-button--primary.is-disabled,
 .el-button--primary.is-disabled,
 .el-button--primary.is-disabled:hover { background-color: #A0C4FF; border-color: #A0C4FF; color: #fff; }
 .el-button--primary.is-disabled:hover { background-color: #A0C4FF; border-color: #A0C4FF; color: #fff; }

+ 27 - 12
cfc-web/src/views/Dashboard.vue

@@ -261,6 +261,7 @@
 <script>
 <script>
 import * as echarts from 'echarts'
 import * as echarts from 'echarts'
 import SectionTitle from '@/components/SectionTitle.vue'
 import SectionTitle from '@/components/SectionTitle.vue'
+import { adminColor } from '@/utils/colorTokens'
 import {
 import {
   getDashboardStats,
   getDashboardStats,
   getMembershipStats,
   getMembershipStats,
@@ -409,6 +410,17 @@ export default {
       const v = getComputedStyle(document.documentElement).getPropertyValue(name).trim()
       const v = getComputedStyle(document.documentElement).getPropertyValue(name).trim()
       return v || ''
       return v || ''
     },
     },
+    // 主色令牌 -> rgba 字符串:canvas / 热力单元格不解析 CSS var,统一在此派生,
+    // 避免主色调整时散落的硬编码 rgba 漂移。令牌值唯一来源为 adminColor('primary')
+    // (内部读 :root 上的 --adm-primary,并带独立于五维品牌色的兜底值)。
+    // 令牌缺失或非法时不伪造品牌色,退到纯黑占位,保证 canvas 不抛错。
+    rgbaFromPrimary(alpha) {
+      const raw = String(adminColor('primary') || '').trim()
+      const m = /^#?([0-9a-f]{6})$/i.exec(raw)
+      const hex = m ? m[1] : '000000'
+      const dec = (i) => parseInt(hex.slice(i, i + 2), 16) || 0
+      return `rgba(${dec(0)}, ${dec(2)}, ${dec(4)}, ${alpha})`
+    },
     // 停留时长 / 热度 相对最大值,用于条形宽度
     // 停留时长 / 热度 相对最大值,用于条形宽度
     sectionList(key) {
     sectionList(key) {
       // v-for 回调内 this 不是组件(Vue2 普通函数 this=undefined),
       // v-for 回调内 this 不是组件(Vue2 普通函数 this=undefined),
@@ -424,21 +436,25 @@ export default {
       if (!max) return 0
       if (!max) return 0
       return Math.max(4, Math.round((Number(value) || 0) / max * 100))
       return Math.max(4, Math.round((Number(value) || 0) / max * 100))
     },
     },
-    // 时段热力单元格颜色:按相对峰值线性加深(浅蓝 → 品牌主色 #0B5FCC,主色加深以满足 AA)
-    // 文字色随背景动态切换,保证对比度:
-    //   alpha < 0.90 → 深字 var(--adm-text-88),实测 ≥ 4.6:1(AA 达标)
-    //   alpha ≥ 0.90 → 白字,实测 3.6~4.1:1(深格粗体数据标签,数据可视化可接受)
-    // 注:任务原建议阈值 0.55 实测白字仅 2.1:1(不达标),故上修至 0.90。
+    // 时段热力单元格颜色:按相对峰值线性加深(浅蓝 -> 品牌主色令牌派生色)
+    // 背景由 rgbaFromPrimary(alpha) 从 --adm-primary 运行时派生,不写死色值。
+    // 有数据格的两支文字必须完全不透明(#000000 / #FFFFFF):主色加深后
+    // alpha=0.83~0.90 区间,半透明文字叠加 opacity 修饰符会让两支同时跌破 4.5:1,
+    // 仅调阈值数学上无解。阈值 0.87 下 0.001 步长全区间实测:深字支最低 4.54:1、
+    // 白字支最低 4.62:1(脚本 heat.py / verify.py)。
     heatCellStyle(v) {
     heatCellStyle(v) {
+      // 有数据格的文字层不得带任何 alpha(含 CSS opacity),否则上述阈值失效。
+      const DARK_TEXT = '#000000'
+      const LIGHT_TEXT = '#FFFFFF'
       const peak = this.dwellByHourPeak || 0
       const peak = this.dwellByHourPeak || 0
       if (!peak || !v) return { background: 'var(--adm-fill)', color: 'var(--adm-text-45)' }
       if (!peak || !v) return { background: 'var(--adm-fill)', color: 'var(--adm-text-45)' }
       const ratio = Math.min(1, Number(v) / peak)
       const ratio = Math.min(1, Number(v) / peak)
       const alpha = (0.1 + ratio * 0.9).toFixed(3)
       const alpha = (0.1 + ratio * 0.9).toFixed(3)
-      const bgColor = `rgba(11, 95, 204, ${alpha})`
-      const useLightText = parseFloat(alpha) >= 0.9
+      const bgColor = this.rgbaFromPrimary(alpha)
+      const useLightText = parseFloat(alpha) >= 0.87
       return {
       return {
         background: bgColor,
         background: bgColor,
-        color: useLightText ? 'rgba(255,255,255,0.92)' : 'var(--adm-text-88)'
+        color: useLightText ? LIGHT_TEXT : DARK_TEXT
       }
       }
     },
     },
     fmtDuration(sec) {
     fmtDuration(sec) {
@@ -597,8 +613,8 @@ export default {
             itemStyle: { color: this.cssVar('--adm-primary') },
             itemStyle: { color: this.cssVar('--adm-primary') },
             areaStyle: {
             areaStyle: {
               color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
               color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
-                { offset: 0, color: 'rgba(11,95,204,0.18)' },
-                { offset: 1, color: 'rgba(11,95,204,0.01)' }
+                { offset: 0, color: this.rgbaFromPrimary(0.18) },
+                { offset: 1, color: this.rgbaFromPrimary(0.01) }
               ])
               ])
             }
             }
           },
           },
@@ -1043,7 +1059,7 @@ export default {
   transition: background 0.25s ease;
   transition: background 0.25s ease;
   overflow: hidden;
   overflow: hidden;
 }
 }
-.heatmap__cell:hover { opacity: 0.88; }
+.heatmap__cell:hover { box-shadow: inset 0 0 0 2px var(--adm-primary-hover); }
 .heatmap__flag {
 .heatmap__flag {
   position: absolute;
   position: absolute;
   top: 2px;
   top: 2px;
@@ -1068,7 +1084,6 @@ export default {
   font-weight: 500;
   font-weight: 500;
   color: inherit;
   color: inherit;
   line-height: 1.2;
   line-height: 1.2;
-  opacity: 0.9;
   white-space: nowrap;
   white-space: nowrap;
   max-width: 100%;
   max-width: 100%;
   overflow: hidden;
   overflow: hidden;

+ 2 - 1
cfc-web/src/views/Login.vue

@@ -1084,7 +1084,8 @@ export default {
   }
   }
 }
 }
 .node-0 { --node-color:#FF8C42; --orbit-angle:0deg; --orbit-delay:0s; }
 .node-0 { --node-color:#FF8C42; --orbit-angle:0deg; --orbit-delay:0s; }
-.node-1 { --node-color:var(--adm-primary); --orbit-angle:72deg; --orbit-delay:-3s; }
+/* 五维「智」品牌色,独立于后台主色(后台主色加深后仍保持 #1677FF) */
+.node-1 { --node-color:#1677FF; --orbit-angle:72deg; --orbit-delay:-3s; }
 .node-2 { --node-color:#F59E0B; --orbit-angle:144deg; --orbit-delay:-6s; }
 .node-2 { --node-color:#F59E0B; --orbit-angle:144deg; --orbit-delay:-6s; }
 .node-3 { --node-color:#10B981; --orbit-angle:216deg; --orbit-delay:-9s; }
 .node-3 { --node-color:#10B981; --orbit-angle:216deg; --orbit-delay:-9s; }
 .node-4 { --node-color:#FF6B9D; --orbit-angle:288deg; --orbit-delay:-12s; }
 .node-4 { --node-color:#FF6B9D; --orbit-angle:288deg; --orbit-delay:-12s; }