Jelajahi Sumber

refactor(beautify): D4 completion-rate 完成率分析页清理 (debt 121->38)

- template 5 处内联 SVG/legend 色绑定到 data.rateColors(JS 色板收编)
- style 全局令牌映射:#F97316->brand-primary, #FFF5F5->status-danger-tint, #E74C3C->status-danger-flat
- flat 图表专属色收编局部变量:青绿(-comp-done/-deep) + 米白(-comp-bg/card/warm) +
  文字灰阶(-comp-ink/muted/muted2) + 柱图渐变(-comp-bar-*)
- 3 处 box-shadow px->rpx(4px->8rpx, 20px->40rpx)
- P0 2->1;hardcoded hex 34->11(余 JS 色板声明)
liaoxg 5 hari lalu
induk
melakukan
6dc3507afe

+ 68 - 40
cfc-frontend/pages/stats/completion-rate.vue

@@ -24,15 +24,15 @@
           <view class="rate-ring-wrapper">
             <view class="rate-ring">
               <svg viewBox="0 0 120 120" class="ring-svg">
-                <circle cx="60" cy="60" r="52" fill="none" stroke="#FFF0E6" stroke-width="8" />
+                <circle cx="60" cy="60" r="52" fill="none" :stroke="rateColors.track" stroke-width="8" />
                 <circle cx="60" cy="60" r="52" fill="none" stroke="url(#rateGradient)" stroke-width="8"
                   stroke-linecap="round"
                   :stroke-dasharray="circumference"
                   :stroke-dashoffset="rateOffset" transform="rotate(-90 60 60)" />
                 <defs>
                   <linearGradient id="rateGradient" x1="0%" y1="0%" x2="100%" y2="100%">
-                    <stop offset="0%" stop-color="#4ECDC4" />
-                    <stop offset="100%" stop-color="#44B09E" />
+                    <stop offset="0%" :stop-color="rateColors.done" />
+                    <stop offset="100%" :stop-color="rateColors.doneDeep" />
                   </linearGradient>
                 </defs>
               </svg>
@@ -44,15 +44,15 @@
           </view>
           <view class="rate-legend">
             <view class="legend-item">
-              <view class="legend-dot" style="background: #4ECDC4;"></view>
+              <view class="legend-dot" :style="{ background: rateColors.done }"></view>
               <text class="legend-label">已完成 {{ stats.completedTasks }}</text>
             </view>
             <view class="legend-item">
-              <view class="legend-dot" style="background: #FFB088;"></view>
+              <view class="legend-dot" :style="{ background: rateColors.pending }"></view>
               <text class="legend-label">进行中 {{ stats.pendingTasks }}</text>
             </view>
             <view class="legend-item">
-              <view class="legend-dot" style="background: #F97316;"></view>
+              <view class="legend-dot" :style="{ background: rateColors.overdue }"></view>
               <text class="legend-label">已逾期 {{ stats.overdueTasks }}</text>
             </view>
           </view>
@@ -149,6 +149,19 @@
 
 <script>
 import { getChildCompletionStats, getFamilyCompletionStats, getChildren } from '../../utils/api.js'
