Răsfoiți Sursa

merge(admin): 全系统主色加深(--adm-primary 达 WCAG AA)+ 五维色解耦 合入 cfclub 主线

Agnes Agent 1 zi în urmă
părinte
comite
08731b70d4

+ 12 - 8
cfc-web/src/styles/admin-tokens.css

@@ -19,10 +19,10 @@
    使所有引用旧变量的规则自动纳入新体系,无需逐条改写。
    ============================================================ */
 :root {
-  /* ---- 品牌主色与功能色(Ant Design 5 语义色) ---- */
-  --color-primary: #1677FF;          /* 原 #409EFF → Ant 蓝 */
-  --color-primary-light: #4096FF;    /* hover */
-  --color-primary-dark: #0958D9;     /* active */
+  /* ---- 品牌主色与功能色(主色加深以满足 AA) ---- */
+  --color-primary: #0B5FCC;          /* 加深以满足 AA 对比度 */
+  --color-primary-light: #0958D9;    /* hover 态(命名沿用 Ant,非亮度指示) */
+  --color-primary-dark: #003EB3;     /* active:三态最深 */
   --color-success: #52C41A;          /* 原 #67C23A */
   --color-warning: #FAAD14;          /* 原 #E6A23C */
   --color-danger: #FF4D4F;           /* 原 #F56C6C */
@@ -81,9 +81,9 @@
   --width-select-xl: 200px;
 
   /* ---- 企业级语义令牌(Ant 规范,新增样式优先使用这一组) ---- */
-  --adm-primary: #1677FF;
-  --adm-primary-hover: #4096FF;
-  --adm-primary-active: #0958D9;
+  --adm-primary: #0B5FCC;
+  --adm-primary-hover: #0958D9;
+  --adm-primary-active: #003EB3;
   --adm-success: #52C41A;
   --adm-success-text: #389E0D;
   --adm-success-bg: #F6FFED;
@@ -96,7 +96,7 @@
   --adm-error-text: #CF1322;
   --adm-error-bg: #FFF1F0;
   --adm-error-border: #FFCCC7;
-  --adm-info: #1677FF;
+  --adm-info: #0B5FCC;
   --adm-info-text: #0958D9;
   --adm-info-bg: #E6F4FF;
   --adm-info-border: #91CAFF;
@@ -138,6 +138,10 @@
 .el-button--primary { background-color: var(--adm-primary); border-color: var(--adm-primary); color: #fff; }
 .el-button--primary:hover,
 .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.is-disabled,
 .el-button--primary.is-disabled:hover { background-color: #A0C4FF; border-color: #A0C4FF; color: #fff; }

+ 8 - 3
cfc-web/src/utils/colorTokens.js

@@ -8,7 +8,9 @@
  * 2. `--adm-primary` 等令牌的取值在 admin-tokens.css 中定义,本模块只读取不改动;
  * 3. canvas / echarts 的 itemStyle.color、color 数组不解析 CSS 变量,
  *    必须经 adminColor() 用 getComputedStyle 读取运行时令牌值;
- * 4. 普通 DOM 样式仍优先写 `var(--adm-primary)`,不要为了统一样式而多绕一层 JS。
+ * 4. 普通 DOM 样式仍优先写 `var(--adm-primary)`,不要为了统一样式而多绕一层 JS;
+ * 5. 后台主色令牌与五维品牌色已解耦、互不影响:主色加深后不再与五维「智」同源,
+ *    adminColor('primary') 的兜底值由 ADMIN_PRIMARY_FALLBACK 独立提供,五维色值恒定不动。
  */
 
 /** 五维色彩映射(单一出口) */
@@ -20,6 +22,9 @@ export const DIMENSION_COLORS = {
   mind: '#FF6B9D' // 心
 }
 
+/** 后台主色兜底值:与五维品牌色解耦,独立维护,勿复用 DIMENSION_COLORS.wisdom */
+const ADMIN_PRIMARY_FALLBACK = '#0B5FCC'
+
 /** 后台语义令牌 → CSS 变量名 */
 const TOKEN_VAR_MAP = {
   primary: '--adm-primary',
@@ -54,8 +59,8 @@ export function adminColor(key) {
   if (Object.prototype.hasOwnProperty.call(DIMENSION_COLORS, key)) return DIMENSION_COLORS[key]
   const varName = TOKEN_VAR_MAP[key]
   if (!varName) return ''
-  // primary 的兜底值与五维「智」同源(--adm-primary 即 #1677FF)
-  const fallback = key === 'primary' ? DIMENSION_COLORS.wisdom : ''
+  // 兜底值独立于五维品牌色:后台主色加深后与「智」解耦,两者互不影响
+  const fallback = key === 'primary' ? ADMIN_PRIMARY_FALLBACK : ''
   return readCssVar(varName, fallback)
 }
 

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

@@ -261,6 +261,7 @@
 <script>
 import * as echarts from 'echarts'
 import SectionTitle from '@/components/SectionTitle.vue'
+import { adminColor } from '@/utils/colorTokens'
 import {
   getDashboardStats,
   getMembershipStats,
@@ -409,6 +410,17 @@ export default {
       const v = getComputedStyle(document.documentElement).getPropertyValue(name).trim()
       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) {
       // v-for 回调内 this 不是组件(Vue2 普通函数 this=undefined),
@@ -424,21 +436,25 @@ export default {
       if (!max) return 0
       return Math.max(4, Math.round((Number(value) || 0) / max * 100))
     },
-    // 时段热力单元格颜色:按相对峰值线性加深(浅蓝 -> 品牌主色 #1677FF)
-    // 文字色随背景动态切换,保证对比度:
-    //   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) {
+      // 有数据格的文字层不得带任何 alpha(含 CSS opacity),否则上述阈值失效。
+      const DARK_TEXT = '#000000'
+      const LIGHT_TEXT = '#FFFFFF'
       const peak = this.dwellByHourPeak || 0
       if (!peak || !v) return { background: 'var(--adm-fill)', color: 'var(--adm-text-45)' }
       const ratio = Math.min(1, Number(v) / peak)
       const alpha = (0.1 + ratio * 0.9).toFixed(3)
-      const bgColor = `rgba(22, 119, 255, ${alpha})`
-      const useLightText = parseFloat(alpha) >= 0.9
+      const bgColor = this.rgbaFromPrimary(alpha)
+      const useLightText = parseFloat(alpha) >= 0.87
       return {
         background: bgColor,
-        color: useLightText ? 'rgba(255,255,255,0.92)' : 'var(--adm-text-88)'
+        color: useLightText ? LIGHT_TEXT : DARK_TEXT
       }
     },
     fmtDuration(sec) {
@@ -597,8 +613,8 @@ export default {
             itemStyle: { color: this.cssVar('--adm-primary') },
             areaStyle: {
               color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
-                { offset: 0, color: 'rgba(22,119,255,0.18)' },
-                { offset: 1, color: 'rgba(22,119,255,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;
   overflow: hidden;
 }
-.heatmap__cell:hover { opacity: 0.88; }
+.heatmap__cell:hover { box-shadow: inset 0 0 0 2px var(--adm-primary-hover); }
 .heatmap__flag {
   position: absolute;
   top: 2px;
@@ -1068,7 +1084,6 @@ export default {
   font-weight: 500;
   color: inherit;
   line-height: 1.2;
-  opacity: 0.9;
   white-space: nowrap;
   max-width: 100%;
   overflow: hidden;

+ 4 - 3
cfc-web/src/views/Login.vue

@@ -1013,7 +1013,7 @@ export default {
 }
 
 .login-btn:hover {
-  background: #4096FF;
+  background: var(--adm-primary-hover);
   box-shadow: 0 4px 16px rgba(74, 155, 215, 0.35);
   transform: translateY(-1px);
 }
@@ -1084,11 +1084,12 @@ export default {
   }
 }
 .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-3 { --node-color:#10B981; --orbit-angle:216deg; --orbit-delay:-9s; }
 .node-4 { --node-color:#FF6B9D; --orbit-angle:288deg; --orbit-delay:-12s; }
-.role-tag-blue { border-color:var(--adm-primary); color:#4096FF; }
+.role-tag-blue { border-color:var(--adm-primary); color:var(--adm-primary); }
 .role-tag-pink { border-color:#FF6B9D; color:#FF8CB3; }
 .role-tag-green { border-color:#10B981; color:#34D399; }