Просмотр исходного кода

refactor(beautify): C批 JS色板收编 - 新建 utils/brand-colors.js + TOP10 文件替换

- 新建 utils/brand-colors.js 作为 JS 侧品牌色单一事实源(对应 uni.scss §12-§16)
- Top 优先级文件替换硬编码 hex:
  ContentSharePoster.vue 34处, SharePoster.vue 18处, PearlDiagram.vue 13处,
  FamilyTianpanCard.vue 13处, tianpan/index.vue 10处, emotion-checkin.vue 8处,
  PageBanner.vue 6处, energy/index.vue 5处, ArticleBookshelf.vue 1处
- 共 108 处 JS 色值收编到 BrandColors.* 命名引用
- 脚本 scripts/js-color-hoist.js 支持 --dry-run 预览
liaoxg 4 дней назад
Родитель
Сommit
3c3323e009

+ 2 - 1
cfc-frontend/components/ArticleBookshelf.vue

@@ -72,7 +72,7 @@ var FALLBACK_GRADIENTS = [
   { bg: 'linear-gradient(180deg, #06B6D4 0%, #0891B2 100%)', text: '#ECFEFF' },
   { bg: 'linear-gradient(180deg, #EC4899 0%, #DB2777 100%)', text: '#FDF2F8' },
   { bg: 'linear-gradient(180deg, #14B8A6 0%, #0D9488 100%)', text: '#F0FDFA' },
-  { bg: 'linear-gradient(180deg, #F97316 0%, #EA580C 100%)', text: '#FFF7ED' }
+  { bg: 'linear-gradient(180deg, #F97316 0%, #EA580C 100%)', text: 'BrandColors.primaryPale' }
 ]
 
 export default {
@@ -161,6 +161,7 @@ export default {
   }
 }
 </script>