+import { BrandColors } from '../../utils/brand-colors.js'
+
+// 完成率页专属色板(flat 青绿图表系 + 米白卡片系,单页专用)
+const RATE_COLORS = {
+  track:      '#FFF0E6', // 环形底轨
+  done:       '$c-comp-done', // 已完成(青绿)
+  doneDeep:   '$c-comp-done-deep', // 青绿渐变深档
+  pending:    '#FFB088', // 进行中(浅橙)
+  overdue:    BrandColors.primary, // 已逾期(品牌橙)
+  bar:        '#FFD93D', // 柱图高亮黄
+  barGreen:   '#A8E063', // 柱图渐变绿起
+  barGreenTo: '#56AB2F', // 柱图渐变绿终
+};
 
 export default {
   data() {
@@ -256,8 +269,23 @@ export default {
 </script>
 
 <style scoped lang="scss">
+// ===== 完成率页专属色阶(flat 图表青绿 + 米白卡片,单页专用;约定见 uni.scss §14)=====
+$c-comp-bg:           #FFF9F0; // 页面米白底
+$c-comp-card:         #FFF5EB; // 暖白卡片底
+$c-comp-warm:         #FFF0E6; // 暖米色(环形底轨 / 分隔)
+$c-comp-done:         $c-comp-done; // 已完成青绿(图表 / 图例)
+$c-comp-done-deep:    $c-comp-done-deep; // 青绿渐变深档
+$c-comp-ink:          #2D3436; // 主文字(深灰)
+$c-comp-muted:        #B2BEC3; // 次要文字(灰)
+$c-comp-muted2:       #636E72; // 辅助文字(中灰)
+$c-comp-danger-line:  #FFE0E0; // 危险浅描边
+$c-comp-orange-text:  #E17055; // 警示橙文字
+$c-comp-bar-yellow:   #FFD93D; // 柱图高亮黄
+$c-comp-bar-green:    #A8E063; // 柱图绿渐变起
+$c-comp-bar-green-to: #56AB2F; // 柱图绿渐变终
+
 .container {
-  background: #FFF9F0;
+  background: $c-comp-bg;
   min-height: 100vh;
   display: flex;
   flex-direction: column;
@@ -270,7 +298,7 @@ export default {
   justify-content: space-between;
   padding: 24rpx 32rpx;
   padding-top: calc(88rpx + env(safe-area-inset-top));
-  background: #FFF9F0;
+  background: $c-comp-bg;
   position: sticky;
   top: 0;
   z-index: 10;
@@ -287,7 +315,7 @@ export default {
 
 .back-arrow {
   font-size: 48rpx;
-  color: #2D3436;
+  color: $c-comp-ink;
   font-weight: 300;
   line-height: 1;
 }
@@ -295,13 +323,13 @@ export default {
 .header-title {
   font-size: 34rpx;
   font-weight: 700;
-  color: #2D3436;
+  color: $c-comp-ink;
 }
 
 .period-btn {
   font-size: 26rpx;
   color: $dim-body;
-  background: #FFF5EB;
+  background: $c-comp-card;
   padding: 8rpx 20rpx;
   border-radius: 24rpx;
   font-weight: 500;
@@ -325,7 +353,7 @@ export default {
 .loading-spinner {
   width: 64rpx;
   height: 64rpx;
-  border: 6rpx solid #FFF0E6;
+  border: 6rpx solid $c-comp-warm;
   border-top-color: $dim-body;
   border-radius: 50%;
   animation: spin 0.8s linear infinite;
@@ -337,7 +365,7 @@ export default {
 
 .loading-text {
   font-size: 26rpx;
-  color: #B2BEC3;
+  color: $c-comp-muted;
   margin-top: 20rpx;
 }
 
@@ -349,7 +377,7 @@ export default {
   margin-bottom: 24rpx;
   display: flex;
   align-items: center;
-  box-shadow: 0 4px 20px rgba(255, 140, 66, 0.08);
+  box-shadow: 0 8rpx 40rpx rgba(255, 140, 66, 0.08);
 }
 
 .rate-ring-wrapper {
@@ -381,13 +409,13 @@ export default {
 .rate-value {
   font-size: 40rpx;
   font-weight: 800;
-  color: #2D3436;
+  color: $c-comp-ink;
   line-height: 1.2;
 }
 
 .rate-label {
   font-size: 22rpx;
-  color: #B2BEC3;
+  color: $c-comp-muted;
   margin-top: 4rpx;
 }
 
@@ -413,7 +441,7 @@ export default {
 
 .legend-label {
   font-size: 26rpx;
-  color: #636E72;
+  color: $c-comp-muted2;
 }
 
 /* 快捷统计卡片 */
@@ -431,7 +459,7 @@ export default {
   display: flex;
   flex-direction: column;
   align-items: center;
-  box-shadow: 0 4px 20px rgba(255, 140, 66, 0.08);
+  box-shadow: 0 8rpx 40rpx rgba(255, 140, 66, 0.08);
 }
 
 .quick-stat-value {
@@ -442,23 +470,23 @@ export default {
 
 .quick-stat-label {
   font-size: 22rpx;
-  color: #B2BEC3;
+  color: $c-comp-muted;
   margin-top: 8rpx;
 }
 
 .primary-color { color: $dim-body; }
-.success-color { color: #4ECDC4; }
+.success-color { color: $c-comp-done; }
 .danger-color { color: $brand-primary; }
 
 /* 逾期警示 */
 .alert-card {
-  background: #FFF5F5;
+  background: $status-danger-tint;
   border-radius: 20rpx;
   padding: 24rpx;
   margin-bottom: 24rpx;
   display: flex;
   align-items: flex-start;
-  border: 1rpx solid #FFE0E0;
+  border: 1rpx solid $c-comp-danger-line;
 }
 
 .alert-icon {
@@ -474,13 +502,13 @@ export default {
 .alert-title {
   font-size: 28rpx;
   font-weight: 600;
-  color: #E74C3C;
+  color: $status-danger-flat;
   display: block;
 }
 
 .alert-desc {
   font-size: 24rpx;
-  color: #E17055;
+  color: $c-comp-orange-text;
   margin-top: 6rpx;
   display: block;
 }
@@ -491,7 +519,7 @@ export default {
   border-radius: 24rpx;
   padding: 28rpx;
   margin-bottom: 24rpx;
-  box-shadow: 0 4px 20px rgba(255, 140, 66, 0.08);
+  box-shadow: 0 8rpx 40rpx rgba(255, 140, 66, 0.08);
 }
 
 .section-header {
@@ -504,7 +532,7 @@ export default {
 .section-title {
   font-size: 30rpx;
   font-weight: 700;
-  color: #2D3436;
+  color: $c-comp-ink;
 }
 
 /* 孩子对比 */
@@ -522,7 +550,7 @@ export default {
 
 .compare-name {
   font-size: 28rpx;
-  color: #2D3436;
+  color: $c-comp-ink;
   font-weight: 500;
 }
 
@@ -534,7 +562,7 @@ export default {
 
 .compare-bar-bg {
   height: 12rpx;
-  background: #FFF0E6;
+  background: $c-comp-warm;
   border-radius: 999rpx;
   overflow: hidden;
 }
@@ -545,11 +573,11 @@ export default {
   transition: width 0.5s ease;
 }
 
-.bar-color-0 { background: linear-gradient(90deg, $dim-body, #FFD93D); }
-.bar-color-1 { background: linear-gradient(90deg, #4ECDC4, #44B09E); }
+.bar-color-0 { background: linear-gradient(90deg, $dim-body, $c-comp-bar-yellow); }
+.bar-color-1 { background: linear-gradient(90deg, $c-comp-done, $c-comp-done-deep); }
 .bar-color-2 { background: linear-gradient(90deg, $accent-indigo, $accent-indigo-deep); }
 .bar-color-3 { background: linear-gradient(90deg, $brand-primary, $brand-primary-light); }
-.bar-color-4 { background: linear-gradient(90deg, #A8E063, #56AB2F); }
+.bar-color-4 { background: linear-gradient(90deg, $c-comp-bar-green, $c-comp-bar-green-to); }
 
 /* 分类列表 */
 .category-list {
@@ -571,13 +599,13 @@ export default {
 
 .category-name {
   font-size: 24rpx;
-  color: #636E72;
+  color: $c-comp-muted2;
   display: block;
 }
 
 .category-count {
   font-size: 20rpx;
-  color: #B2BEC3;
+  color: $c-comp-muted;
   display: block;
   margin-top: 2rpx;
 }
@@ -585,14 +613,14 @@ export default {
 .category-bar-bg {
   flex: 1;
   height: 16rpx;
-  background: #FFF0E6;
+  background: $c-comp-warm;
   border-radius: 999rpx;
   overflow: hidden;
 }
 
 .category-bar-fill {
   height: 100%;
-  background: linear-gradient(90deg, #4ECDC4, #44B09E);
+  background: linear-gradient(90deg, $c-comp-done, $c-comp-done-deep);
   border-radius: 999rpx;
   transition: width 0.5s ease;
 }
@@ -602,7 +630,7 @@ export default {
   text-align: right;
   font-size: 24rpx;
   font-weight: 600;
-  color: #4ECDC4;
+  color: $c-comp-done;
   flex-shrink: 0;
 }
 
@@ -625,7 +653,7 @@ export default {
 
 .completed-title {
   font-size: 28rpx;
-  color: #2D3436;
+  color: $c-comp-ink;
   flex: 1;
   padding-right: 16rpx;
 }
@@ -647,7 +675,7 @@ export default {
 .empty-icon-area {
   width: 160rpx;
   height: 160rpx;
-  background: #FFF5EB;
+  background: $c-comp-card;
   border-radius: 50%;
   display: flex;
   align-items: center;
@@ -662,13 +690,13 @@ export default {
 .empty-title {
   font-size: 30rpx;
   font-weight: 600;
-  color: #2D3436;
+  color: $c-comp-ink;
   margin-bottom: 12rpx;
 }
 
 .empty-hint {
   font-size: 26rpx;
-  color: #B2BEC3;
+  color: $c-comp-muted;
   text-align: center;
 }
 </style>

+ 104 - 0
cfc-frontend/scripts/d4-completion-rate.js

@@ -0,0 +1,104 @@
+#!/usr/bin/env node
+/**
+ * D4: completion-rate.vue 完成率分析页清理 (debt 121)
+ *
+ * 1) template 5 处内联 SVG/legend 色绑定到 data.rateColors(JS 色板收编)
+ * 2) style 块 ~25 处专属 hex 收编为页面级局部 SCSS 变量
+ * 3) 3 个全局令牌映射:#F97316->$brand-primary / #FFF5F5->$status-danger-tint / #E74C3C->$status-danger-flat
+ */
+const fs = require('fs');
+const F = 'pages/stats/completion-rate.vue';
+let src = fs.readFileSync(F, 'utf8');
+
+// ---- 1. template 内联色 -> 动态绑定 ----
+const TEMPLATE_REPL = [
+  ['stroke="#FFF0E6"', ':stroke="rateColors.track"'],
+  ['stroke="#4ECDC4"', ':stroke="rateColors.done"'],
+  ['stop-color="#4ECDC4"', ':stop-color="rateColors.done"'],
+  ['stop-color="#44B09E"', ':stop-color="rateColors.doneDeep"'],
+  ['style="background: #4ECDC4;"', ':style="{ background: rateColors.done }"'],
+  ['style="background: #FFB088;"', ':style="{ background: rateColors.pending }"'],
+  ['style="background: #F97316;"', ':style="{ background: rateColors.overdue }"'],
+];
+for (const [from, to] of TEMPLATE_REPL) {
+  if (!src.includes(from)) console.warn('TPL NOT FOUND:', from);
+  src = src.split(from).join(to);
+}
+
+// ---- 2. script data 增加色板 ----
+const PALETTE_JS = [
+  "import { BrandColors } from '../../utils/brand-colors.js'",
+  "",
+  "// 完成率页专属色板(flat 青绿图表系 + 米白卡片系,单页专用)",
+  "const RATE_COLORS = {",
+  "  track:      '#FFF0E6', // 环形底轨",
+  "  done:       '#4ECDC4', // 已完成(青绿)",
+  "  doneDeep:   '#44B09E', // 青绿渐变深档",
+  "  pending:    '#FFB088', // 进行中(浅橙)",
+  "  overdue:    BrandColors.primary, // 已逾期(品牌橙)",
+  "  bar:        '#FFD93D', // 柱图高亮黄",
+  "  barGreen:   '#A8E063', // 柱图渐变绿起",
+  "  barGreenTo: '#56AB2F', // 柱图渐变绿终",
+  "};",
+].join('\n');
+
+src = src.replace(
+  "import { getChildCompletionStats, getFamilyCompletionStats, getChildren } from '../../utils/api.js'",
+  "import { getChildCompletionStats, getFamilyCompletionStats, getChildren } from '../../utils/api.js'\n" + PALETTE_JS
+);
+
+// data() 中注入 rateColors
+src = src.replace(
+  /    return \{\n      loading: true,/,
+  "    return {\n      rateColors: RATE_COLORS,\n      loading: true,"
+);
+
+// ---- 3. style 块收编 ----
+const styleIdx = src.indexOf('<style');
+const before = src.slice(0, styleIdx);
+let styleBody = src.slice(styleIdx);
+
+const GLOBAL_MAP = [
+  ['#F97316', '$brand-primary'],
+  ['#FFF5F5', '$status-danger-tint'],
+  ['#E74C3C', '$status-danger-flat'],
+];
+const PALETTE_MAP = [
+  ['#FFF9F0', '$c-comp-bg'],        // 页面米白底 x3
+  ['#2D3436', '$c-comp-ink'],       // 主文字(深灰)x9
+  ['#4223FC', '$c-comp-ink'],       // 防呆(实际无此值,占位无害)
+  ['#B2BEC3', '$c-comp-muted'],     // 次要灰文字 x5
+  ['#636E72', '$c-comp-muted2'],    // 辅助灰文字 x2
+  ['#FFF5EB', '$c-comp-card'],      // 暖白卡片底 x3
+  ['#FFF0E6', '$c-comp-warm'],      // 暖米底色(环形轨/按钮)x4
+  ['#FFE0E0', '$c-comp-danger-line'], // 危险浅描边 x1
+  ['#E17055', '$c-comp-orange-text'], // 警示橙文字(未打卡)x1
+  ['#FFD93D', '$c-comp-bar-yellow'],  // 柱图黄 x1
+  ['#A8E063', '$c-comp-bar-green'],   // 柱图绿渐变起 x1
+  ['#56AB2F', '$c-comp-bar-green-to'],// 柱图绿渐变终 x1
+];
+for (const [from, to] of [...GLOBAL_MAP, ...PALETTE_MAP.filter(p => p[0] !== '#4223FC')]) {
+  const esc = from.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+  styleBody = styleBody.replace(new RegExp(esc + '(?![0-9a-fA-F])', 'g'), to);
+}
+
+const palette = [
+  '// ===== 完成率页专属色阶(flat 图表青绿 + 米白卡片,单页专用;约定见 uni.scss §14)=====',
+  '$c-comp-bg:           #FFF9F0; // 页面米白底',
+  '$c-comp-card:         #FFF5EB; // 暖白卡片底',
+  '$c-comp-warm:         #FFF0E6; // 暖米色(环形底轨 / 分隔)',
+  '$c-comp-ink:          #2D3436; // 主文字(深灰)',
+  '$c-comp-muted:        #B2BEC3; // 次要文字(灰)',
+  '$c-comp-muted2:       #636E72; // 辅助文字(中灰)',
+  '$c-comp-danger-line:  #FFE0E0; // 危险浅描边',
+  '$c-comp-orange-text:  #E17055; // 警示橙文字',
+  '$c-comp-bar-yellow:   #FFD93D; // 柱图高亮黄',
+  '$c-comp-bar-green:    #A8E063; // 柱图绿渐变起',
+  '$c-comp-bar-green-to: #56AB2F; // 柱图绿渐变终',
+  ''
+].join('\n');
+
+styleBody = styleBody.replace(/(<style[^>]*>\s*\n)/, '$1' + palette + '\n');
+
+fs.writeFileSync(F, before + styleBody);
+console.log('D4 done');

+ 21 - 0
cfc-frontend/scripts/d4b-completion-rate.js

@@ -0,0 +1,21 @@
+#!/usr/bin/env node
+const fs = require('fs');
+const F = 'pages/stats/completion-rate.vue';
+let src = fs.readFileSync(F, 'utf8');
+const REPL = [
+  ['#4ECDC4', '$c-comp-done'],
+  ['#44B09E', '$c-comp-done-deep'],
+];
+let cnt = 0;
+for (const [f, t] of REPL) {
+  const esc = f.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+  src = src.replace(new RegExp(esc + '(?![0-9a-fA-F])', 'g'), () => { cnt++; return t; });
+}
+fs.writeFileSync(F, src);
+console.log('replaced:', cnt);
+const lines = src.split('\n');
+let n = 0;
+for (let i = 0; i < lines.length; i++) {
+  if (/#[0-9a-fA-F]{3,8}\b/.test(lines[i]) && !/^\s*\$c-/.test(lines[i])) { console.log('REMAIN ' + (i + 1) + ': ' + lines[i].trim()); n++; }
+}
+console.log('remaining non-palette:', n);

+ 17 - 0
cfc-frontend/scripts/d4c-completion-rate-px.js

@@ -0,0 +1,17 @@
+#!/usr/bin/env node
+const fs = require('fs');
+const F = 'pages/stats/completion-rate.vue';
+let src = fs.readFileSync(F, 'utf8');
+const REPL = [
+  ['box-shadow: 0 4px 20px rgba(255, 140, 66, 0.08);', 'box-shadow: 0 8rpx 40rpx rgba(255, 140, 66, 0.08);'],
+];
+let cnt = 0;
+for (const [f, t] of REPL) {
+  while (src.includes(f)) { src = src.replace(f, t); cnt++; }
+}
+fs.writeFileSync(F, src);
+console.log('replaced:', cnt);
+const lines = src.split('\n');
+for (let i = 0; i < lines.length; i++) {
+  if (/\d+px/.test(lines[i])) console.log('REMAIN ' + (i + 1) + ': ' + lines[i].trim());
+}