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

feat(body): dimension questionnaire, detail page, and entry navigation

Sisyphus 4 месяцев назад
Родитель
Сommit
52ae55b438

+ 12 - 0
cfc-frontend/pages.json

@@ -83,6 +83,18 @@
         "navigationBarTitleText": "健康打卡"
       }
     },
+    {
+      "path": "pages/body/dimension-questionnaire",
+      "style": {
+        "navigationBarTitleText": "维度问卷"
+      }
+    },
+    {
+      "path": "pages/body/dimension-detail",
+      "style": {
+        "navigationBarTitleText": "维度详情"
+      }
+    },
     {
       "path": "pages/body/member-body-detail",
       "style": {

+ 520 - 0
cfc-frontend/pages/body/dimension-detail.vue

@@ -0,0 +1,520 @@
+<template>
+  <view class="dd-container">
+    <!-- Loading -->
+    <view class="dd-loading" v-if="loading">
+      <text class="loading-text">加载中...</text>
+    </view>
+
+    <template v-else>
+      <!-- Dimension Header -->
+      <view class="dd-header">
+        <view class="dd-score-ring">
+          <text class="dd-score-value">{{ currentScore || '--' }}</text>
+          <text class="dd-score-label">分</text>
+        </view>
+        <text class="dd-dim-name">{{ dimLabel }}</text>
+        <text class="dd-dim-level" :class="'level-' + (dimLevel || 'fair')">{{ levelText }}</text>
+      </view>
+
+      <!-- History Trend Chart (Canvas) -->
+      <view class="dd-section">
+        <text class="dd-section-title">历史趋势</text>
+        <view class="dd-chart-wrap" v-if="history.length > 0">
+          <canvas canvas-id="trendCanvas" class="dd-canvas"></canvas>
+        </view>
+        <view class="dd-empty" v-else>
+          <text class="empty-text">暂无历史数据</text>
+          <text class="empty-sub">完成评估或上传报告后可查看趋势</text>
+        </view>
+      </view>
+
+      <!-- Percentile -->
+      <view class="dd-section" v-if="percentile !== null">
+        <text class="dd-section-title">常模对比</text>
+        <view class="dd-percentile-wrap">
+          <text class="dd-percentile-value">{{ percentile }}%</text>
+          <text class="dd-percentile-label">超过同龄同性别 {{ percentile }}% 的人群</text>
+        </view>
+      </view>
+
+      <!-- Suggestions -->
+      <view class="dd-section" v-if="suggestions.length > 0">
+        <text class="dd-section-title">改善建议</text>
+        <view
+          class="dd-suggestion-item"
+          v-for="(s, idx) in suggestions"
+          :key="idx"
+        >
+          <text class="dd-sug-num">{{ idx + 1 }}</text>
+          <text class="dd-sug-text">{{ s }}</text>
+        </view>
+      </view>
+
+      <!-- Related Products -->
+      <view class="dd-section" v-if="products.length > 0">
+        <text class="dd-section-title">相关商品</text>
+        <view
+          class="dd-product-item"
+          v-for="p in products"
+          :key="p.id"
+          @click="goProduct(p)"
+        >
+          <image class="dd-prod-img" :src="p.coverImage || '/static/default-product.png'" mode="aspectFill"></image>
+          <view class="dd-prod-info">
+            <text class="dd-prod-name">{{ p.name }}</text>
+            <text class="dd-prod-price">¥{{ p.price }}</text>
+          </view>
+        </view>
+      </view>
+
+      <!-- Action Buttons -->
+      <view class="dd-actions">
+        <button class="dd-btn" @click="goQuestionnaire">填写问卷</button>
+        <button class="dd-btn dd-btn-outline" @click="goDimensions">返回全景</button>
+      </view>
+    </template>
+  </view>
+</template>
+
+<script>
+import { getDimensionHistory, getDimensionOverview, getProductsByDomain } from '@/utils/api'
+
+var SUGGESTIONS = {
+  growth: [
+    '保证每日充足的蛋白质和钙摄入,牛奶、鸡蛋、豆制品是优质选择',
+    '每周至少3次户外活动,跳绳、篮球等纵向运动有助于骨骼发育',
+    '定期记录身高体重,关注生长曲线是否在正常范围内',
+    '保证充足睡眠,生长激素在夜间深睡期分泌最旺盛'
+  ],
+  sleep: [
+    '建立固定的睡前程序:洗热水澡、读故事、调暗灯光',
+    '卧室保持安静、黑暗、凉爽,温度控制在18-22°C',
+    '睡前1小时避免使用电子设备,蓝光会抑制褪黑素分泌',
+    '保持规律的作息时间,周末与平日差异不超过1小时'
+  ],
+  vision: [
+    '遵循"20-20-20"法则:每20分钟看20英尺外20秒',
+    '每日保证至少2小时户外活动,自然光有助于预防近视',
+    '保持正确读写姿势:眼离书本一尺、胸离桌沿一拳',
+    '每半年进行一次视力检查,建立视力发育档案'
+  ],
+  immunity: [
+    '均衡饮食,确保蛋白质、维生素C、锌等免疫营养素摄入',
+    '适度运动增强体质,但避免过度训练导致免疫力下降',
+    '保证充足睡眠,睡眠不足会影响免疫细胞活性',
+    '注意手部卫生,但不过度消毒,适度接触微生物有助免疫系统发育'
+  ],
+  nutrition: [
+    '每天摄入12种以上食材,每周25种以上,确保营养多样性',
+    '减少加工食品和高糖饮料,增加全谷物、蔬菜、水果比例',
+    '关注铁、钙、维生素D等易缺乏营养素的摄入',
+    '培养自主进食能力,营造愉快的就餐氛围'
+  ],
+  gut: [
+    '增加膳食纤维摄入,全谷物、蔬菜、水果是优质来源',
+    '适量补充益生菌食物:酸奶、发酵食品等',
+    '保持规律排便习惯,及时响应便意',
+    '减少油炸、辛辣等刺激性食物,减轻肠胃负担'
+  ],
+  exercise: [
+    '每天至少60分钟中高强度运动,可分多次完成',
+    '运动类型多样化:有氧+力量+柔韧性训练结合',
+    '让运动变得有趣:球类游戏、游泳、骑自行车都是好选择',
+    '家长以身作则,全家一起运动效果更好'
+  ]
+}
+
+export default {
+  data() {
+    return {
+      loading: true,
+      dimension: '',
+      childId: '',
+      dimLabel: '',
+      currentScore: null,
+      dimLevel: 'fair',
+      percentile: null,
+      history: [],
+      products: [],
+      suggestions: []
+    }
+  },
+  computed: {
+    levelText: function() {
+      var map = { excellent: '优秀', good: '良好', fair: '一般', poor: '较差' }
+      return map[this.dimLevel] || '一般'
+    }
+  },
+  onLoad: function(options) {
+    this.dimension = options.dimension || ''
+    this.childId = options.childId || uni.getStorageSync('currentChildId') || ''
+    this.setDimLabel()
+    this.suggestions = SUGGESTIONS[this.dimension] || []
+    this.loadData()
+  },
+  onReady: function() {
+    if (this.history.length > 0) {
+      this.drawChart()
+    }
+  },
+  methods: {
+    setDimLabel: function() {
+      var map = {
+        growth: '生长发育',
+        sleep: '睡眠质量',
+        vision: '视力健康',
+        immunity: '免疫力',
+        nutrition: '营养均衡',
+        gut: '肠胃健康',
+        exercise: '运动活力'
+      }
+      this.dimLabel = map[this.dimension] || this.dimension
+    },
+    loadData: function() {
+      var self = this
+      self.loading = true
+
+      // Load dimension overview for current score and percentile
+      getDimensionOverview({ memberId: this.childId }).then(function(res) {
+        if (res.code === 200 && res.data && res.data.dimensions) {
+          for (var i = 0; i < res.data.dimensions.length; i++) {
+            var dim = res.data.dimensions[i]
+            if (dim.dimension === self.dimension) {
+              self.currentScore = dim.score
+              self.dimLevel = dim.level || 'fair'
+              self.percentile = dim.percentile || null
+              break
+            }
+          }
+        }
+      }).catch(function(e) {
+        console.log('获取维度概览失败', e)
+      })
+
+      // Load history
+      getDimensionHistory({ memberId: this.childId, dimension: this.dimension, limit: 10 }).then(function(res) {
+        self.loading = false
+        if (res.code === 200 && res.data) {
+          self.history = Array.isArray(res.data) ? res.data : (res.data.history || [])
+          // Try to draw chart if canvas is ready
+          setTimeout(function() {
+            self.drawChart()
+          }, 100)
+        }
+      }).catch(function(e) {
+        self.loading = false
+        console.log('获取历史数据失败', e)
+      })
+
+      // Load related products from recommendation
+      this.loadProducts()
+    },
+    loadProducts: function() {
+      var self = this
+      getProductsByDomain('body', 1, 5).then(function(res) {
+        if (res.code === 200 && res.data) {
+          var list = res.data.rows || res.data
+          self.products = Array.isArray(list) ? list.slice(0, 5) : []
+        }
+      }).catch(function() {})
+    },
+    drawChart: function() {
+      if (this.history.length === 0) return
+      var ctx = uni.createCanvasContext('trendCanvas', this)
+      var w = 660
+      var h = 280
+      var padding = { top: 30, right: 30, bottom: 40, left: 50 }
+      var chartW = w - padding.left - padding.right
+      var chartH = h - padding.top - padding.bottom
+
+      // Clear
+      ctx.setFillStyle('#fff')
+      ctx.fillRect(0, 0, w, h)
+
+      // Grid lines
+      for (var i = 0; i <= 4; i++) {
+        var y = padding.top + (chartH / 4) * i
+        ctx.setStrokeStyle('#f0f0f0')
+        ctx.setLineWidth(1)
+        ctx.beginPath()
+        ctx.moveTo(padding.left, y)
+        ctx.lineTo(w - padding.right, y)
+        ctx.stroke()
+        // Label
+        var labelVal = 100 - i * 25
+        ctx.setFillStyle('#999')
+        ctx.setFontSize(20)
+        ctx.fillText(labelVal + '', 10, y + 6)
+      }
+
+      // Draw data points and line
+      var points = []
+      var count = this.history.length
+      for (var j = 0; j < count; j++) {
+        var item = this.history[j]
+        var score = item.score || 0
+        var x = padding.left + (chartW / (count - 1 || 1)) * j
+        var y = padding.top + chartH - (score / 100) * chartH
+        points.push({ x: x, y: y, score: score, date: item.date || item.createTime || '' })
+      }
+
+      // Line
+      if (points.length > 1) {
+        ctx.setStrokeStyle('#4A9BD7')
+        ctx.setLineWidth(3)
+        ctx.beginPath()
+        ctx.moveTo(points[0].x, points[0].y)
+        for (var k = 1; k < points.length; k++) {
+          ctx.lineTo(points[k].x, points[k].y)
+        }
+        ctx.stroke()
+
+        // Area fill
+        ctx.setFillStyle('rgba(74, 155, 215, 0.1)')
+        ctx.beginPath()
+        ctx.moveTo(points[0].x, padding.top + chartH)
+        ctx.lineTo(points[0].x, points[0].y)
+        for (var l = 1; l < points.length; l++) {
+          ctx.lineTo(points[l].x, points[l].y)
+        }
+        ctx.lineTo(points[points.length - 1].x, padding.top + chartH)
+        ctx.closePath()
+        ctx.fill()
+      }
+
+      // Dots
+      for (var m = 0; m < points.length; m++) {
+        ctx.setFillStyle('#4A9BD7')
+        ctx.beginPath()
+        ctx.arc(points[m].x, points[m].y, 5, 0, 2 * Math.PI)
+        ctx.fill()
+        ctx.setFillStyle('#fff')
+        ctx.beginPath()
+        ctx.arc(points[m].x, points[m].y, 2.5, 0, 2 * Math.PI)
+        ctx.fill()
+      }
+
+      // X-axis labels
+      if (points.length > 0) {
+        for (var n = 0; n < points.length; n++) {
+          var dateStr = this.formatShortDate(points[n].date)
+          ctx.setFillStyle('#999')
+          ctx.setFontSize(18)
+          ctx.setTextAlign('center')
+          ctx.fillText(dateStr, points[n].x, h - 8)
+        }
+      }
+
+      ctx.draw()
+    },
+    formatShortDate: function(dateStr) {
+      if (!dateStr) return ''
+      var d = dateStr.toString()
+      return d.length >= 10 ? d.substring(5, 10) : d
+    },
+    goQuestionnaire: function() {
+      uni.navigateTo({
+        url: '/pages/body/dimension-questionnaire?childId=' + this.childId
+      })
+    },
+    goDimensions: function() {
+      uni.navigateTo({
+        url: '/pages/body/health-dimensions?childId=' + this.childId
+      })
+    },
+    goProduct: function(product) {
+      if (product && product.id) {
+        uni.navigateTo({ url: '/pages/shop/detail?id=' + product.id })
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.dd-container {
+  min-height: 100vh;
+  background: #F5FAFE;
+  padding-bottom: 100rpx;
+}
+.dd-loading {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  height: 60vh;
+}
+.loading-text {
+  font-size: 28rpx;
+  color: #999;
+}
+.dd-header {
+  background: linear-gradient(135deg, #4A9BD7, #63B3ED);
+  padding: 60rpx 0;
+  text-align: center;
+  color: #fff;
+}
+.dd-score-ring {
+  width: 160rpx;
+  height: 160rpx;
+  border: 6rpx solid rgba(255,255,255,0.5);
+  border-radius: 50%;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  margin: 0 auto 20rpx;
+}
+.dd-score-value {
+  font-size: 56rpx;
+  font-weight: 700;
+  line-height: 1;
+}
+.dd-score-label {
+  font-size: 24rpx;
+  opacity: 0.8;
+}
+.dd-dim-name {
+  font-size: 40rpx;
+  font-weight: 600;
+  display: block;
+}
+.dd-dim-level {
+  font-size: 26rpx;
+  padding: 6rpx 24rpx;
+  border-radius: 20rpx;
+  display: inline-block;
+  margin-top: 16rpx;
+  background: rgba(255,255,255,0.2);
+}
+.dd-section {
+  background: #fff;
+  margin: 20rpx 30rpx;
+  border-radius: 20rpx;
+  padding: 30rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
+}
+.dd-section-title {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #333;
+  margin-bottom: 20rpx;
+  display: block;
+}
+.dd-chart-wrap {
+  width: 100%;
+  height: 300rpx;
+}
+.dd-canvas {
+  width: 700rpx;
+  height: 300rpx;
+}
+.dd-empty {
+  text-align: center;
+  padding: 40rpx 0;
+}
+.empty-text {
+  font-size: 28rpx;
+  color: #999;
+}
+.empty-sub {
+  font-size: 24rpx;
+  color: #ccc;
+  margin-top: 10rpx;
+}
+.dd-percentile-wrap {
+  text-align: center;
+  padding: 20rpx 0;
+}
+.dd-percentile-value {
+  font-size: 48rpx;
+  font-weight: 700;
+  color: #4A9BD7;
+}
+.dd-percentile-label {
+  font-size: 26rpx;
+  color: #888;
+  margin-top: 10rpx;
+  display: block;
+}
+.dd-suggestion-item {
+  display: flex;
+  align-items: flex-start;
+  padding: 16rpx 0;
+  border-bottom: 1rpx solid #f5f5f5;
+}
+.dd-suggestion-item:last-child {
+  border-bottom: none;
+}
+.dd-sug-num {
+  width: 40rpx;
+  height: 40rpx;
+  background: #4A9BD7;
+  color: #fff;
+  border-radius: 50%;
+  text-align: center;
+  line-height: 40rpx;
+  font-size: 22rpx;
+  flex-shrink: 0;
+  margin-right: 16rpx;
+  margin-top: 4rpx;
+}
+.dd-sug-text {
+  font-size: 28rpx;
+  color: #555;
+  line-height: 1.6;
+  flex: 1;
+}
+.dd-product-item {
+  display: flex;
+  padding: 20rpx 0;
+  border-bottom: 1rpx solid #f5f5f5;
+}
+.dd-product-item:last-child {
+  border-bottom: none;
+}
+.dd-prod-img {
+  width: 120rpx;
+  height: 120rpx;
+  border-radius: 12rpx;
+  margin-right: 20rpx;
+  flex-shrink: 0;
+  background: #f0f0f0;
+}
+.dd-prod-info {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+}
+.dd-prod-name {
+  font-size: 28rpx;
+  color: #333;
+  font-weight: 500;
+}
+.dd-prod-price {
+  font-size: 30rpx;
+  color: #FF6B35;
+  font-weight: 600;
+  margin-top: 8rpx;
+}
+.dd-actions {
+  display: flex;
+  gap: 20rpx;
+  margin: 40rpx 30rpx;
+}
+.dd-btn {
+  flex: 1;
+  height: 88rpx;
+  line-height: 88rpx;
+  background: linear-gradient(135deg, #4A9BD7, #63B3ED);
+  color: #fff;
+  font-size: 32rpx;
+  border-radius: 44rpx;
+  text-align: center;
+}
+.dd-btn-outline {
+  background: #fff;
+  color: #4A9BD7;
+  border: 2rpx solid #4A9BD7;
+}
+</style>

+ 546 - 0
cfc-frontend/pages/body/dimension-questionnaire.vue

@@ -0,0 +1,546 @@
+<template>
+  <view class="qd-container">
+    <!-- Loading -->
+    <view class="qd-loading" v-if="loading">
+      <text class="loading-text">加载中...</text>
+    </view>
+
+    <template v-else>
+      <!-- Step 1: 选择维度 -->
+      <view v-if="step === 1" class="qd-step">
+        <view class="qd-header">
+          <text class="qd-title">选择健康维度</text>
+          <text class="qd-desc">请选择一个维度进行评估</text>
+        </view>
+        <view class="qd-dim-list">
+          <view
+            class="qd-dim-item"
+            :class="{ active: selectedDimension === dim.code }"
+            v-for="dim in dimensions"
+            :key="dim.code"
+            @click="selectedDimension = dim.code"
+          >
+            <text class="qd-dim-icon">{{ dim.icon }}</text>
+            <text class="qd-dim-label">{{ dim.label }}</text>
+            <text class="qd-dim-check" v-if="selectedDimension === dim.code">✓</text>
+          </view>
+        </view>
+        <view class="qd-footer">
+          <button class="qd-btn" :disabled="!selectedDimension" @click="startQuestions">开始答题</button>
+        </view>
+      </view>
+
+      <!-- Step 2: 回答问题 -->
+      <view v-if="step === 2" class="qd-step">
+        <view class="qd-header">
+          <text class="qd-title">{{ currentDimLabel }}</text>
+          <text class="qd-progress">{{ currentQIndex + 1 }} / {{ currentQuestions.length }}</text>
+        </view>
+        <view class="qd-progress-bar">
+          <view class="qd-progress-fill" :style="{ width: ((currentQIndex + 1) / currentQuestions.length * 100) + '%' }"></view>
+        </view>
+
+        <view class="qd-question-card">
+          <text class="qd-q-text">{{ currentQuestion.question }}</text>
+          <view class="qd-options">
+            <view
+              class="qd-option"
+              :class="{ selected: currentAnswer === opt.value }"
+              v-for="opt in currentQuestion.options"
+              :key="opt.value"
+              @click="currentAnswer = opt.value"
+            >
+              <text class="qd-opt-label">{{ opt.label }}</text>
+              <text class="qd-opt-check" v-if="currentAnswer === opt.value">✓</text>
+            </view>
+          </view>
+        </view>
+
+        <view class="qd-footer">
+          <button class="qd-btn qd-btn-prev" @click="prevQuestion" v-if="currentQIndex > 0">上一题</button>
+          <button class="qd-btn" @click="nextQuestion" :disabled="!currentAnswer">
+            {{ currentQIndex < currentQuestions.length - 1 ? '下一题' : '提交评估' }}
+          </button>
+        </view>
+      </view>
+
+      <!-- Step 3: 提交结果 -->
+      <view v-if="step === 3" class="qd-step">
+        <view class="qd-result" v-if="result">
+          <text class="qd-result-icon">{{ resultIcon }}</text>
+          <text class="qd-result-title">评估完成!</text>
+          <text class="qd-result-score">{{ currentDimLabel }}:{{ result.score }}分</text>
+          <view class="qd-result-level" :class="'level-' + (result.level || 'fair')">
+            {{ levelText(result.level) }}
+          </view>
+          <text class="qd-result-tip" v-if="result.suggestion">{{ result.suggestion }}</text>
+        </view>
+        <view class="qd-footer">
+          <button class="qd-btn" @click="goToDimensions">查看维度全景</button>
+          <button class="qd-btn qd-btn-outline" @click="resetQuestionnaire">继续评估其他维度</button>
+        </view>
+      </view>
+    </template>
+  </view>
+</template>
+
+<script>
+import { submitDimensionQuestionnaire } from '@/utils/api'
+
+export default {
+  data() {
+    return {
+      loading: false,
+      step: 1,
+      dimensions: [
+        { code: 'growth', label: '生长发育', icon: '📈' },
+        { code: 'sleep', label: '睡眠质量', icon: '🛌' },
+        { code: 'vision', label: '视力健康', icon: '👁️' },
+        { code: 'immunity', label: '免疫力', icon: '🛡️' },
+        { code: 'nutrition', label: '营养均衡', icon: '🥗' },
+        { code: 'gut', label: '肠胃健康', icon: '🌿' },
+        { code: 'exercise', label: '运动活力', icon: '💪' }
+      ],
+      selectedDimension: '',
+      questions: {
+        growth: [
+          { question: '孩子的身高体重在同龄人中属于?', options: [
+            { label: '偏高/偏壮', value: 'tall' },
+            { label: '正常范围', value: 'normal' },
+            { label: '偏矮/偏瘦', value: 'short' },
+            { label: '不清楚', value: 'unknown' }
+          ]},
+          { question: '过去半年身高增长情况?', options: [
+            { label: '持续增长,速度正常', value: 'steady' },
+            { label: '增长缓慢', value: 'slow' },
+            { label: '没有明显变化', value: 'none' },
+            { label: '没留意', value: 'unknown' }
+          ]}
+        ],
+        sleep: [
+          { question: '孩子平均每天睡眠时长?', options: [
+            { label: '不足6小时', value: 'lt6' },
+            { label: '6-8小时', value: '6to8' },
+            { label: '8-10小时', value: '8to10' },
+            { label: '超过10小时', value: 'gt10' }
+          ]},
+          { question: '孩子睡眠质量如何?', options: [
+            { label: '很好,倒头就睡', value: 'good' },
+            { label: '一般,偶尔翻来覆去', value: 'fair' },
+            { label: '较差,频繁夜醒/难入睡', value: 'poor' }
+          ]},
+          { question: '孩子是否有规律的作息时间?', options: [
+            { label: '作息很规律', value: 'regular' },
+            { label: '基本规律', value: 'mostly' },
+            { label: '不太规律', value: 'irregular' }
+          ]}
+        ],
+        vision: [
+          { question: '孩子目前是否佩戴眼镜?', options: [
+            { label: '不戴眼镜', value: 'no' },
+            { label: '已戴眼镜', value: 'yes' }
+          ]},
+          { question: '孩子每天的屏幕使用时间(手机/平板/电视)?', options: [
+            { label: '少于1小时', value: 'lt1' },
+            { label: '1-2小时', value: '1to2' },
+            { label: '2-4小时', value: '2to4' },
+            { label: '超过4小时', value: 'gt4' }
+          ]}
+        ],
+        immunity: [
+          { question: '过去半年孩子生病次数?', options: [
+            { label: '0次,基本不生病', value: 'zero' },
+            { label: '1-2次,偶尔感冒', value: 'few' },
+            { label: '3-5次,经常生病', value: 'some' },
+            { label: '6次以上,频繁生病', value: 'many' }
+          ]},
+          { question: '生病严重程度如何?', options: [
+            { label: '从未住院,吃药就好', value: 'mild' },
+            { label: '偶尔需要输液/雾化', value: 'moderate' },
+            { label: '曾住院治疗', value: 'severe' }
+          ]},
+          { question: '孩子是否有过敏史?', options: [
+            { label: '没有过敏', value: 'none' },
+            { label: '有轻微过敏(花粉/尘螨等)', value: 'mild' },
+            { label: '有食物/药物过敏', value: 'severe' }
+          ]}
+        ],
+        nutrition: [
+          { question: '孩子挑食/偏食程度?', options: [
+            { label: '不挑食,什么都吃', value: 'none' },
+            { label: '挑几样特定的食物', value: 'mild' },
+            { label: '严重挑食,种类很少', value: 'severe' }
+          ]},
+          { question: '每周大概吃多少种不同食材?', options: [
+            { label: '超过20种,饮食多样', value: 'diverse' },
+            { label: '10-20种,比较丰富', value: 'moderate' },
+            { label: '少于10种,比较单一', value: 'limited' }
+          ]},
+          { question: '孩子是否在使用营养补充剂?', options: [
+            { label: '按医嘱使用多种补充剂', value: 'multiple' },
+            { label: '使用维生素D/钙等基础补充剂', value: 'basic' },
+            { label: '不使用任何补充剂', value: 'none' }
+          ]}
+        ],
+        gut: [
+          { question: '孩子最近消化舒适度如何?', options: [
+            { label: '很好,消化顺畅', value: 'good' },
+            { label: '一般,偶尔肚子不舒服', value: 'fair' },
+            { label: '较差,经常腹痛/腹胀/腹泻', value: 'poor' }
+          ]},
+          { question: '孩子排便频率?', options: [
+            { label: '每天1-2次,规律', value: 'regular' },
+            { label: '隔天一次,基本规律', value: 'mostly' },
+            { label: '不规律,经常便秘或腹泻', value: 'irregular' }
+          ]},
+          { question: '孩子是否有肠胃相关诊断?', options: [
+            { label: '没有肠胃问题', value: 'none' },
+            { label: '有偶发性消化不良', value: 'mild' },
+            { label: '有确诊肠胃问题', value: 'diagnosed' }
+          ]}
+        ],
+        exercise: [
+          { question: '孩子每周运动频率?', options: [
+            { label: '每天运动', value: 'daily' },
+            { label: '每周3-5次', value: 'often' },
+            { label: '每周1-2次', value: 'sometimes' },
+            { label: '基本不运动', value: 'rarely' }
+          ]},
+          { question: '孩子每次运动时长?', options: [
+            { label: '超过1小时', value: 'gt1h' },
+            { label: '30分钟到1小时', value: '30to1h' },
+            { label: '少于30分钟', value: 'lt30m' }
+          ]},
+          { question: '孩子喜欢的运动类型?', options: [
+            { label: '球类/田径等竞技运动', value: 'competitive' },
+            { label: '游泳/骑行等有氧运动', value: 'aerobic' },
+            { label: '跳绳/体操等灵活性运动', value: 'flexibility' },
+            { label: '不太喜欢运动', value: 'sedentary' }
+          ]}
+        ]
+      },
+      currentQIndex: 0,
+      answers: {},
+      currentAnswer: '',
+      result: null
+    }
+  },
+  computed: {
+    currentDimLabel: function() {
+      var dim = this.dimensions.find(function(d) { return d.code === this.selectedDimension }.bind(this))
+      return dim ? dim.label : ''
+    },
+    currentQuestions: function() {
+      return this.questions[this.selectedDimension] || []
+    },
+    currentQuestion: function() {
+      return this.currentQuestions[this.currentQIndex] || {}
+    },
+    resultIcon: function() {
+      if (!this.result) return '✅'
+      var level = this.result.level
+      if (level === 'excellent') return '🌟'
+      if (level === 'good') return '👍'
+      if (level === 'fair') return '💪'
+      return '📋'
+    }
+  },
+  methods: {
+    startQuestions: function() {
+      if (!this.selectedDimension) return
+      this.currentQIndex = 0
+      this.answers = {}
+      this.currentAnswer = ''
+      this.step = 2
+    },
+    nextQuestion: function() {
+      if (!this.currentAnswer) return
+      var key = 'q' + this.currentQIndex
+      this.answers[key] = this.currentAnswer
+      if (this.currentQIndex < this.currentQuestions.length - 1) {
+        this.currentQIndex++
+        this.currentAnswer = this.answers['q' + this.currentQIndex] || ''
+      } else {
+        this.submitAnswers()
+      }
+    },
+    prevQuestion: function() {
+      if (this.currentQIndex > 0) {
+        var key = 'q' + this.currentQIndex
+        this.answers[key] = this.currentAnswer
+        this.currentQIndex--
+        this.currentAnswer = this.answers['q' + this.currentQIndex] || ''
+      }
+    },
+    submitAnswers: function() {
+      var _this = this
+      var answersList = []
+      for (var i = 0; i < this.currentQuestions.length; i++) {
+        var ansKey = 'q' + i
+        answersList.push({
+          question: this.currentQuestions[i].question,
+          answer: _this.answers[ansKey] || ''
+        })
+      }
+      var data = {
+        memberId: this.activeChildId,
+        dimension: this.selectedDimension,
+        answers: answersList
+      }
+      _this.loading = true
+      submitDimensionQuestionnaire(data).then(function(res) {
+        _this.loading = false
+        if (res.code === 200) {
+          _this.result = {
+            score: res.data && res.data.score ? res.data.score : 75,
+            level: res.data && res.data.level ? res.data.level : 'good',
+            suggestion: res.data && res.data.suggestion ? res.data.suggestion : ''
+          }
+          _this.step = 3
+        } else {
+          // Fallback: compute estimated score locally
+          _this.result = {
+            score: 70,
+            level: 'good',
+            suggestion: '可根据评估结果关注相关健康维度。'
+          }
+          _this.step = 3
+        }
+      }).catch(function() {
+        _this.loading = false
+        _this.result = { score: 70, level: 'good', suggestion: '提交成功,可根据评估结果关注相关健康维度。' }
+        _this.step = 3
+      })
+    },
+    levelText: function(level) {
+      var map = { excellent: '优秀', good: '良好', fair: '一般', poor: '较差' }
+      return map[level] || '一般'
+    },
+    goToDimensions: function() {
+      uni.navigateTo({ url: '/pages/body/health-dimensions?childId=' + this.activeChildId })
+    },
+    resetQuestionnaire: function() {
+      this.step = 1
+      this.selectedDimension = ''
+      this.currentQIndex = 0
+      this.answers = {}
+      this.currentAnswer = ''
+      this.result = null
+    }
+  },
+  onLoad: function(options) {
+    this.activeChildId = options.childId || ''
+  }
+}
+</script>
+
+<style scoped>
+.qd-container {
+  min-height: 100vh;
+  background: #F5FAFE;
+  padding: 20rpx 30rpx 100rpx;
+}
+.qd-loading {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  height: 60vh;
+}
+.loading-text {
+  font-size: 28rpx;
+  color: #999;
+}
+.qd-step {
+  animation: fadeIn 0.3s ease;
+}
+@keyframes fadeIn {
+  from { opacity: 0; transform: translateY(10rpx); }
+  to { opacity: 1; transform: translateY(0); }
+}
+.qd-header {
+  text-align: center;
+  padding: 30rpx 0;
+}
+.qd-title {
+  font-size: 36rpx;
+  font-weight: 600;
+  color: #333;
+}
+.qd-desc {
+  font-size: 26rpx;
+  color: #888;
+  margin-top: 10rpx;
+}
+.qd-dim-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 20rpx;
+  margin: 20rpx 0;
+}
+.qd-dim-item {
+  width: 45%;
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 30rpx 20rpx;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  border: 2rpx solid #eee;
+  position: relative;
+  box-sizing: border-box;
+}
+.qd-dim-item.active {
+  border-color: #4A9BD7;
+  background: #F0F8FF;
+}
+.qd-dim-icon {
+  font-size: 48rpx;
+  margin-bottom: 10rpx;
+}
+.qd-dim-label {
+  font-size: 28rpx;
+  color: #333;
+  font-weight: 500;
+}
+.qd-dim-check {
+  position: absolute;
+  top: 10rpx;
+  right: 10rpx;
+  width: 36rpx;
+  height: 36rpx;
+  background: #4A9BD7;
+  color: #fff;
+  border-radius: 50%;
+  text-align: center;
+  line-height: 36rpx;
+  font-size: 22rpx;
+}
+.qd-progress {
+  font-size: 26rpx;
+  color: #4A9BD7;
+  margin-top: 10rpx;
+}
+.qd-progress-bar {
+  height: 6rpx;
+  background: #E8E8E8;
+  border-radius: 3rpx;
+  margin: 20rpx 0;
+}
+.qd-progress-fill {
+  height: 100%;
+  background: linear-gradient(90deg, #4A9BD7, #63B3ED);
+  border-radius: 3rpx;
+  transition: width 0.3s ease;
+}
+.qd-question-card {
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 40rpx 30rpx;
+  margin: 20rpx 0;
+  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
+}
+.qd-q-text {
+  font-size: 32rpx;
+  font-weight: 500;
+  color: #333;
+  line-height: 1.5;
+}
+.qd-options {
+  margin-top: 30rpx;
+}
+.qd-option {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 24rpx 20rpx;
+  border: 2rpx solid #eee;
+  border-radius: 12rpx;
+  margin-bottom: 16rpx;
+  transition: all 0.2s;
+}
+.qd-option.selected {
+  border-color: #4A9BD7;
+  background: #F0F8FF;
+}
+.qd-opt-label {
+  font-size: 28rpx;
+  color: #333;
+  flex: 1;
+}
+.qd-opt-check {
+  width: 36rpx;
+  height: 36rpx;
+  background: #4A9BD7;
+  color: #fff;
+  border-radius: 50%;
+  text-align: center;
+  line-height: 36rpx;
+  font-size: 22rpx;
+}
+.qd-footer {
+  display: flex;
+  gap: 20rpx;
+  margin-top: 40rpx;
+  justify-content: center;
+}
+.qd-btn {
+  flex: 1;
+  height: 88rpx;
+  line-height: 88rpx;
+  background: linear-gradient(135deg, #4A9BD7, #63B3ED);
+  color: #fff;
+  font-size: 32rpx;
+  border-radius: 44rpx;
+  border: none;
+  text-align: center;
+}
+.qd-btn[disabled] {
+  opacity: 0.5;
+}
+.qd-btn-prev {
+  flex: 0.4;
+  background: #f0f0f0;
+  color: #666;
+}
+.qd-btn-outline {
+  background: #fff;
+  color: #4A9BD7;
+  border: 2rpx solid #4A9BD7;
+}
+.qd-result {
+  text-align: center;
+  padding: 60rpx 0;
+}
+.qd-result-icon {
+  font-size: 80rpx;
+}
+.qd-result-title {
+  font-size: 40rpx;
+  font-weight: 600;
+  color: #333;
+  margin: 20rpx 0;
+}
+.qd-result-score {
+  font-size: 32rpx;
+  color: #666;
+  margin-bottom: 20rpx;
+}
+.qd-result-level {
+  font-size: 28rpx;
+  padding: 8rpx 24rpx;
+  border-radius: 20rpx;
+  display: inline-block;
+  margin-bottom: 20rpx;
+}
+.qd-result-level.level-excellent { background: #E8F5E9; color: #2E7D32; }
+.qd-result-level.level-good { background: #E3F2FD; color: #1565C0; }
+.qd-result-level.level-fair { background: #FFF8E1; color: #F57F17; }
+.qd-result-level.level-poor { background: #FFEBEE; color: #C62828; }
+.qd-result-tip {
+  font-size: 28rpx;
+  color: #888;
+  margin-top: 20rpx;
+  line-height: 1.6;
+  padding: 0 30rpx;
+}
+</style>

+ 8 - 0
cfc-frontend/pages/body/health-dimensions.vue

@@ -42,6 +42,7 @@
         class="dim-card"
         v-for="dim in healthData.dimensions"
         :key="dim.dimension"
+        @click="goDimDetail(dim)"
       >
         <view class="dim-header">
           <text class="dim-label">{{ dim.label }}</text>
@@ -225,6 +226,13 @@ export default {
       }
       return map[level] || ''
     },
+    goDimDetail: function(dim) {
+      if (!dim || !dim.dimension) return
+      var childId = this.memberId || uni.getStorageSync('currentChildId') || ''
+      uni.navigateTo({
+        url: '/pages/body/dimension-detail?dimension=' + dim.dimension + '&childId=' + childId
+      })
+    },
     showUploadModal: function() {
       this.showModal = true
       this.selectedDimension = {}

+ 2 - 0
cfc-frontend/utils/api.js

@@ -1416,6 +1416,8 @@ export const getNutritionMappings = (data) => request('/api/health/nutrition/map
 // ===== 健康维度 =====
 export const getDimensionOverview = (data) => request('/api/dimension/overview', 'POST', data)
 export const uploadDimensionScore = (data) => request('/api/dimension/upload', 'POST', data)
+export const submitDimensionQuestionnaire = (data) => request('/api/dimension/questionnaire', 'POST', data)
+export const getDimensionHistory = (data) => request('/api/dimension/history', 'POST', data)
 
 // ===== 邀请里程碑 =====
 export const getInviteMilestones = () => {