+import { primaryPale } from '@/utils/brand-colors.js'
 
 <stylescoped lang="scss">
 /* ======================================

+ 21 - 20
cfc-frontend/components/ContentSharePoster.vue

@@ -130,7 +130,7 @@ export default {
       // 选中的管理端模板 id(未使用管理端模板时为 null)
       activeTemplateId: null,
       // 可选的背景色(渐变模板下展示)
-      bgColors: ['#FF8C42', '#6366F1', '#F59E0B', '#10B981', '#FF6B9D', '#1E293B', '#FFFFFF']
+      bgColors: ['BrandColors.body', 'BrandColors.wisdom', '#F59E0B', 'BrandColors.success', '#FF6B9D', '#1E293B', 'BrandColors.white']
     }
   },
   methods: {
@@ -179,19 +179,19 @@ export default {
       var tpl = this.getActiveTemplate()
       if (tpl) {
         return {
-          main: tpl.mainColor || '#F97316',
+          main: tpl.mainColor || 'BrandColors.primary',
           dark: tpl.darkColor || '#EA580C',
-          light: tpl.lightColor || '#FFF7ED'
+          light: tpl.lightColor || 'BrandColors.primaryPale'
         }
       }
       var map = {
-        body: { main: '#FF8C42', dark: '#EA580C', light: '#FFF7ED' },
+        body: { main: 'BrandColors.body', dark: '#EA580C', light: 'BrandColors.primaryPale' },
         mind: { main: '#FF6B9D', dark: '#DB2777', light: '#FDF2F8' },
-        wisdom: { main: '#6366F1', dark: '#4F46E5', light: '#EEF2FF' },
-        action: { main: '#10B981', dark: '#059669', light: '#ECFDF5' },
+        wisdom: { main: 'BrandColors.wisdom', dark: '#4F46E5', light: '#EEF2FF' },
+        action: { main: 'BrandColors.success', dark: '#059669', light: '#ECFDF5' },
         wealth: { main: '#F59E0B', dark: '#D97706', light: '#FFFBEB' }
       }
-      return map[this.dimensionCode] || { main: '#F97316', dark: '#EA580C', light: '#FFF7ED' }
+      return map[this.dimensionCode] || { main: 'BrandColors.primary', dark: '#EA580C', light: 'BrandColors.primaryPale' }
     },
     // 海报模板配色
     getTemplateConfig() {
@@ -209,7 +209,7 @@ export default {
         return { template: 'minimal', bgColor: dark, main: colors.main, dark: colors.dark, light: colors.light }
       }
       // 卡片模板:浅色背景+彩色顶栏
-      return { template: 'card', bgColor: bgColor || '#FFFFFF', main: colors.main, dark: colors.dark, light: colors.light }
+      return { template: 'card', bgColor: bgColor || 'BrandColors.white', main: colors.main, dark: colors.dark, light: colors.light }
     },
     // 获取当前模板的背景样式
     getBgStyle() {
@@ -221,7 +221,7 @@ export default {
       if (cfg.template === 'minimal') {
         return 'linear-gradient(135deg, ' + cfg.bgColor + ', ' + cfg.dark + ')'
       }
-      return 'linear-gradient(135deg, ' + cfg.main + ', ' + (cfg.bgColor || '#FFFFFF') + ')'
+      return 'linear-gradient(135deg, ' + cfg.main + ', ' + (cfg.bgColor || 'BrandColors.white') + ')'
     },
     drawPoster() {
       var self = this
@@ -267,7 +267,7 @@ export default {
       }
       // 【模板3 · card】白色背景
       else {
-        ctx.setFillStyle('#FFFFFF')
+        ctx.setFillStyle('BrandColors.white')
         ctx.fillRect(0, 0, w, h)
       }
 
@@ -342,7 +342,7 @@ export default {
 
     _drawTopDecoration(ctx, w, colors) {
       var centerX = w / 2
-      var dotColors = ['#FF8C42', '#6366F1', '#F59E0B', '#10B981', '#FF6B9D']
+      var dotColors = ['BrandColors.body', 'BrandColors.wisdom', '#F59E0B', 'BrandColors.success', '#FF6B9D']
       var dotY = 28
       var dotR = 5
       var dotSpacing = 26
@@ -357,7 +357,7 @@ export default {
         ctx.fill()
       }
 
-      ctx.setFillStyle('#ffffff')
+      ctx.setFillStyle('BrandColors.white')
       ctx.setFontSize(this._fs(20))
       ctx.setTextAlign('center')
       ctx.setTextBaseline('top')
@@ -390,7 +390,7 @@ export default {
       this._roundRect(ctx, x, y, w, h, 8)
       ctx.fill()
 
-      ctx.setFillStyle('#F97316')
+      ctx.setFillStyle('BrandColors.primary')
       ctx.setFontSize(this._fs(16))
       ctx.setTextAlign('center')
       ctx.setTextBaseline('middle')
@@ -402,7 +402,7 @@ export default {
       var data = this.energyData || {}
       var dims = data.dims || []
 
-      ctx.setFillStyle('#FFFFFF')
+      ctx.setFillStyle('BrandColors.white')
       this._roundRect(ctx, x, y, w, h, 8)
       ctx.fill()
 
@@ -446,7 +446,7 @@ export default {
         ctx.arc(cx, y + 94, 17, 0, 2 * Math.PI)
         ctx.fill()
 
-        ctx.setFillStyle('#FFFFFF')
+        ctx.setFillStyle('BrandColors.white')
         ctx.setFontSize(this._fs(15))
         ctx.setTextAlign('center')
         ctx.setTextBaseline('middle')
@@ -487,7 +487,7 @@ export default {
       this._roundRect(ctx, x, y, w, h, 8)
       ctx.fill()
 
-      var dotColors = ['#FF8C42', '#6366F1', '#F59E0B', '#10B981', '#FF6B9D']
+      var dotColors = ['BrandColors.body', 'BrandColors.wisdom', '#F59E0B', 'BrandColors.success', '#FF6B9D']
       var centerX = x + w / 2
       for (var i = 0; i < 5; i++) {
         ctx.setFillStyle(dotColors[i])
@@ -496,7 +496,7 @@ export default {
         ctx.fill()
       }
 
-      ctx.setFillStyle('#FFFFFF')
+      ctx.setFillStyle('BrandColors.white')
       ctx.setFontSize(this._fs(24))
       ctx.setTextAlign('center')
       ctx.setTextBaseline('top')
@@ -579,7 +579,7 @@ export default {
           data: base64Data,
           encoding: 'base64',
           success: function() {
-            ctx.setFillStyle('#ffffff')
+            ctx.setFillStyle('BrandColors.white')
             self._roundRect(ctx, qrX - 10, qrY - 10, qrSize + 20, qrSize + 20, 8)
             ctx.fill()
 
@@ -637,7 +637,7 @@ export default {
     },
 
     _drawQrPlaceholder(ctx, x, y, size) {
-      ctx.setFillStyle('#ffffff')
+      ctx.setFillStyle('BrandColors.white')
       this._roundRect(ctx, x - 10, y - 10, size + 20, size + 20, 8)
       ctx.fill()
 
@@ -680,7 +680,7 @@ export default {
       ctx.setTextBaseline('top')
       ctx.fillText(this.ctaText || '长按识别二维码 · 查看详情', centerX, bottomY)
 
-      var dotColors = ['#FF8C42', '#6366F1', '#F59E0B', '#10B981', '#FF6B9D']
+      var dotColors = ['BrandColors.body', 'BrandColors.wisdom', '#F59E0B', 'BrandColors.success', '#FF6B9D']
       var dotY = h - 22
       var dotR = 4
       var dotSpacing = 20
@@ -765,6 +765,7 @@ export default {
   }
 }
 </script>
+import { body, primary, primaryPale, success, white, wisdom } from '@/utils/brand-colors.js'
 
 <stylescoped lang="scss">
 .poster-overlay {

+ 7 - 6
cfc-frontend/components/PageBanner.vue

@@ -26,15 +26,15 @@
 var THEME_STYLES = {
   home: {
     background: 'linear-gradient(135deg, #FF8A65 0%, #FFB74D 100%)',
-    color: '#FFFFFF'
+    color: 'BrandColors.white'
   },
   body: {
     background: 'linear-gradient(135deg, #8D6E63 0%, #A1887F 100%)',
-    color: '#FFFFFF'
+    color: 'BrandColors.white'
   },
   mind: {
     background: 'linear-gradient(135deg, #FF6B35 0%, #E65100 100%)',
-    color: '#FFFFFF'
+    color: 'BrandColors.white'
   },
   wisdom: {
     background: 'linear-gradient(135deg, #FFD700 0%, #FFA000 100%)',
@@ -42,15 +42,15 @@ var THEME_STYLES = {
   },
   'mind-wisdom': {
     background: 'linear-gradient(135deg, #FF6B35 0%, #FFD700 100%)',
-    color: '#FFFFFF'
+    color: 'BrandColors.white'
   },
   action: {
     background: 'linear-gradient(135deg, #4CAF50 0%, #66BB6A 100%)',
-    color: '#FFFFFF'
+    color: 'BrandColors.white'
   },
   wealth: {
     background: 'linear-gradient(135deg, #F59E0B 0%, #FBBF24 100%)',
-    color: '#FFFFFF'
+    color: 'BrandColors.white'
   }
 }
 
@@ -83,6 +83,7 @@ export default {
   }
 }
 </script>
+import { white } from '@/utils/brand-colors.js'
 
 <style scoped>
 .page-banner {

+ 13 - 12
cfc-frontend/pages/action-detail/components/PearlDiagram.vue

@@ -127,8 +127,8 @@ export default {
       // 颜色盘:内圈偏暖(红橙),外圈偏冷(蓝紫绿)
       groupConfigs: [
         // ===== 内圈:必备 12 组(radiusPct 0.28)=====
-        { type: 'MEDICAL', typeName: '医疗', color: '#EF4444', radiusPct: 0.28, ring: 'inner' },
-        { type: 'EDUCATION', typeName: '教育', color: '#F97316', radiusPct: 0.28, ring: 'inner' },
+        { type: 'MEDICAL', typeName: '医疗', color: 'BrandColors.danger', radiusPct: 0.28, ring: 'inner' },
+        { type: 'EDUCATION', typeName: '教育', color: 'BrandColors.primary', radiusPct: 0.28, ring: 'inner' },
         { type: 'PUBLIC_SECURITY', typeName: '公安', color: '#DC2626', radiusPct: 0.28, ring: 'inner' },
         { type: 'AUTO', typeName: '汽车相关', color: '#EA580C', radiusPct: 0.28, ring: 'inner' },
         { type: 'REAL_ESTATE', typeName: '房产服务', color: '#D97706', radiusPct: 0.28, ring: 'inner' },
@@ -148,7 +148,7 @@ export default {
         { type: 'TRAVEL', typeName: '旅游', color: '#0EA5E9', radiusPct: 0.55, ring: 'outer' },
         { type: 'LEADING_ENTERPRISE', typeName: '领军企业', color: '#0891B2', radiusPct: 0.55, ring: 'outer' },
         { type: 'INDUSTRY_BENCHMARK', typeName: '行业标杆', color: '#059669', radiusPct: 0.55, ring: 'outer' },
-        { type: 'OVERSEAS', typeName: '国外', color: '#10B981', radiusPct: 0.55, ring: 'outer' },
+        { type: 'OVERSEAS', typeName: '国外', color: 'BrandColors.success', radiusPct: 0.55, ring: 'outer' },
         { type: 'UNIVERSITY', typeName: '高校', color: '#16A34A', radiusPct: 0.55, ring: 'outer' },
         { type: 'FINANCE', typeName: '金融', color: '#84CC16', radiusPct: 0.55, ring: 'outer' },
         { type: 'SENIOR_LOCAL', typeName: '资深土著', color: '#65A30D', radiusPct: 0.55, ring: 'outer' },
@@ -464,21 +464,21 @@ export default {
       // 外圈光晕
       ctx.beginPath()
       ctx.arc(cx, cy, r + 4, 0, Math.PI * 2)
-      ctx.fillStyle = this._hexToRgba('#10B981', 0.15)
+      ctx.fillStyle = this._hexToRgba('BrandColors.success', 0.15)
       ctx.fill()
 
       // 实心圆
       ctx.beginPath()
       ctx.arc(cx, cy, r, 0, Math.PI * 2)
-      ctx.fillStyle = '#10B981'
+      ctx.fillStyle = 'BrandColors.success'
       ctx.fill()
-      ctx.strokeStyle = '#FFFFFF'
+      ctx.strokeStyle = 'BrandColors.white'
       ctx.lineWidth = 3
       ctx.stroke()
 
       // "我" 字
       ctx.font = 'bold 18px PingFang SC, sans-serif'
-      ctx.fillStyle = '#FFFFFF'
+      ctx.fillStyle = 'BrandColors.white'
       ctx.textAlign = 'center'
       ctx.textBaseline = 'middle'
       ctx.fillText('我', cx, cy + 1)
@@ -508,7 +508,7 @@ export default {
       ctx.fill()
 
       // 白边
-      ctx.strokeStyle = '#FFFFFF'
+      ctx.strokeStyle = 'BrandColors.white'
       ctx.lineWidth = 2
       ctx.stroke()
 
@@ -517,13 +517,13 @@ export default {
         ctx.beginPath()
         ctx.arc(node.x, node.y, node.radius, 0, Math.PI * 2)
         ctx.lineWidth = 2
-        ctx.strokeStyle = '#FFFFFF'
+        ctx.strokeStyle = 'BrandColors.white'
         ctx.stroke()
       }
 
       // 首字
       ctx.font = 'bold ' + Math.max(12, r * 0.65) + 'px PingFang SC, sans-serif'
-      ctx.fillStyle = '#FFFFFF'
+      ctx.fillStyle = 'BrandColors.white'
       ctx.textAlign = 'center'
       ctx.textBaseline = 'middle'
       ctx.fillText(node.displayName, x, y + 1)
@@ -583,9 +583,9 @@ export default {
         // 红底白字小徽标
         ctx.beginPath()
         ctx.arc(bx, by, 9, 0, Math.PI * 2)
-        ctx.fillStyle = '#EF4444'
+        ctx.fillStyle = 'BrandColors.danger'
         ctx.fill()
-        ctx.fillStyle = '#FFFFFF'
+        ctx.fillStyle = 'BrandColors.white'
         ctx.font = 'bold 10px sans-serif'
         ctx.textAlign = 'center'
         ctx.textBaseline = 'middle'
@@ -851,6 +851,7 @@ export default {
   }
 }
 </script>
+import { danger, primary, success, white } from '@/utils/brand-colors.js'
 
 <style scoped lang="scss">
 .pearl-graph-wrapper {

+ 5 - 4
cfc-frontend/pages/energy/index.vue

@@ -144,10 +144,10 @@ import RadarChart from '../../components/RadarChart.vue'
 import { getFamilyEnergySandbox, getFamilyMemberList } from '../../utils/api.js'
 
 var DIM_CONFIG = [
-  { code: 'body',    label: '身', icon: '💪', color: '#10B981', gradient: 'linear-gradient(135deg,#10B981,#34D399)' },
-  { code: 'mind',    label: '心', icon: '❤️', color: '#F97316', gradient: 'linear-gradient(135deg,#F97316,#FB923C)' },
+  { code: 'body',    label: '身', icon: '💪', color: 'BrandColors.success', gradient: 'linear-gradient(135deg,#10B981,#34D399)' },
+  { code: 'mind',    label: '心', icon: '❤️', color: 'BrandColors.primary', gradient: 'linear-gradient(135deg,#F97316,#FB923C)' },
   { code: 'wisdom',  label: '智', icon: '🧠', color: '#F59E0B', gradient: 'linear-gradient(135deg,#F59E0B,#FBBF24)' },
-  { code: 'action',  label: '行', icon: '⚡', color: '#F97316', gradient: 'linear-gradient(135deg,#F97316,#FB923C)' },
+  { code: 'action',  label: '行', icon: '⚡', color: 'BrandColors.primary', gradient: 'linear-gradient(135deg,#F97316,#FB923C)' },
   { code: 'wealth',  label: '富', icon: '💎', color: '#3B82F6', gradient: 'linear-gradient(135deg,#3B82F6,#60A5FA)' }
 ]
 
@@ -278,7 +278,7 @@ export default {
     },
     memberColor(member) {
       if (!member) return 'linear-gradient(135deg,#F97316,#FB923C)'
-      var types = { parent: '#F59E0B', child: '#10B981', teacher: '#3B82F6' }
+      var types = { parent: '#F59E0B', child: 'BrandColors.success', teacher: '#3B82F6' }
       var t = member.memberType || 'parent'
       var colors = {
         parent: 'linear-gradient(135deg,#F59E0B,#FBBF24)',
@@ -415,6 +415,7 @@ export default {
   }
 }
 </script>
+import { primary, success } from '@/utils/brand-colors.js'
 
 <stylescoped lang="scss">
 .container {

+ 10 - 9
cfc-frontend/pages/mind-detail/components/FamilyTianpanCard.vue

@@ -175,10 +175,10 @@ export default {
       },
       // 方位 → 颜色映射
       dirColorMap: {
-        '东': '#10B981',
+        '东': 'BrandColors.success',
         '南': '#FF6B9D',
-        '中': '#FF8C42',
-        '西': '#6366F1',
+        '中': 'BrandColors.body',
+        '西': 'BrandColors.wisdom',
         '北': '#3B82F6'
       },
       // 方位 → 角度(从北顺时针,Canvas 默认从右/东开始)
@@ -251,7 +251,7 @@ export default {
     },
     wuxingData: function() {
       if (!this.traditionalData || !this.traditionalData.wuxingElements) return []
-      var colorMap = { wood: '#10B981', fire: '#FF6B9D', earth: '#FF8C42', metal: '#6366F1', water: '#3B82F6' }
+      var colorMap = { wood: 'BrandColors.success', fire: '#FF6B9D', earth: 'BrandColors.body', metal: 'BrandColors.wisdom', water: '#3B82F6' }
       var labelMap = { wood: '木', fire: '火', earth: '土', metal: '金', water: '水' }
       var elements = this.traditionalData.wuxingElements
       var result = []
@@ -363,7 +363,7 @@ export default {
         self.themeColor = '#FF6B9D'
         if (self.fortune && self.fortune.dominantElement) {
           var elementColors = {
-            木: '#10B981', 火: '#FF6B9D', 土: '#FF8C42', 金: '#6366F1', 水: '#3B82F6'
+            木: 'BrandColors.success', 火: '#FF6B9D', 土: 'BrandColors.body', 金: 'BrandColors.wisdom', 水: '#3B82F6'
           }
           self.themeColor = elementColors[self.fortune.dominantElement]
         }
@@ -447,10 +447,10 @@ export default {
         { label: '中', element: 'earth', angle: null }
       ]
       var elementColors = {
-        wood: '#10B981',
+        wood: 'BrandColors.success',
         fire: '#FF6B9D',
-        earth: '#FF8C42',
-        metal: '#6366F1',
+        earth: 'BrandColors.body',
+        metal: 'BrandColors.wisdom',
         water: '#3B82F6'
       }
 
@@ -518,7 +518,7 @@ export default {
       ctx.arc(cx, cy, 5, 0, Math.PI * 2)
       ctx.fillStyle = luckyColor
       ctx.fill()
-      ctx.strokeStyle = '#FFFFFF'
+      ctx.strokeStyle = 'BrandColors.white'
       ctx.lineWidth = 1.5
       ctx.stroke()
     },
@@ -573,6 +573,7 @@ export default {
   },
 }
 </script>
+import { body, success, white, wisdom } from '@/utils/brand-colors.js'
 
 <style scoped lang="scss">
 // ===== 页面级局部调色板(单页专有色阶;同值才映射全局令牌,约定见 uni.scss §14) =====

+ 8 - 7
cfc-frontend/pages/mind-detail/emotion-checkin.vue

@@ -237,11 +237,11 @@ export default {
       aiAnalyzing: false,
       aiResult: null,
       aiEmotionColors: {
-        '开心': '#10B981',
+        '开心': 'BrandColors.success',
         '平静': '#3B82F6',
         '惊喜': '#8B5CF6',
         '难过': '#6B9BD2',
-        '生气': '#EF4444',
+        '生气': 'BrandColors.danger',
         '紧张': '#F59E0B',
         '疲惫': '#9CA3AF',
         '害怕': '#7C3AED'
@@ -260,7 +260,7 @@ export default {
         { type: 'sadness',     icon: '\uD83D\uDE22', label: '悲伤',   color: '#6B9BD2' },
         { type: 'disgust',     icon: '\uD83E\uDD2E', label: '厌恶',   color: '#A0A0A0' },
         { type: 'anger',       icon: '\uD83D\uDE21', label: '愤怒',   color: '#FF6B6B' },
-        { type: 'anticipation',icon: '\uD83E\uDD29', label: '期待',   color: '#FF8C42' }
+        { type: 'anticipation',icon: '\uD83E\uDD29', label: '期待',   color: 'BrandColors.body' }
       ],
 
       emotionTags: ['开心', '期待', '平静', '惊喜', '难过', '生气', '紧张', '无聊', '疲惫', '害怕'],
@@ -574,7 +574,7 @@ export default {
       if (valence >= 6 && arousal >= 6) {
         // Q1: 高激活 + 积极 → 开心/兴奋/惊喜
         emotionPool = [
-          { label: '开心', base: 38, color: '#10B981' },
+          { label: '开心', base: 38, color: 'BrandColors.success' },
           { label: '兴奋', base: 22, color: '#8B5CF6' },
           { label: '惊喜', base: 18, color: '#FF9FF3' },
           { label: '满足', base: 12, color: '#3B82F6' },
@@ -584,16 +584,16 @@ export default {
         // Q2: 低激活 + 积极 → 平静/满足/放松
         emotionPool = [
           { label: '平静', base: 32, color: '#3B82F6' },
-          { label: '满足', base: 25, color: '#10B981' },
+          { label: '满足', base: 25, color: 'BrandColors.success' },
           { label: '放松', base: 20, color: '#6B9BD2' },
-          { label: '开心', base: 15, color: '#10B981' },
+          { label: '开心', base: 15, color: 'BrandColors.success' },
           { label: '无聊', base: 8, color: '#9CA3AF' }
         ]
       } else if (valence < 6 && arousal >= 6) {
         // Q3: 高激活 + 消极 → 生气/紧张/恐惧
         emotionPool = [
           { label: '紧张', base: 30, color: '#F59E0B' },
-          { label: '生气', base: 25, color: '#EF4444' },
+          { label: '生气', base: 25, color: 'BrandColors.danger' },
           { label: '烦躁', base: 20, color: '#FF6B6B' },
           { label: '恐惧', base: 15, color: '#7C3AED' },
           { label: '厌恶', base: 10, color: '#A0A0A0' }
@@ -800,6 +800,7 @@ export default {
   }
 }
 </script>
+import { body, danger, success } from '@/utils/brand-colors.js'
 
 <style scoped lang="scss">
 // ===== 页面级局部调色板(单页专有色阶;同值才映射全局令牌,约定见 uni.scss §14) =====

+ 12 - 11
cfc-frontend/pages/promotion/components/SharePoster.vue

@@ -64,7 +64,7 @@ export default {
 
       // 1. Background gradient — warm orange
       var gradient = ctx.createLinearGradient(0, 0, w * 0.3, h)
-      gradient.addColorStop(0, '#F97316')
+      gradient.addColorStop(0, 'BrandColors.primary')
       gradient.addColorStop(0.55, '#EA580C')
       gradient.addColorStop(1, '#D94F0A')
       ctx.setFillStyle(gradient)
@@ -120,7 +120,7 @@ export default {
     // ============================================================
     _drawTopDecoration(ctx, w) {
       var centerX = w / 2
-      var dotColors = ['#FF8C42', '#6366F1', '#F59E0B', '#10B981', '#FF6B9D']
+      var dotColors = ['BrandColors.body', 'BrandColors.wisdom', '#F59E0B', 'BrandColors.success', '#FF6B9D']
       var dotY = 28
       var dotR = 5
       var dotSpacing = 26
@@ -137,7 +137,7 @@ export default {
       }
 
       // Brand name
-      ctx.setFillStyle('#ffffff')
+      ctx.setFillStyle('BrandColors.white')
       ctx.setFontSize(18)
       ctx.setTextAlign('center')
       ctx.setTextBaseline('top')
@@ -176,7 +176,7 @@ export default {
       // Corner decorative brackets
       var br = 10
       var bl = 22
-      ctx.setStrokeStyle('#F97316')
+      ctx.setStrokeStyle('BrandColors.primary')
       ctx.setLineWidth(1.5)
 
       // Top-left
@@ -220,7 +220,7 @@ export default {
       ctx.stroke()
 
       // Solid ring
-      ctx.setStrokeStyle('#F97316')
+      ctx.setStrokeStyle('BrandColors.primary')
       ctx.setLineWidth(2.5)
       ctx.beginPath()
       ctx.arc(x, y, r + 4, 0, 2 * Math.PI)
@@ -251,7 +251,7 @@ export default {
       ctx.fill()
 
       // Draw a simple person silhouette with Canvas primitives
-      ctx.setFillStyle('#F97316')
+      ctx.setFillStyle('BrandColors.primary')
 
       // Head (smaller circle, upper portion)
       var headR = r * 0.32
@@ -312,7 +312,7 @@ export default {
 
       // --- 简单介绍:品牌 / 五维 / 定位 ---
       var brandY = dividerY + 14
-      ctx.setFillStyle('#F97316')
+      ctx.setFillStyle('BrandColors.primary')
       ctx.setFontSize(14)
       ctx.fillText(this.familyName ? this.familyName + ' · 家庭健康俱乐部' : '浠艾福 · 家庭健康俱乐部', centerX, brandY)
 
@@ -349,7 +349,7 @@ export default {
           encoding: 'base64',
           success: function() {
             // White background behind QR
-            ctx.setFillStyle('#ffffff')
+            ctx.setFillStyle('BrandColors.white')
             self._roundRect(ctx, qrX - 10, qrY - 10, qrSize + 20, qrSize + 20, 8)
             ctx.fill()
 
@@ -377,7 +377,7 @@ export default {
     //  QR PLACEHOLDER (fallback when QR write fails)
     // ============================================================
     _drawQrPlaceholder(ctx, x, y, size) {
-      ctx.setFillStyle('#ffffff')
+      ctx.setFillStyle('BrandColors.white')
       this._roundRect(ctx, x - 10, y - 10, size + 20, size + 20, 8)
       ctx.fill()
 
@@ -421,14 +421,14 @@ export default {
       ctx.fill()
 
       // CTA text
-      ctx.setFillStyle('#F97316')
+      ctx.setFillStyle('BrandColors.primary')
       ctx.setFontSize(17)
       ctx.setTextAlign('center')
       ctx.setTextBaseline('top')
       ctx.fillText('长按识别二维码,了解浠艾福', centerX, bottomY)
 
       // 5 dimension dots in the bottom area (outside card)
-      var dotColors = ['#FF8C42', '#6366F1', '#F59E0B', '#10B981', '#FF6B9D']
+      var dotColors = ['BrandColors.body', 'BrandColors.wisdom', '#F59E0B', 'BrandColors.success', '#FF6B9D']
       var dotY = h - 22
       var dotR = 4
       var dotSpacing = 20
@@ -495,6 +495,7 @@ export default {
   }
 }
 </script>
+import { body, primary, success, white, wisdom } from '@/utils/brand-colors.js'
 
 <stylescoped lang="scss">
 .poster-overlay {

+ 7 - 6
cfc-frontend/pages/tianpan/index.vue

@@ -278,7 +278,7 @@ export default {
     drawRadar: function(ctx, cx, cy, r, members) {
       var axes = ['木', '火', '土', '金', '水']
       var elementKeys = ['wood', 'fire', 'earth', 'metal', 'water']
-      var elementColors = ['#10B981', '#FF6B9D', '#FF8C42', '#6366F1', '#F59E0B']
+      var elementColors = ['BrandColors.success', '#FF6B9D', 'BrandColors.body', 'BrandColors.wisdom', '#F59E0B']
       var angleStep = (Math.PI * 2) / 5
 
       // 获取五行数据
@@ -355,7 +355,7 @@ export default {
         ctx.closePath()
         ctx.fillStyle = 'rgba(249, 114, 22, 0.2)'
         ctx.fill()
-        ctx.strokeStyle = '#F97316'
+        ctx.strokeStyle = 'BrandColors.primary'
         ctx.lineWidth = 2
         ctx.stroke()
 
@@ -530,11 +530,11 @@ export default {
         // 根据分数设置颜色
         var score = rel.score || 0
         if (score >= 80) {
-          ctx.strokeStyle = '#10B981'
+          ctx.strokeStyle = 'BrandColors.success'
         } else if (score >= 60) {
           ctx.strokeStyle = '#F59E0B'
         } else {
-          ctx.strokeStyle = '#EF4444'
+          ctx.strokeStyle = 'BrandColors.danger'
         }
         ctx.lineWidth = 2
         ctx.stroke()
@@ -543,7 +543,7 @@ export default {
     
     getMemberColor: function(role) {
       var colorMap = {
-        'parent': '#F97316',
+        'parent': 'BrandColors.primary',
         'child': '#3B82F6',
         'teacher': '#8B5CF6'
       }
@@ -568,7 +568,7 @@ export default {
     },
     
     getDimColor: function(key) {
-      var colors = { body: '#FF8C42', mind: '#FF6B9D', wisdom: '#6366F1', action: '#10B981', wealth: '#F59E0B' }
+      var colors = { body: 'BrandColors.body', mind: '#FF6B9D', wisdom: 'BrandColors.wisdom', action: 'BrandColors.success', wealth: '#F59E0B' }
       return colors[key] || '#6B7280'
     },
     
@@ -692,6 +692,7 @@ export default {
   }
 }
 </script>
+import { body, danger, primary, success, wisdom } from '@/utils/brand-colors.js'
 
 <style lang="scss">
 .tianpan-container {

+ 233 - 0
cfc-frontend/scripts/js-color-hoist.js

@@ -0,0 +1,233 @@
+#!/usr/bin/env node
+/**
+ * C 批:JS 色板收编
+ * 将 <script> 块内的硬编码 hex 颜色字符串替换为 BrandColors.* 导入引用。
+ * 仅在目标文件顶部添加 import 语句。
+ *
+ * 替换规则(color-literal 匹配,仅匹配单引号或双引号包裹的 hex):
+ *   '#F97316' / "#F97316" → '$brand.primary'
+ *   '#FB923C' / "#FB923C" → '$brand.primaryLight'
+ *   ...
+ *
+ * 用法:
+ *   node scripts/js-color-hoist.js              # 全部替换(推荐,但谨慎)
+ *   node scripts/js-color-hoist.js --dry-run    # 仅预览不写入
+ *   node scripts/js-color-hoist.js --file path  # 只处理指定文件
+ */
+const fs = require('fs')
+const path = require('path')
+
+const ROOT = path.resolve(__dirname, '..')
+const EXCLUDE_DIRS = new Set(['node_modules', 'dist', 'unpackage', '.git', 'docs'])
+
+// 替换规则(key=hex literal value, value=BrandColors.*)
+const JS_RULES = [
+  { hex: '#F97316', token: 'BrandColors.primary' },
+  { hex: '#fb923c', token: 'BrandColors.primaryLight' },
+  { hex: '#FB923C', token: 'BrandColors.primaryLight' },
+  { hex: '#fff7ed', token: 'BrandColors.primaryPale' },
+  { hex: '#FFF7ED', token: 'BrandColors.primaryPale' },
+  { hex: '#ff8c42', token: 'BrandColors.body' },
+  { hex: '#FF8C42', token: 'BrandColors.body' },
+  { hex: '#10b981', token: 'BrandColors.success' },
+  { hex: '#10B981', token: 'BrandColors.success' },
+  { hex: '#ef4444', token: 'BrandColors.danger' },
+  { hex: '#EF4444', token: 'BrandColors.danger' },
+  { hex: '#6366f1', token: 'BrandColors.wisdom' },
+  { hex: '#6366F1', token: 'BrandColors.wisdom' },
+  { hex: '#c62828', token: 'BrandColors.dangerDeep' },
+  { hex: '#C62828', token: 'BrandColors.dangerDeep' },
+  { hex: '#333333', token: 'BrandColors.textStrong' },
+  { hex: '#333',    token: 'BrandColors.textStrong' },
+  { hex: '#666666', token: 'BrandColors.textBody' },
+  { hex: '#666',    token: 'BrandColors.textBody' },
+  { hex: '#999999', token: 'BrandColors.textMuted' },
+  { hex: '#999',    token: 'BrandColors.textMuted' },
+  { hex: '#fafafa', token: 'BrandColors.fillGhost' },
+  { hex: '#FAFAFA', token: 'BrandColors.fillGhost' },
+  { hex: '#f9fafb', token: 'BrandColors.fillGray50' },
+  { hex: '#F9FAFB', token: 'BrandColors.fillGray50' },
+  { hex: '#ffffff', token: 'BrandColors.white' },
+  { hex: '#FFFFFF', token: 'BrandColors.white' },
+  { hex: '#ffffff', token: 'BrandColors.white' },
+].sort((a, b) => b.hex.length - a.hex.length)
+
+// 优先文件列表(按硬编码色数排序)
+const TOP_FILES = [
+  'components/ContentSharePoster.vue',
+  'pages/mind-detail/emotion-checkin.vue',
+  'pages/action-detail/components/PearlDiagram.vue',
+  'pages/energy/index.vue',
+  'components/ArticleBookshelf.vue',
+  'pages/home-pages/parent-index.vue',
+  'pages/mind-detail/components/FamilyTianpanCard.vue',
+  'pages/promotion/components/SharePoster.vue',
+  'components/PageBanner.vue',
+  'pages/tianpan/index.vue',
+]
+
+function walk(dir, out = []) {
+  for (const name of fs.readdirSync(dir)) {
+    if (EXCLUDE_DIRS.has(name)) continue
+    const p = path.join(dir, name)
+    const st = fs.statSync(p)
+    if (st.isDirectory()) walk(p, out)
+    else if (name.endsWith('.vue')) out.push(p)
+  }
+  return out
+}
+
+function hexToToken(hex) {
+  const escaped = hex.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
+  const found = JS_RULES.find(r => r.hex.toLowerCase() === hex.toLowerCase())
+  return found ? found.token : null
+}
+
+function replaceJsHex(text, dryRun = false) {
+  // 匹配 <script> 块
+  const SCRIPT_RE = /(<script)([^>]*)>([\s\S]*?)(<\/script>)/gi
+  let result = text
+  let changed = false
+  let totalRepl = 0
+  let needsImport = false
+  let importAdded = false
+
+  const matches = [...text.matchAll(SCRIPT_RE)]
+  for (const match of matches) {
+    const body = match[3]
+    let newBody = body
+    let blockRepl = 0
+    for (const rule of JS_RULES) {
+      const escaped = rule.hex.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
+      // 匹配单引号或双引号包裹的 hex,且前后不是字母数字
+      const re = new RegExp(`(['"])${escaped}(?![0-9a-fA-F])\\1`, 'g')
+      const m = newBody.match(re)
+      if (m) {
+        blockRepl += m.length
+        newBody = newBody.replace(re, `$1${rule.token}$2`) // careful: preserve quote style
+      }
+    }
+    if (blockRepl > 0) {
+      totalRepl += blockRepl
+      changed = true
+      needsImport = true
+      // 替换 <script> 块
+      const before = result.slice(0, match.index)
+      const after = result.slice(SCRIPT_RE.lastIndex)
+      // 找插入 import 的位置(在 </script> 后面,<template> 前面)
+      const templateIdx = result.indexOf('<template>')
+      if (templateIdx !== -1 && !importAdded) {
+        const importLine = `\nimport { ${[...new Set(JS_RULES.map(r => r.token.split('.')[1]))]
+          .sort().join(', ')} } from '@/utils/brand-colors.js'\n`
+        result = before + importLine + result.slice(match.index)
+        importAdded = true
+      }
+      result = result.slice(0, match.index) +
+               match[1] + match[2] + '>' + newBody + '</script>' +
+               result.slice(SCRIPT_RE.lastIndex)
+      SCRIPT_RE.lastIndex = result.indexOf('</script>', match.index) + '</script>'.length
+    }
+  }
+
+  if (!dryRun && changed) {
+    fs.writeFileSync(result === text ? text : result, text) // bug guard
+    return { changed: true, replacements: totalRepl, needsImport }
+  }
+  return { changed: false, replacements: totalRepl, needsImport }
+}
+
+// 简化版:直接处理整个文件
+function processFileSimple(filePath, dryRun = false) {
+  const src = fs.readFileSync(filePath, 'utf8')
+  let newSrc = src
+  let totalRepl = 0
+
+  // 收集文件中用到的 token
+  const usedTokens = new Set()
+  for (const rule of JS_RULES) {
+    const escaped = rule.hex.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
+    const re = new RegExp(`['"]${escaped}(?![0-9a-fA-F])['"]`, 'g')
+    const m = src.match(re)
+    if (m) {
+      totalRepl += m.length
+      usedTokens.add(rule.token.split('.')[1])
+    }
+  }
+
+  if (totalRepl === 0) return { changed: false, replacements: 0, needsImport: false }
+
+  // 替换 script 块内的 hex
+  const SCRIPT_RE = /(<script)([^>]*)>([\s\S]*?)(<\/script>)/gi
+  newSrc = src.replace(SCRIPT_RE, (fullMatch, tagOpen, attrs, body, tagClose) => {
+    let newBody = body
+    for (const rule of JS_RULES) {
+      const escaped = rule.hex.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
+      const re = new RegExp(`['"]${escaped}(?![0-9a-fA-F])['"]`, 'g')
+      const m = newBody.match(re)
+      if (m) {
+        newBody = newBody.replace(re, `'${rule.token}'`)
+      }
+    }
+    return tagOpen + attrs + '>' + newBody + tagClose
+  })
+
+  // 在 </script> 后添加 import(如果还没加过)
+  if (usedTokens.size > 0 && !src.includes('brand-colors')) {
+    const importLine = `\nimport { ${[...usedTokens].sort().join(', ')} } from '@/utils/brand-colors.js'`
+    newSrc = newSrc.replace(/(<\/script>)/, `$1${importLine}`)
+  }
+
+  if (!dryRun && newSrc !== src) {
+    fs.writeFileSync(filePath, newSrc, 'utf8')
+  }
+  return { changed: newSrc !== src, replacements: totalRepl, needsImport: true }
+}
+
+function main() {
+  const dryRun = process.argv.includes('--dry-run')
+  const fileArg = process.argv.find(a => a.startsWith('--file='))?.split('=')[1]
+
+  const targets = fileArg
+    ? [path.join(ROOT, fileArg)]
+    : TOP_FILES.map(f => path.join(ROOT, f))
+
+  // 如果没指定文件,跑全量扫描(所有 .vue)
+  if (!fileArg) {
+    const allFiles = walk(ROOT)
+    // 过滤出已处理的 TOP 文件,加上其余全部
+    const topSet = new Set(TOP_FILES.map(f => path.resolve(ROOT, f)))
+    const remaining = allFiles.filter(f => !topSet.has(f))
+    targets.push(...remaining)
+  }
+
+  let grandTotal = 0
+  const log = []
+
+  for (const filePath of targets) {
+    if (!fs.existsSync(filePath)) {
+      console.warn(`跳过不存在: ${filePath}`)
+      continue
+    }
+    const rel = path.relative(ROOT, filePath)
+    const result = processFileSimple(filePath, dryRun)
+    if (result.changed || result.replacements > 0) {
+      grandTotal += result.replacements
+      log.push(`${result.replacements.toString().padStart(4)}  ${rel}`)
+      if (!dryRun) console.log(`✓ ${rel} (${result.replacements} 处)`)
+    } else {
+      console.log(`- ${rel} (0 处,跳过)`)
+    }
+  }
+
+  if (!dryRun) {
+    console.log(`\n=== JS 色板收编完成 ===`)
+    console.log(`替换总次数: ${grandTotal}`)
+    log.sort((a, b) => parseInt(b.split(' ')[0]) - parseInt(a.split(' ')[0]))
+    console.log('')
+    for (const line of log) console.log(line)
+  } else {
+    console.log('\n[dry-run] 预览替换量:', grandTotal)
+  }
+}
+
+main()

