Kaynağa Gözat

feat(body): radar chart dual-line support (avg + user scores)

- Add avgScores prop to RadarChart component (gray dashed line)
- New _drawAvgScoreArea() method renders average as background layer
- Body page passes mock avg scores alongside user dimension scores
- Avg scores: [72,68,75,70,73,69,71] for 7 dimensions
- Without user data, only avg line shows; with data, both lines visible
Xiaogang Liao 2 ay önce
ebeveyn
işleme
0d42b8f168

+ 82 - 5
cfc-frontend/components/RadarChart.vue

@@ -75,6 +75,28 @@ export default {
     height: {
       type: Number,
       default: 600
+    },
+    /** 平均值分数 0-100,与 dimensions 顺序一一对应(可选,显示为灰色虚线) */
+    avgScores: {
+      type: Array,
+      default: function() {
+        return []
+      }
+    },
+    /** 平均值填充颜色(可选,默认灰色) */
+    avgFillColor: {
+      type: String,
+      default: '#CBD5E1'
+    },
+    /** 平均值网格颜色(可选,默认浅灰) */
+    avgGridColor: {
+      type: String,
+      default: '#E2E8F0'
+    },
+    /** 平均值标签颜色(可选,默认浅灰) */
+    avgLabelColor: {
+      type: String,
+      default: '#94A3B8'
     }
   },
   data: function() {
@@ -123,6 +145,14 @@ export default {
         }
       },
       deep: true
+    },
+    avgScores: {
+      handler: function() {
+        if (this.canvasReady) {
+          this._doRender()
+        }
+      },
+      deep: true
     }
   },
   mounted: function() {
@@ -206,19 +236,24 @@ export default {
       // 1. 绘制背景网格
       this._drawGrid(ctx, cx, cy, outerR, angles, n)
 
-      // 2. 绘制分数区域
+      // 2. 如果有平均值,先画平均值(灰色虚线)
+      if (this.avgScores && this.avgScores.length >= n && n >= 3) {
+        this._drawAvgScoreArea(ctx, cx, cy, outerR, angles, n)
+      }
+
+      // 3. 绘制用户分数区域
       this._drawScoreArea(ctx, cx, cy, outerR, angles, n)
 
-      // 3. 绘制顶点标记
+      // 4. 绘制顶点标记
       this._drawVertices(ctx, cx, cy, outerR, angles, n)
 
-      // 4. 绘制分数文字
+      // 5. 绘制分数文字
       this._drawScoreText(ctx, cx, cy, outerR, angles, n)
 
-      // 5. 绘制维度标签
+      // 6. 绘制维度标签
       this._drawLabels(ctx, cx, cy, outerR, angles, n)
 
-      // 6. 绘制中心标题
+      // 7. 绘制中心标题
       this._drawCenterText(ctx, cx, cy)
     },
 
@@ -310,6 +345,48 @@ export default {
       ctx.stroke()
     },
 
+    /**
+     * 绘制平均值填充多边形(灰色虚线)
+     */
+    _drawAvgScoreArea: function(ctx, cx, cy, outerR, angles, n) {
+      var scores = this.avgScores
+      if (!scores || scores.length < n) { return }
+
+      // 构建顶点数据
+      var points = []
+      for (var i = 0; i < n; i++) {
+        var score = scores[i] || 0
+        var r = outerR * Math.max(0, Math.min(1, score / 100))
+        points.push({
+          x: cx + r * Math.cos(angles[i]),
+          y: cy + r * Math.sin(angles[i])
+        })
+      }
+
+      // 填充多边形(半透明灰色)
+      ctx.beginPath()
+      for (var pi = 0; pi < points.length; pi++) {
+        if (pi === 0) {
+          ctx.moveTo(points[pi].x, points[pi].y)
+        } else {
+          ctx.lineTo(points[pi].x, points[pi].y)
+        }
+      }
+      ctx.closePath()
+
+      var baseColor = this.avgFillColor || '#CBD5E1'
+      ctx.fillStyle = 'rgba(203,213,225,0.15)'
+      ctx.fill()
+
+      // 描边(灰色虚线)
+      ctx.save()
+      ctx.setLineDash([6, 4])
+      ctx.strokeStyle = baseColor
+      ctx.lineWidth = 2
+      ctx.stroke()
+      ctx.restore()
+    },
+
     /**
      * 在各顶点绘制小圆点
      */

+ 22 - 19
cfc-frontend/pages/body/index.vue

@@ -47,25 +47,26 @@
       @childChanged="onChildChanged"
       @scrollTo="scrollToSection" />
 
-    <!-- 雷达图 -->
-    <view class="section" v-if="dimensionData">
-      <view class="section-header" v-if="dimensionData.isAverage">
-        <text class="section-title">七维健康图</text>
-        <text class="avg-badge">同龄同性平均值 · 上传报告后显示个人数据</text>
-      </view>
-      <view class="section-header" v-else>
-        <text class="section-title">七维健康图</text>
-      </view>
-      <RadarChart
-        v-if="dimensionData && dimensionData.dimensions"
-        :dimensions="dimensionData.dimensions.map(function(d) { return { label: d.label, key: d.dimension } })"
-        :scores="dimensionData.dimensions.map(function(d) { return d.score })"
-        fillColor="#FF8C42"
-        gridColor="#FED7AA"
-        labelColor="#9A3412"
-        :width="580"
-        :height="580"
-        @dimensionClick="goDimensionDetail" />
+      <!-- 雷达图 -->
+      <view class="section" v-if="dimensionData">
+        <view class="section-header" v-if="dimensionData.isAverage">
+          <text class="section-title">七维健康图</text>
+          <text class="avg-badge">同龄同性平均值 · 上传报告后显示个人数据</text>
+        </view>
+        <view class="section-header" v-else>
+          <text class="section-title">七维健康图</text>
+        </view>
+        <RadarChart
+          v-if="dimensionData && dimensionData.dimensions"
+          :dimensions="dimensionData.dimensions.map(function(d) { return { label: d.label, key: d.dimension } })"
+          :scores="dimensionData.dimensions.map(function(d) { return d.score })"
+          :avg-scores="radarAvgScores"
+          fillColor="#FF8C42"
+          gridColor="#FED7AA"
+          labelColor="#9A3412"
+          :width="580"
+          :height="580"
+          @dimensionClick="goDimensionDetail" />
     </view>
 
     <!-- 七维健康全景(嵌入卡片) -->
@@ -266,6 +267,8 @@ export default {
       dimensionProducts: [],
       bodyArticles: [],
       dimensionData: null,
+      /** Mock 平均值(7维):生长发育/睡眠质量/视力健康/免疫力/营养均衡/肠胃健康/运动活力 */
+      radarAvgScores: [72, 68, 75, 70, 73, 69, 71],
       funcList: [
         { icon: '\u{1F3C3}', label: '运动', needLogin: true, page: '/pages/body/exercise-record' },
         { icon: '\u{1F957}', label: '饮食', needLogin: true, page: '/pages/body/meal-record' },