Kaynağa Gözat

fix: 行首页登录后保持TabBar;家庭挑战任务移至身页面

liaoxg 1 ay önce
ebeveyn
işleme
c715f356d7

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

@@ -1,520 +0,0 @@
-<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>

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

@@ -1,546 +0,0 @@
-<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>

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

@@ -1,620 +0,0 @@
-<template>
-  <view class="hd-container">
-    <view class="hd-header">
-      <text class="hd-title">健康维度全景</text>
-      <text class="hd-subtitle" v-if="memberName">{{ memberName }}的健康档案</text>
-    </view>
-
-    <view class="hd-health-index" v-if="healthData && healthData.healthIndex">
-      <view class="index-circle">
-        <text class="index-value">{{ healthData.healthIndex }}</text>
-        <text class="index-label">健康指数</text>
-      </view>
-    </view>
-
-    <view class="radar-section" v-if="healthData && healthData.dimensions">
-      <view class="radar-title">七维健康雷达</view>
-      <view class="radar-wrap">
-        <view class="radar-center">
-          <view class="radar-grid">
-            <view class="grid-ring ring-3"></view>
-            <view class="grid-ring ring-2"></view>
-            <view class="grid-ring ring-1"></view>
-          </view>
-          <view class="radar-spokes">
-            <view
-              class="spoke-item"
-              v-for="(dim, idx) in healthData.dimensions"
-              :key="dim.dimension"
-              :style="spokeStyles[idx]"
-            >
-              <view class="spoke-dot" :class="'level-' + dim.level"></view>
-              <view class="spoke-label">{{ dim.label }}</view>
-              <view class="spoke-score">{{ dim.score || '--' }}</view>
-            </view>
-          </view>
-        </view>
-      </view>
-    </view>
-
-    <view class="dimension-list" v-if="healthData && healthData.dimensions">
-      <view
-        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>
-          <text class="dim-score" :class="'score-' + dim.level">{{ dim.score || '--' }}分</text>
-        </view>
-        <view class="dim-bar-wrap">
-          <view class="dim-bar-bg">
-            <view
-              class="dim-bar-fill"
-              :class="'fill-' + dim.level"
-              :style="{ width: (dim.score || 0) + '%' }"
-            ></view>
-          </view>
-          <text class="dim-norms" v-if="dim.normMin && dim.normMax">
-            正常范围 {{ dim.normMin }}-{{ dim.normMax }}
-          </text>
-        </view>
-        <view class="dim-footer">
-          <text class="dim-level-tag" :class="'tag-' + dim.level">{{ getLevelText(dim.level) }}</text>
-          <text class="dim-source" v-if="dim.trend">{{ dim.trend === 'up' ? '↑ 上升' : dim.trend === 'down' ? '↓ 下降' : '→ 稳定' }}</text>
-        </view>
-      </view>
-    </view>
-
-    <view class="hd-empty" v-if="!healthData && !loading">
-      <text class="empty-tip">暂无维度数据</text>
-      <text class="empty-sub">上传健康报告或填写问卷后可查看</text>
-    </view>
-
-    <view class="loading-wrap" v-if="loading">
-      <text class="loading-text">加载中...</text>
-    </view>
-
-    <view class="fab-upload" @click="showUploadModal">
-      <text class="fab-icon">+</text>
-    </view>
-
-    <view class="modal-mask" v-if="showModal" @click="showModal = false">
-      <view class="modal-content" @click.stop>
-        <view class="modal-header">
-          <text class="modal-title">录入维度评分</text>
-          <text class="modal-close" @click="showModal = false">×</text>
-        </view>
-        <view class="modal-body">
-          <view class="form-item">
-            <text class="form-label">选择维度</text>
-            <picker
-              mode="selector"
-              :range="dimensionOptions"
-              range-key="label"
-              @change="onDimensionChange"
-            >
-              <view class="picker-value">
-                {{ selectedDimension.label || '请选择' }}
-              </view>
-            </picker>
-          </view>
-          <view class="form-item">
-            <text class="form-label">评分 (0-100)</text>
-            <input
-              class="form-input"
-              type="number"
-              v-model="uploadForm.score"
-              placeholder="请输入评分"
-              min="0"
-              max="100"
-            />
-          </view>
-          <view class="form-item">
-            <text class="form-label">数据来源</text>
-            <picker
-              mode="selector"
-              :range="sourceOptions"
-              @change="onSourceChange"
-            >
-              <view class="picker-value">
-                {{ selectedSource.label || '请选择' }}
-              </view>
-            </picker>
-          </view>
-        </view>
-        <view class="modal-footer">
-          <view class="btn-cancel" @click="showModal = false">取消</view>
-          <view class="btn-confirm" @click="submitUpload">确认提交</view>
-        </view>
-      </view>
-    </view>
-  </view>
-</template>
-
-<script>
-import { getDimensionOverview, uploadDimensionScore } from '../../utils/api.js'
-
-export default {
-  data() {
-    return {
-      memberId: null,
-      memberName: '',
-      healthData: null,
-      loading: false,
-      showModal: false,
-      selectedDimension: {},
-      selectedSource: {},
-      uploadForm: {
-        memberId: null,
-        dimension: '',
-        score: '',
-        dataSource: ''
-      },
-      dimensionOptions: [
-        { code: 'growth', label: '生长发育' },
-        { code: 'sleep', label: '睡眠质量' },
-        { code: 'vision', label: '视力健康' },
-        { code: 'immunity', label: '免疫力' },
-        { code: 'nutrition', label: '营养均衡' },
-        { code: 'gut', label: '肠胃健康' },
-        { code: 'exercise', label: '运动活力' }
-      ],
-      sourceOptions: [
-        { code: 'MANUAL', label: '手动录入' },
-        { code: 'REPORT', label: '健康报告' },
-        { code: 'PHOTO', label: '照片' },
-        { code: 'VOICE', label: '语音' }
-      ]
-    }
-  },
-  computed: {
-    spokeStyles: function() {
-      if (!this.healthData || !this.healthData.dimensions) return []
-      var cache = []
-      var dims = this.healthData.dimensions
-      for (var i = 0; i < dims.length; i++) {
-        var angle = i * 51.4 - 90
-        var radius = 85
-        var normalizedScore = (dims[i].score || 0) / 100
-        var translateY = -radius * normalizedScore
-        cache.push({
-          transform: 'rotate(' + angle + 'deg) translateY(' + translateY + 'px)',
-          '-webkit-transform': 'rotate(' + angle + 'deg) translateY(' + translateY + 'px)'
-        })
-      }
-      return cache
-    }
-  },
-  onLoad: function(options) {
-    var childId = options.childId || uni.getStorageSync('currentChildId')
-    var childName = options.name || ''
-    this.memberId = Number(childId)
-    this.memberName = childName
-    this.uploadForm.memberId = this.memberId
-    if (this.memberId) {
-      this.loadDimensionData()
-    }
-  },
-  onShow: function() {
-    if (this.memberId) {
-      this.loadDimensionData()
-    }
-  },
-  methods: {
-    loadDimensionData: function() {
-      var self = this
-      self.loading = true
-      getDimensionOverview({ memberId: this.memberId }).then(function(res) {
-        self.loading = false
-        if (res.code === 200 && res.data) {
-          self.healthData = res.data
-        }
-      }).catch(function(e) {
-        self.loading = false
-        console.log('获取维度数据失败', e)
-      })
-    },
-    getLevelText: function(level) {
-      var map = {
-        'excellent': '优秀',
-        'good': '良好',
-        'fair': '一般',
-        'poor': '较差',
-        'none': '暂无数据'
-      }
-      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 = {}
-      this.selectedSource = {}
-      this.uploadForm.dimension = ''
-      this.uploadForm.score = ''
-      this.uploadForm.dataSource = ''
-    },
-    onDimensionChange: function(e) {
-      var idx = e.detail.value
-      this.selectedDimension = this.dimensionOptions[idx]
-      this.uploadForm.dimension = this.selectedDimension.code
-    },
-    onSourceChange: function(e) {
-      var idx = e.detail.value
-      this.selectedSource = this.sourceOptions[idx]
-      this.uploadForm.dataSource = this.selectedSource.code
-    },
-    submitUpload: function() {
-      if (!this.uploadForm.dimension) {
-        uni.showToast({ title: '请选择维度', icon: 'none' })
-        return
-      }
-      if (!this.uploadForm.score || this.uploadForm.score < 0 || this.uploadForm.score > 100) {
-        uni.showToast({ title: '评分需在0-100之间', icon: 'none' })
-        return
-      }
-      if (!this.uploadForm.dataSource) {
-        uni.showToast({ title: '请选择数据来源', icon: 'none' })
-        return
-      }
-      var self = this
-      var payload = {
-        memberId: this.memberId,
-        dimension: this.uploadForm.dimension,
-        score: Number(this.uploadForm.score),
-        sourceType: this.uploadForm.dataSource
-      }
-      uploadDimensionScore(payload).then(function(res) {
-        if (res.code === 200) {
-          uni.showToast({ title: '提交成功', icon: 'success' })
-          self.showModal = false
-          self.loadDimensionData()
-        }
-      }).catch(function(e) {
-        console.log('提交失败', e)
-      })
-    }
-  }
-}
-</script>
-
-<style scoped>
-.hd-container {
-  min-height: 100vh;
-  background: #f5f5f5;
-  padding-bottom: 120rpx;
-}
-.hd-header {
-  background: linear-gradient(135deg, #FF8C42, #FF6B35);
-  padding: 40rpx 30rpx 30rpx;
-  color: #fff;
-}
-.hd-title {
-  font-size: 40rpx;
-  font-weight: bold;
-  display: block;
-}
-.hd-subtitle {
-  font-size: 26rpx;
-  opacity: 0.9;
-  display: block;
-  margin-top: 8rpx;
-}
-.hd-health-index {
-  display: flex;
-  justify-content: center;
-  padding: 30rpx;
-  background: #fff;
-}
-.index-circle {
-  width: 160rpx;
-  height: 160rpx;
-  border-radius: 50%;
-  background: linear-gradient(135deg, #FF8C42, #FF6B35);
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
-  color: #fff;
-}
-.index-value {
-  font-size: 48rpx;
-  font-weight: bold;
-  line-height: 1;
-}
-.index-label {
-  font-size: 22rpx;
-  opacity: 0.9;
-  margin-top: 8rpx;
-}
-.radar-section {
-  background: #fff;
-  margin-top: 20rpx;
-  padding: 30rpx;
-}
-.radar-title {
-  font-size: 32rpx;
-  font-weight: bold;
-  color: #333;
-  margin-bottom: 30rpx;
-  text-align: center;
-}
-.radar-wrap {
-  display: flex;
-  justify-content: center;
-}
-.radar-center {
-  width: 440rpx;
-  height: 440rpx;
-  position: relative;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-}
-.radar-grid {
-  position: absolute;
-  width: 100%;
-  height: 100%;
-}
-.grid-ring {
-  position: absolute;
-  border-radius: 50%;
-  border: 1px solid #ddd;
-  left: 50%;
-  top: 50%;
-  transform: translate(-50%, -50%);
-}
-.ring-1 { width: 33%; height: 33%; }
-.ring-2 { width: 66%; height: 66%; }
-.ring-3 { width: 100%; height: 100%; }
-.radar-spokes {
-  position: absolute;
-  width: 100%;
-  height: 100%;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-}
-.spoke-item {
-  position: absolute;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  transform-origin: center center;
-  width: 80rpx;
-  margin-left: -40rpx;
-  top: 10rpx;
-}
-.spoke-dot {
-  width: 20rpx;
-  height: 20rpx;
-  border-radius: 50%;
-  background: #FF8C42;
-  margin-bottom: 6rpx;
-}
-.spoke-dot.level-excellent { background: #10B981; }
-.spoke-dot.level-good { background: #3B82F6; }
-.spoke-dot.level-fair { background: #F59E0B; }
-.spoke-dot.level-poor { background: #EF4444; }
-.spoke-dot.level-none { background: #ddd; }
-.spoke-label {
-  font-size: 22rpx;
-  color: #333;
-  white-space: nowrap;
-}
-.spoke-score {
-  font-size: 20rpx;
-  color: #666;
-  margin-top: 2rpx;
-}
-.dimension-list {
-  padding: 20rpx 30rpx;
-}
-.dim-card {
-  background: #fff;
-  border-radius: 16rpx;
-  padding: 24rpx;
-  margin-bottom: 20rpx;
-}
-.dim-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 16rpx;
-}
-.dim-label {
-  font-size: 30rpx;
-  font-weight: bold;
-  color: #333;
-}
-.dim-score {
-  font-size: 36rpx;
-  font-weight: bold;
-  color: #FF8C42;
-}
-.dim-score.score-excellent { color: #10B981; }
-.dim-score.score-good { color: #3B82F6; }
-.dim-score.score-fair { color: #F59E0B; }
-.dim-score.score-poor { color: #EF4444; }
-.dim-bar-wrap {
-  margin-bottom: 12rpx;
-}
-.dim-bar-bg {
-  height: 16rpx;
-  background: #f0f0f0;
-  border-radius: 8rpx;
-  overflow: hidden;
-  margin-bottom: 6rpx;
-}
-.dim-bar-fill {
-  height: 100%;
-  border-radius: 8rpx;
-  background: #FF8C42;
-  transition: width 0.3s;
-}
-.dim-bar-fill.fill-excellent { background: #10B981; }
-.dim-bar-fill.fill-good { background: #3B82F6; }
-.dim-bar-fill.fill-fair { background: #F59E0B; }
-.dim-bar-fill.fill-poor { background: #EF4444; }
-.dim-norms {
-  font-size: 22rpx;
-  color: #999;
-}
-.dim-footer {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-}
-.dim-level-tag {
-  font-size: 22rpx;
-  padding: 4rpx 16rpx;
-  border-radius: 20rpx;
-  background: #f0f0f0;
-  color: #666;
-}
-.dim-level-tag.tag-excellent { background: #D1FAE5; color: #10B981; }
-.dim-level-tag.tag-good { background: #DBEAFE; color: #3B82F6; }
-.dim-level-tag.tag-fair { background: #FEF3C7; color: #F59E0B; }
-.dim-level-tag.tag-poor { background: #FEE2E2; color: #EF4444; }
-.dim-source {
-  font-size: 22rpx;
-  color: #999;
-}
-.hd-empty {
-  padding: 100rpx 30rpx;
-  text-align: center;
-}
-.empty-tip {
-  font-size: 30rpx;
-  color: #999;
-  display: block;
-  margin-bottom: 16rpx;
-}
-.empty-sub {
-  font-size: 24rpx;
-  color: #ccc;
-  display: block;
-}
-.loading-wrap {
-  padding: 80rpx 30rpx;
-  text-align: center;
-}
-.loading-text {
-  font-size: 28rpx;
-  color: #999;
-}
-.fab-upload {
-  position: fixed;
-  right: 40rpx;
-  bottom: 60rpx;
-  width: 100rpx;
-  height: 100rpx;
-  border-radius: 50%;
-  background: linear-gradient(135deg, #FF8C42, #FF6B35);
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  box-shadow: 0 8rpx 24rpx rgba(255, 140, 66, 0.4);
-}
-.fab-icon {
-  font-size: 48rpx;
-  color: #fff;
-  font-weight: bold;
-  line-height: 1;
-}
-.modal-mask {
-  position: fixed;
-  top: 0;
-  left: 0;
-  right: 0;
-  bottom: 0;
-  background: rgba(0,0,0,0.5);
-  z-index: 999;
-  display: flex;
-  align-items: flex-end;
-  justify-content: center;
-}
-.modal-content {
-  width: 100%;
-  background: #fff;
-  border-radius: 24rpx 24rpx 0 0;
-  padding-bottom: 60rpx;
-}
-.modal-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  padding: 30rpx;
-  border-bottom: 1px solid #f0f0f0;
-}
-.modal-title {
-  font-size: 32rpx;
-  font-weight: bold;
-  color: #333;
-}
-.modal-close {
-  font-size: 48rpx;
-  color: #999;
-  line-height: 1;
-}
-.modal-body {
-  padding: 30rpx;
-}
-.form-item {
-  margin-bottom: 30rpx;
-}
-.form-label {
-  font-size: 28rpx;
-  color: #666;
-  display: block;
-  margin-bottom: 12rpx;
-}
-.picker-value {
-  height: 80rpx;
-  line-height: 80rpx;
-  border: 1px solid #e0e0e0;
-  border-radius: 12rpx;
-  padding: 0 20rpx;
-  font-size: 28rpx;
-  color: #333;
-}
-.form-input {
-  height: 80rpx;
-  border: 1px solid #e0e0e0;
-  border-radius: 12rpx;
-  padding: 0 20rpx;
-  font-size: 28rpx;
-}
-.modal-footer {
-  display: flex;
-  padding: 0 30rpx;
-  gap: 20rpx;
-}
-.btn-cancel {
-  flex: 1;
-  height: 88rpx;
-  line-height: 88rpx;
-  text-align: center;
-  background: #f0f0f0;
-  border-radius: 44rpx;
-  font-size: 30rpx;
-  color: #666;
-}
-.btn-confirm {
-  flex: 1;
-  height: 88rpx;
-  line-height: 88rpx;
-  text-align: center;
-  background: linear-gradient(135deg, #FF8C42, #FF6B35);
-  border-radius: 44rpx;
-  font-size: 30rpx;
-  color: #fff;
-}
-</style>

+ 29 - 3
cfc-frontend/pages/body/index.vue

@@ -49,6 +49,17 @@
       v-if="isLoggedIn && activeChildId"
       :childId="activeChildId" />
 
+    <!-- 家庭挑战任务 -->
+    <view class="section challenge-section" v-if="isLoggedIn && activeChallenges.length > 0">
+      <view class="section-header">
+        <text class="section-title">🔥 家庭挑战</text>
+      </view>
+      <FamilyChallengeCard
+        v-for="ch in activeChallenges"
+        :key="ch.id"
+        :challenge="ch" />
+    </view>
+
       <!-- 雷达图 -->
       <view class="section" v-if="dimensionData">
         <view class="section-header" v-if="dimensionData.isAverage">
@@ -169,11 +180,12 @@ import HealthDimensionsSection from '../../components/HealthDimensionsSection.vu
 import HealthCheckinCard from '../../components/HealthCheckinCard.vue'
 import AIFloatingAvatar from '../../components/AIFloatingAvatar.vue'
 import FamilyMemberStrip from '../../components/FamilyMemberStrip.vue'
+import FamilyChallengeCard from '../../components/FamilyChallengeCard.vue'
 
-import { getVisibleSections, getEnergyOverview, getChildren, getTodayTasksByCategory, getActivityList, getProductsByDomain, getFamilyEnergySandbox, getVisibleFamilyMembers, getDimensionOverview, getFeaturedArticles } from '../../utils/api.js'
+import { getVisibleSections, getEnergyOverview, getChildren, getTodayTasksByCategory, getActivityList, getProductsByDomain, getFamilyEnergySandbox, getVisibleFamilyMembers, getDimensionOverview, getFeaturedArticles, getChallengeList } from '../../utils/api.js'
 
 export default {
-  components: { TabTransition, PageBanner, LoginGuideCard, FamilyEnergyBar, DimensionTasks, DimensionActivities, DimensionProducts, DimensionProductList, DimensionArticles, FamilyRelationGraph, RadarChart, HealthDimensionsSection, HealthCheckinCard, AIFloatingAvatar, FamilyMemberStrip },
+  components: { TabTransition, PageBanner, LoginGuideCard, FamilyEnergyBar, DimensionTasks, DimensionActivities, DimensionProducts, DimensionProductList, DimensionArticles, FamilyRelationGraph, RadarChart, HealthDimensionsSection, HealthCheckinCard, AIFloatingAvatar, FamilyMemberStrip, FamilyChallengeCard },
   data() {
     return {
       isLoggedIn: false,
@@ -194,6 +206,7 @@ export default {
       dimensionProducts: [],
       bodyArticles: [],
       dimensionData: null,
+      activeChallenges: [],
       /** Mock 平均值(7维):生长发育/睡眠质量/视力健康/免疫力/营养均衡/肠胃健康/运动活力 */
       radarAvgScores: [72, 68, 75, 70, 73, 69, 71],
       funcList: [
@@ -264,6 +277,7 @@ export default {
     if (this.isLoggedIn) {
       this.loadChildren()
       this.loadFamilyMembersVisible()
+      this.loadChallenges()
     }
     // 游客也能浏览活动和商品
     this.loadDimensionActivities()
@@ -445,6 +459,18 @@ export default {
         self.familyMembersVisible = []
       })
     },
+    loadChallenges: function() {
+      var familyId = uni.getStorageSync('familyId')
+      if (!familyId) return
+      var self = this
+      getChallengeList({ familyId: familyId }).then(function(res) {
+        if (res.code === 200 && Array.isArray(res.data)) {
+          self.activeChallenges = res.data
+        }
+      }).catch(function() {
+        self.activeChallenges = []
+      })
+    },
     scrollToSection: function(section) {
       uni.pageScrollTo({ selector: '#' + section, duration: 300 })
     },
@@ -506,7 +532,7 @@ export default {
       if (item.page) uni.navigateTo({ url: item.page })
     },
     goDimensionDetail: function(dimKey) {
-      uni.navigateTo({ url: '/pages/body/dimension-detail?dimension=' + dimKey + '&childId=' + this.activeChildId })
+      uni.navigateTo({ url: '/pages/body-detail/dimension-detail?dimension=' + dimKey + '&childId=' + this.activeChildId })
     },
     goToCheckin: function() {
       var id = this.activeChildId || uni.getStorageSync('currentChildId')

+ 17 - 12
cfc-frontend/pages/index-home/index.vue

@@ -47,8 +47,8 @@
   <!-- Tabbar 切换过渡页 -->
   <tab-transition v-if="showTabTransition" dimCode="action" />
 
-  <!-- 未登录态 — 发现页/引流内容,直接内嵌,不跳转 -->
-  <view v-if="!currentRole" class="discover-container">
+  <!-- 发现页/引流内容,登录后也保留 TabBar -->
+  <view class="discover-container">
     <!-- 品牌头部 -->
     <PageBanner theme="home"
       tagline="家庭健康成长俱乐部"
@@ -201,17 +201,16 @@
       </view>
     </view>
 
-    <!-- 底部登录按钮 -->
+    <!-- 底部按钮:未登录显示登录,已登录显示进入首页 -->
     <view class="login-footer">
-      <button class="login-btn" @click="handleLogin">登录 / 注册</button>
-      <text class="login-footer-text">登录即表示同意《用户协议》和《隐私政策》</text>
+      <button class="login-btn" v-if="!isLoggedIn()" @click="handleLogin">登录 / 注册</button>
+      <button class="login-btn home-btn" v-else @click="goHomeDashboard">进入家庭首页</button>
+      <text class="login-footer-text" v-if="!isLoggedIn()">登录即表示同意《用户协议》和《隐私政策》</text>
     </view>
 
     <!-- 底部占位 -->
     <view style="height: 120rpx;"></view>
   </view>
-
-  <!-- 登录态 — 已迁移到 home 分包,通过 redirectTo 跳转,不再静态渲染 -->
 </view>
 </template>
 
@@ -385,11 +384,11 @@ export default {
         uni.removeStorageSync('inviteType')
         return
       }
-      // 登录用户:跳转到 home 分包页面,不再静态渲染组件
-      var targetUrl = this.currentRole === 'child'
-        ? '/pages/home-pages/child-index'
-        : '/pages/home-pages/parent-index'
-      uni.redirectTo({ url: targetUrl })
+      // 登录用户:留在 TabBar 页,显示推荐内容
+      // 不再 redirectTo 到 home 分包(分包页无 TabBar)
+      this.loadProducts()
+      this.loadDimensionActivities()
+      this.loadFeaturedArticles()
     },
     isLoggedIn() {
       return !!uni.getStorageSync('token')
@@ -511,6 +510,12 @@ export default {
     handleLogin() {
       uni.navigateTo({ url: '/pages/login/login?redirect=' + encodeURIComponent('/pages/index-home/index') })
     },
+    goHomeDashboard() {
+      var targetUrl = this.currentRole === 'child'
+        ? '/pages/home-pages/child-index'
+        : '/pages/home-pages/parent-index'
+      uni.navigateTo({ url: targetUrl })
+    },
     async handleParentInvite(code) {
       // 延迟显示,等待登录完成
       setTimeout(async () => {