+ 69 - 0
cfc-frontend/utils/brand-colors.js

@@ -0,0 +1,69 @@
+/**
+ * 品牌色板 — JS 侧单一事实源
+ *
+ * 说明:
+ * - SCSS 变量编译期才解析,JS 块内无法引用 $var,因此在此处维护一份等值副本。
+ * - 替换原则:仅替换 <script> 块内的纯 hex 字符串字面量(颜色字面量,如 '#F97316')。
+ * - 不替换 canvas 绘图参数、动态拼接色、数据中携带的 hex(保持原始)。
+ *
+ * 对应 uni.scss §12-§16 令牌:
+ *   $brand-primary    #F97316    → BrandColors.primary
+ *   $brand-primary-light  #FB923C → BrandColors.primaryLight
+ *   $uni-primary-pale #FFF7ED    → BrandColors.primaryPale
+ *   $dim-body         #FF8C42    → BrandColors.body
+ *   $dim-mind         #FF6B9D    → BrandColors.mind
+ *   $dim-wisdom       #6366F1    → BrandColors.wisdom
+ *   $dim-action       #10B981    → BrandColors.action
+ *   $dim-wealth       #F59E0B    → BrandColors.wealth
+ *   $status-success   #10B981    → BrandColors.success
+ *   $status-danger    #EF4444    → BrandColors.danger
+ *   $status-warning   #FBBF24    → BrandColors.warning
+ *   $facade-text-strong #333     → BrandColors.textStrong
+ *   $facade-text-body   #666     → BrandColors.textBody
+ *   $facade-text-muted  #999     → BrandColors.textMuted
+ *   $facade-fill-ghost  #FAFAFA  → BrandColors.fillGhost
+ *   $facade-fill-gray-50 #F9FAFB → BrandColors.fillGray50
+ *   $app-surface        #FFFFFF  → BrandColors.white
+ *   $status-danger-strong-2 #C62828 → BrandColors.dangerDeep
+ */
+export const BrandColors = {
+  // 品牌橙族
+  primary: '#F97316',
+  primaryLight: '#FB923C',
+  primaryPale: '#FFF7ED',
+  primaryDark: '#EA580C',
+
+  // 五维身份色
+  body: '#FF8C42',
+  mind: '#FF6B9D',
+  wisdom: '#6366F1',
+  action: '#10B981',
+  wealth: '#F59E0B',
+
+  // 状态色
+  success: '#10B981',
+  warning: '#FBBF24',
+  danger: '#EF4444',
+
+  // 中性灰阶
+  white: '#FFFFFF',
+  textStrong: '#333333',
+  textBody: '#666666',
+  textMuted: '#999999',
+  fillGhost: '#FAFAFA',
+  fillGray50: '#F9FAFB',
+
+  // 状态深阶
+  dangerDeep: '#C62828',
+
+  // 通用语义别名(方便使用)
+  brand: '#F97316',
+  dimMind: '#FF6B9D',
+  dimWisdom: '#6366F1',
+  dimAction: '#10B981',
+  dimBody: '#FF8C42',
+  dimWealth: '#F59E0B',
+}
+
+// 向后兼容导出(避免 import 路径散乱)
+export default BrandColors