Sfoglia il codice sorgente

refactor: 子包重组 - 详情页移至 *-detail 目录降低主包大小

- 将 body/mind/wisdom/action/discover 的详情页移至对应的 *-detail 子包
- 主包从 54 页减少至 21 页,符合微信 2MB 限制
- 更新 pages.json 配置匹配新的目录结构
- 更新 splash 页面使用项目 logo
- 修复 report-survey.vue 语法错误
User 4 mesi fa
parent
commit
9bbdafa64f
32 ha cambiato i file con 11320 aggiunte e 41 eliminazioni
  1. 5 5
      cfc-frontend/pages.json
  2. 387 0
      cfc-frontend/pages/action-detail/contact-detail.vue
  3. 283 0
      cfc-frontend/pages/action-detail/interaction-log.vue
  4. 308 0
      cfc-frontend/pages/action-detail/member-action-detail.vue
  5. 372 0
      cfc-frontend/pages/action-detail/relationship-questionnaire.vue
  6. 8 8
      cfc-frontend/pages/action/index.vue
  7. 43 5
      cfc-frontend/pages/ai/chat.vue
  8. 517 0
      cfc-frontend/pages/body-detail/checkin.vue
  9. 520 0
      cfc-frontend/pages/body-detail/dimension-detail.vue
  10. 546 0
      cfc-frontend/pages/body-detail/dimension-questionnaire.vue
  11. 620 0
      cfc-frontend/pages/body-detail/health-dimensions.vue
  12. 1009 0
      cfc-frontend/pages/body-detail/health-report.vue
  13. 302 0
      cfc-frontend/pages/body-detail/member-body-detail.vue
  14. 6 6
      cfc-frontend/pages/body/index.vue
  15. 371 0
      cfc-frontend/pages/discover-detail/activity-detail/activity-detail.vue
  16. 327 0
      cfc-frontend/pages/discover-detail/product-detail/product-detail.vue
  17. 0 1
      cfc-frontend/pages/health/report-survey.vue
  18. 401 0
      cfc-frontend/pages/mind-detail/article-detail.vue
  19. 348 0
      cfc-frontend/pages/mind-detail/articles.vue
  20. 679 0
      cfc-frontend/pages/mind-detail/emotion-checkin.vue
  21. 422 0
      cfc-frontend/pages/mind-detail/emotion-report.vue
  22. 441 0
      cfc-frontend/pages/mind-detail/emotion-trend.vue
  23. 308 0
      cfc-frontend/pages/mind-detail/member-mind-detail.vue
  24. 396 0
      cfc-frontend/pages/mind-detail/soothe-toolbox.vue
  25. 398 0
      cfc-frontend/pages/mind-detail/traditional-mirror.vue
  26. 514 0
      cfc-frontend/pages/mind-detail/training.vue
  27. 8 8
      cfc-frontend/pages/mind/index.vue
  28. 407 0
      cfc-frontend/pages/wisdom-detail/cognitive-report.vue
  29. 267 0
      cfc-frontend/pages/wisdom-detail/member-wisdom-detail.vue
  30. 921 0
      cfc-frontend/pages/wisdom-detail/self-quiz.vue
  31. 178 0
      cfc-frontend/pages/wisdom-detail/training-hub.vue
  32. 8 8
      cfc-frontend/pages/wisdom/index.vue

+ 5 - 5
cfc-frontend/pages.json

@@ -575,7 +575,7 @@
       ]
     },
     {
-      "root": "pages/body",
+      "root": "pages/body-detail",
       "pages": [
         {
           "path": "health-report",
@@ -604,7 +604,7 @@
       ]
     },
     {
-      "root": "pages/mind",
+      "root": "pages/mind-detail",
       "pages": [
         {
           "path": "articles",
@@ -645,7 +645,7 @@
       ]
     },
     {
-      "root": "pages/wisdom",
+      "root": "pages/wisdom-detail",
       "pages": [
         {
           "path": "cognitive-report",
@@ -666,7 +666,7 @@
       ]
     },
     {
-      "root": "pages/action",
+      "root": "pages/action-detail",
       "pages": [
         {
           "path": "interaction-log",
@@ -687,7 +687,7 @@
       ]
     },
     {
-      "root": "pages/discover",
+      "root": "pages/discover-detail",
       "pages": [
         {
           "path": "product-detail/product-detail",

+ 387 - 0
cfc-frontend/pages/action-detail/contact-detail.vue

@@ -0,0 +1,387 @@
+<template>
+  <view class="detail-page">
+    <!-- 头部 -->
+    <view class="detail-header">
+      <view class="header-top">
+        <text class="back-btn" @click="goBack">← 返回</text>
+        <text class="page-title">关系详情</text>
+        <text class="edit-btn" @click="toggleEdit">{{ isEditing ? '取消' : '编辑' }}</text>
+      </view>
+      <view class="header-avatar" :style="{ background: avatarColor }">
+        <text class="avatar-text">{{ initial }}</text>
+      </view>
+      <text class="header-name">{{ contact.name || '未知' }}</text>
+      <text class="header-meta">{{ typeLabel }}</text>
+      <text class="header-known" v-if="contact.knownSinceLabel">{{ contact.knownSinceLabel }}</text>
+      <view class="header-intimacy">
+        <text class="intimacy-heart">❤️</text>
+        <text class="intimacy-num">{{ contact.intimacyLevel || 0 }}</text>
+        <text class="intimacy-label">亲密度</text>
+      </view>
+    </view>
+
+    <!-- 基本信息 -->
+    <view class="info-section">
+      <text class="section-title">基本信息</text>
+      <view class="info-row" v-if="!isEditing">
+        <text class="info-icon">📞</text>
+        <text class="info-value">{{ contact.phone || '未填写' }}</text>
+      </view>
+      <view class="info-row" v-if="!isEditing && contact.birthdayLabel">
+        <text class="info-icon">🎂</text>
+        <text class="info-value">{{ contact.birthdayLabel }}
+          <text v-if="contact.birthdayCountdown" class="countdown"> · {{ contact.birthdayCountdown }}</text>
+        </text>
+      </view>
+      <view class="info-row" v-if="!isEditing && contact.bio">
+        <text class="info-icon">📝</text>
+        <text class="info-value">{{ contact.bio }}</text>
+      </view>
+      <view class="info-row" v-if="!isEditing && contact.tags">
+        <text class="info-icon">🏷️</text>
+        <text class="info-value">{{ contact.tags }}</text>
+      </view>
+      <view class="info-row" v-if="!isEditing && contact.notes">
+        <text class="info-icon">💬</text>
+        <text class="info-value">{{ contact.notes }}</text>
+      </view>
+
+      <!-- 编辑模式 -->
+      <view v-if="isEditing">
+        <view class="edit-row">
+          <text class="edit-label">姓名</text>
+          <input class="edit-input" v-model="editForm.name" />
+        </view>
+        <view class="edit-row">
+          <text class="edit-label">手机号</text>
+          <input class="edit-input" v-model="editForm.phone" />
+        </view>
+        <view class="edit-row">
+          <text class="edit-label">关系类型</text>
+          <picker mode="selector" :range="typeOptions" @change="onTypeChange">
+            <view class="edit-picker">{{ editForm.relationshipTypeLabel || '请选择' }}</view>
+          </picker>
+        </view>
+        <view class="edit-row">
+          <text class="edit-label">认识时间</text>
+          <picker mode="date" @change="onDateChange">
+            <view class="edit-picker">{{ editForm.knownSince || '请选择' }}</view>
+          </picker>
+        </view>
+        <view class="edit-row">
+          <text class="edit-label">生日</text>
+          <picker mode="date" @change="onBirthdayChange">
+            <view class="edit-picker">{{ editForm.birthday || '请选择' }}</view>
+          </picker>
+        </view>
+        <view class="edit-row">
+          <text class="edit-label">个人介绍</text>
+          <input class="edit-input" v-model="editForm.bio" placeholder="选填" />
+        </view>
+        <view class="edit-row">
+          <text class="edit-label">备注</text>
+          <input class="edit-input" v-model="editForm.notes" placeholder="选填" />
+        </view>
+        <button class="btn-save" @click="saveEdit">保存</button>
+      </view>
+    </view>
+
+    <!-- 互动记录 -->
+    <view class="interaction-section">
+      <text class="section-title">互动记录</text>
+      <view class="interaction-item" v-if="contact.lastContactAt">
+        <text class="interaction-date">{{ formatDate(contact.lastContactAt) }}</text>
+        <text class="interaction-desc">标记了联系</text>
+      </view>
+      <view class="interaction-item" v-if="contact.createdAt">
+        <text class="interaction-date">{{ formatDate(contact.createdAt) }}</text>
+        <text class="interaction-desc">添加了联系人</text>
+      </view>
+      <view class="interaction-empty" v-if="!contact.lastContactAt && !contact.updatedAt">
+        <text>暂无互动记录</text>
+      </view>
+      <button class="btn-interaction" @click="markInteraction">📞 标记联系</button>
+    </view>
+
+    <!-- 删除 -->
+    <view class="danger-section">
+      <button class="btn-delete" @click="confirmDelete">删除联系人</button>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getContactDetail, updateContact, deleteContact, recordContactInteraction } from '../../utils/api.js'
+
+export default {
+  data: function() {
+    return {
+      contactId: null,
+      contact: {},
+      isEditing: false,
+      editForm: {},
+      typeOptions: ['家人', '朋友', '伴侣', '同事', '其他'],
+      typeValues: { '家人': 'family', '朋友': 'friend', '伴侣': 'partner', '同事': 'colleague', '其他': 'other' },
+      reverseTypeValues: { 'family': '家人', 'friend': '朋友', 'partner': '伴侣', 'colleague': '同事', 'other': '其他' }
+    }
+  },
+  computed: {
+    initial: function() {
+      return this.contact.name ? this.contact.name[0] : '?'
+    },
+    avatarColor: function() {
+      var colors = ['#FF6B35', '#667eea', '#10B981', '#F59E0B', '#8B5CF6', '#EC4899']
+      var idx = (this.contact.id || 0) % colors.length
+      return colors[idx]
+    },
+    typeLabel: function() {
+      var map = { family: '家人', friend: '朋友', partner: '伴侣', colleague: '同事', other: '其他' }
+      return map[this.contact.relationshipType] || '其他'
+    }
+  },
+  onLoad: function(options) {
+    if (options.id) {
+      this.contactId = options.id
+      this.loadDetail()
+    }
+  },
+  methods: {
+    loadDetail: function() {
+      var self = this
+      if (!this.contactId) return
+      getContactDetail(this.contactId).then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.contact = res.data
+        } else {
+          uni.showToast({ title: res.message || '加载失败', icon: 'none' })
+        }
+      }).catch(function() {
+        uni.showToast({ title: '网络错误', icon: 'none' })
+      })
+    },
+    toggleEdit: function() {
+      if (this.isEditing) {
+        this.isEditing = false
+      } else {
+        this.editForm = {
+          name: this.contact.name || '',
+          phone: this.contact.phone || '',
+          relationshipType: this.contact.relationshipType || '',
+          relationshipTypeLabel: this.reverseTypeValues[this.contact.relationshipType] || '其他',
+          knownSince: this.contact.knownSince ? this.contact.knownSince.substring(0, 10) : '',
+          birthday: this.contact.birthday ? this.contact.birthday.substring(0, 10) : '',
+          bio: this.contact.bio || '',
+          notes: this.contact.notes || ''
+        }
+        this.isEditing = true
+      }
+    },
+    onTypeChange: function(e) {
+      var label = this.typeOptions[e.detail.value]
+      this.editForm.relationshipType = this.typeValues[label] || 'other'
+      this.editForm.relationshipTypeLabel = label
+    },
+    onDateChange: function(e) {
+      this.editForm.knownSince = e.detail.value
+    },
+    onBirthdayChange: function(e) {
+      this.editForm.birthday = e.detail.value
+    },
+    saveEdit: function() {
+      var self = this
+      var payload = { id: this.contact.id, name: this.editForm.name, phone: this.editForm.phone }
+      if (this.editForm.relationshipType) payload.relationshipType = this.editForm.relationshipType
+      if (this.editForm.knownSince) payload.knownSince = this.editForm.knownSince
+      if (this.editForm.birthday) payload.birthday = this.editForm.birthday
+      if (this.editForm.bio !== undefined) payload.bio = this.editForm.bio
+      if (this.editForm.notes !== undefined) payload.notes = this.editForm.notes
+      updateContact(payload).then(function(res) {
+        if (res.code === 200) {
+          uni.showToast({ title: '保存成功', icon: 'success' })
+          self.isEditing = false
+          self.loadDetail()
+        } else {
+          uni.showToast({ title: res.message || '保存失败', icon: 'none' })
+        }
+      }).catch(function() {
+        uni.showToast({ title: '网络错误', icon: 'none' })
+      })
+    },
+    markInteraction: function() {
+      var self = this
+      recordContactInteraction(this.contact.id).then(function(res) {
+        if (res.code === 200) {
+          uni.showToast({ title: '已标记联系', icon: 'success' })
+          self.loadDetail()
+        } else {
+          uni.showToast({ title: res.message || '操作失败', icon: 'none' })
+        }
+      }).catch(function() {
+        uni.showToast({ title: '网络错误', icon: 'none' })
+      })
+    },
+    confirmDelete: function() {
+      var self = this
+      uni.showModal({
+        title: '确认删除',
+        content: '确定要删除该联系人吗?',
+        success: function(r) {
+          if (r.confirm) {
+            deleteContact({ id: self.contact.id }).then(function(res) {
+              if (res.code === 200) {
+                uni.showToast({ title: '已删除', icon: 'success' })
+                setTimeout(function() { uni.navigateBack() }, 500)
+              } else {
+                uni.showToast({ title: res.message || '删除失败', icon: 'none' })
+              }
+            }).catch(function() {
+              uni.showToast({ title: '网络错误', icon: 'none' })
+            })
+          }
+        }
+      })
+    },
+    formatDate: function(dateStr) {
+      if (!dateStr) return '-'
+      return dateStr.substring(0, 10)
+    },
+    goBack: function() {
+      uni.navigateBack()
+    }
+  }
+}
+</script>
+
+<style scoped>
+.detail-page {
+  min-height: 100vh;
+  background: #f5f7fa;
+  padding-bottom: 40rpx;
+}
+.detail-header {
+  background: linear-gradient(135deg, #667eea, #764ba2);
+  padding: 60rpx 30rpx 40rpx;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  color: #fff;
+}
+.header-top {
+  width: 100%;
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 30rpx;
+}
+.back-btn, .edit-btn {
+  font-size: 28rpx;
+  padding: 10rpx 20rpx;
+}
+.page-title { font-size: 32rpx; font-weight: bold; }
+.header-avatar {
+  width: 120rpx;
+  height: 120rpx;
+  border-radius: 60rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-bottom: 16rpx;
+  border: 4rpx solid rgba(255,255,255,0.4);
+}
+.avatar-text { font-size: 48rpx; font-weight: bold; color: #fff; }
+.header-name { font-size: 36rpx; font-weight: bold; margin-bottom: 8rpx; }
+.header-meta { font-size: 26rpx; opacity: 0.9; margin-bottom: 4rpx; }
+.header-known { font-size: 24rpx; opacity: 0.7; margin-bottom: 12rpx; }
+.header-intimacy { display: flex; align-items: center; gap: 8rpx; }
+.intimacy-heart { font-size: 32rpx; }
+.intimacy-num { font-size: 40rpx; font-weight: bold; }
+.intimacy-label { font-size: 24rpx; opacity: 0.8; }
+
+.info-section, .interaction-section {
+  background: #fff;
+  margin: 20rpx;
+  border-radius: 20rpx;
+  padding: 30rpx;
+}
+.section-title {
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #333;
+  display: block;
+  margin-bottom: 20rpx;
+}
+.info-row {
+  display: flex;
+  align-items: flex-start;
+  margin-bottom: 16rpx;
+}
+.info-icon { font-size: 28rpx; margin-right: 12rpx; width: 40rpx; }
+.info-value { font-size: 26rpx; color: #555; flex: 1; }
+.countdown { color: #EC4899; }
+
+.edit-row {
+  margin-bottom: 20rpx;
+}
+.edit-label {
+  font-size: 26rpx;
+  color: #666;
+  display: block;
+  margin-bottom: 8rpx;
+}
+.edit-input {
+  border: 1rpx solid #ddd;
+  border-radius: 12rpx;
+  padding: 16rpx 20rpx;
+  font-size: 28rpx;
+  width: 100%;
+  box-sizing: border-box;
+}
+.edit-picker {
+  border: 1rpx solid #ddd;
+  border-radius: 12rpx;
+  padding: 16rpx 20rpx;
+  font-size: 28rpx;
+}
+.btn-save {
+  width: 100%;
+  padding: 24rpx;
+  border-radius: 12rpx;
+  background: linear-gradient(135deg, #FF6B35, #F97316);
+  font-size: 30rpx;
+  color: #fff;
+  text-align: center;
+  border: none;
+  margin-top: 20rpx;
+}
+
+.interaction-item {
+  display: flex;
+  justify-content: space-between;
+  padding: 16rpx 0;
+  border-bottom: 1rpx solid #f0f0f0;
+}
+.interaction-date { font-size: 24rpx; color: #999; }
+.interaction-desc { font-size: 26rpx; color: #555; }
+.interaction-empty { text-align: center; padding: 30rpx; color: #999; font-size: 26rpx; }
+.btn-interaction {
+  width: 100%;
+  padding: 24rpx;
+  border-radius: 12rpx;
+  background: linear-gradient(135deg, #667eea, #764ba2);
+  font-size: 30rpx;
+  color: #fff;
+  text-align: center;
+  border: none;
+  margin-top: 20rpx;
+}
+.danger-section { margin: 20rpx; }
+.btn-delete {
+  width: 100%;
+  padding: 24rpx;
+  border-radius: 12rpx;
+  background: #fff;
+  border: 1rpx solid #ff4d4f;
+  font-size: 28rpx;
+  color: #ff4d4f;
+  text-align: center;
+}
+</style>

+ 283 - 0
cfc-frontend/pages/action-detail/interaction-log.vue

@@ -0,0 +1,283 @@
+<template>
+  <view class="container">
+    <view class="page-header">
+      <text class="page-title">记录互动</text>
+    </view>
+
+    <!-- 选择互动对象 -->
+    <view class="form-section">
+      <text class="section-label">互动对象</text>
+      <view class="member-select">
+        <view class="member-item" v-for="m in members" :key="m.id"
+              :class="{selected: selectedMemberId === m.id}"
+              @click="selectMember(m)">
+          <text class="member-name">{{ m.nickname }}</text>
+          <text class="member-rel">{{ m.relationshipTypeName || m.relationshipType }}</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 选择互动类型 -->
+    <view class="form-section">
+      <text class="section-label">互动类型</text>
+      <view class="type-grid">
+        <view class="type-item" v-for="(name, key) in interactionTypes" :key="key"
+              :class="{selected: selectedType === key}"
+              @click="selectedType = key">
+          <text class="type-icon">{{ typeIcons[key] || '💬' }}</text>
+          <text class="type-name">{{ name }}</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 互动描述 -->
+    <view class="form-section">
+      <text class="section-label">描述(选填)</text>
+      <textarea class="desc-input" v-model="description"
+                 placeholder="简单描述一下这次互动..." maxlength="256"></textarea>
+    </view>
+
+    <!-- 互动时间 -->
+    <view class="form-section">
+      <text class="section-label">互动时间</text>
+      <view class="time-selector">
+        <view class="time-option" :class="{selected: timeType === 'now'}" @click="timeType = 'now'">现在</view>
+        <view class="time-option" :class="{selected: timeType === 'custom'}" @click="timeType = 'custom'">选择时间</view>
+      </view>
+      <view v-if="timeType === 'custom'" class="custom-time">
+        <picker mode="dateTime" :value="customTime" @change="onTimeChange">
+          <view class="time-display">{{ customTime || '选择时间' }}</view>
+        </picker>
+      </view>
+    </view>
+
+    <!-- 提交按钮 -->
+    <view class="submit-bar">
+      <button class="btn-submit" @click="onSubmit" :disabled="!canSubmit">保存记录</button>
+    </view>
+  </view>
+</template>
+
+<script>
+import { listFamilyMembers, addInteraction, getInteractionTypes } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      members: [],
+      selectedMemberId: null,
+      selectedType: '',
+      description: '',
+      timeType: 'now',
+      customTime: '',
+      interactionTypes: {
+        dining: '一起用餐',
+        outgoing: '外出活动',
+        call: '电话联系',
+        video: '视频通话',
+        chat: '聊天交流',
+        play: '一起游戏',
+        gift: '赠送礼物'
+      },
+      typeIcons: {
+        dining: '🍽️',
+        outgoing: '🚗',
+        call: '📞',
+        video: '📹',
+        chat: '💬',
+        play: '🎮',
+        gift: '🎁'
+      }
+    }
+  },
+  computed: {
+    canSubmit() {
+      return this.selectedMemberId && this.selectedType
+    }
+  },
+  onLoad(options) {
+    if (options.memberId) {
+      this.selectedMemberId = Number(options.memberId)
+    }
+    this.loadMembers()
+  },
+  methods: {
+    async loadMembers() {
+      try {
+        var res = await listFamilyMembers()
+        if (res.code === 200 && res.data) {
+          this.members = res.data || []
+        }
+      } catch (e) {
+        uni.showToast({ title: '加载成员失败', icon: 'none' })
+      }
+    },
+    selectMember(member) {
+      this.selectedMemberId = member.id
+    },
+    onTimeChange(e) {
+      this.customTime = e.detail.value
+    },
+    async onSubmit() {
+      if (!this.canSubmit) {
+        uni.showToast({ title: '请选择对象和类型', icon: 'none' })
+        return
+      }
+      try {
+        var happenedAt = ''
+        if (this.timeType === 'custom' && this.customTime) {
+          happenedAt = this.customTime
+        }
+        var res = await addInteraction({
+          familyId: this.members[0] && this.members[0].familyId,
+          fromMemberId: this.selectedMemberId,
+          toMemberId: this.selectedMemberId,
+          interactionType: this.selectedType,
+          description: this.description,
+          happenedAt: happenedAt
+        })
+        if (res.code === 200) {
+          uni.showToast({ title: '记录成功', icon: 'success' })
+          setTimeout(function() {
+            uni.navigateBack()
+          }, 1500)
+        } else {
+          uni.showToast({ title: res.message || '保存失败', icon: 'none' })
+        }
+      } catch (e) {
+        uni.showToast({ title: '保存失败', icon: 'none' })
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  min-height: 100vh;
+  background: #f5f5f5;
+  padding: 24rpx;
+}
+.page-header {
+  padding: 20rpx 0 30rpx;
+}
+.page-title {
+  font-size: 36rpx;
+  font-weight: bold;
+  color: #333;
+}
+.form-section {
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 24rpx;
+  margin-bottom: 20rpx;
+}
+.section-label {
+  font-size: 28rpx;
+  color: #333;
+  font-weight: 500;
+  display: block;
+  margin-bottom: 16rpx;
+}
+.member-select {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 16rpx;
+}
+.member-item {
+  padding: 16rpx 24rpx;
+  border: 2rpx solid #e0e0e0;
+  border-radius: 12rpx;
+  background: #fafafa;
+}
+.member-item.selected {
+  border-color: #F97316;
+  background: #FFF7ED;
+}
+.member-name {
+  font-size: 28rpx;
+  color: #333;
+  display: block;
+}
+.member-rel {
+  font-size: 22rpx;
+  color: #999;
+}
+.type-grid {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 16rpx;
+}
+.type-item {
+  width: calc(25% - 12rpx);
+  padding: 20rpx 0;
+  text-align: center;
+  border: 2rpx solid #e0e0e0;
+  border-radius: 12rpx;
+  background: #fafafa;
+}
+.type-item.selected {
+  border-color: #F97316;
+  background: #FFF7ED;
+}
+.type-icon {
+  font-size: 40rpx;
+  display: block;
+  margin-bottom: 8rpx;
+}
+.type-name {
+  font-size: 22rpx;
+  color: #666;
+}
+.desc-input {
+  width: 100%;
+  height: 160rpx;
+  background: #fafafa;
+  border-radius: 12rpx;
+  padding: 20rpx;
+  font-size: 28rpx;
+  box-sizing: border-box;
+}
+.time-selector {
+  display: flex;
+  gap: 16rpx;
+  margin-bottom: 16rpx;
+}
+.time-option {
+  padding: 16rpx 32rpx;
+  border: 2rpx solid #e0e0e0;
+  border-radius: 12rpx;
+  font-size: 28rpx;
+  color: #666;
+}
+.time-option.selected {
+  border-color: #F97316;
+  color: #F97316;
+  background: #FFF7ED;
+}
+.custom-time {
+  margin-top: 12rpx;
+}
+.time-display {
+  padding: 20rpx;
+  background: #fafafa;
+  border-radius: 12rpx;
+  font-size: 28rpx;
+  color: #333;
+}
+.submit-bar {
+  margin-top: 40rpx;
+}
+.btn-submit {
+  width: 100%;
+  background: #F97316;
+  color: #fff;
+  border: none;
+  border-radius: 12rpx;
+  padding: 24rpx 0;
+  font-size: 32rpx;
+}
+.btn-submit[disabled] {
+  background: #ccc;
+}
+</style>

+ 308 - 0
cfc-frontend/pages/action-detail/member-action-detail.vue

@@ -0,0 +1,308 @@
+<template>
+  <view class="detail-container">
+    <!-- 顶部导航 -->
+    <view class="nav-bar">
+      <view class="nav-back" @click="goBack">
+        <text class="nav-back-icon">&#x2190;</text>
+      </view>
+      <text class="nav-title">行 · 成员详情</text>
+    </view>
+
+    <!-- 五维能量条 -->
+    <FamilyEnergyBar
+      dimensionCode="action"
+      :sandboxData="sandboxData"
+      :dualDimension="dualDimension" />
+
+    <!-- 成员信息卡 -->
+    <view class="member-card" v-if="memberInfo">
+      <view class="member-avatar action-avatar">
+        <text class="avatar-text">{{ memberInfo.name && memberInfo.name.charAt(0) || '孩' }}</text>
+      </view>
+      <view class="member-info">
+        <text class="member-name">{{ memberInfo.name || '孩子' }}</text>
+        <text class="member-role">行动维度</text>
+      </view>
+      <view class="member-score" v-if="dualDimension">
+        <text class="score-value action-score">{{ dualDimension.energy || 0 }}</text>
+        <text class="score-label">能量值</text>
+      </view>
+    </view>
+
+    <!-- 家庭成员关系图谱(只读) -->
+    <FamilyRelationGraph
+      dimensionCode="action"
+      :selfId="selfId"
+      :members="graphMembers"
+      :energyMap="energyMapForGraph"
+      :intimacyMap="intimacyMapForGraph"
+      :interactive="false" />
+
+    <!-- 今日任务 -->
+    <DimensionTasks
+      dimensionCode="action"
+      :tasks="dimensionTasks"
+      @taskClick="onTaskClick"
+      @moreTasks="goTasks" />
+
+    <!-- 活动 -->
+    <DimensionActivities
+      dimensionCode="action"
+      :activities="dimensionActivities"
+      @activityClick="goActivityDetail"
+      @moreActivities="goMoreActivities" />
+
+    <!-- 商品 -->
+    <DimensionProducts
+      dimensionCode="action"
+      :products="dimensionProducts"
+      @productClick="goProductDetail"
+      @moreProducts="goMoreProducts" />
+
+    <!-- 底部占位 -->
+    <view class="bottom-spacer"></view>
+  </view>
+</template>
+
+<script>
+import FamilyEnergyBar from '../../components/FamilyEnergyBar.vue'
+import FamilyRelationGraph from '../../components/FamilyRelationGraph.vue'
+import DimensionTasks from '../../components/DimensionTasks.vue'
+import DimensionActivities from '../../components/DimensionActivities.vue'
+import DimensionProducts from '../../components/DimensionProducts.vue'
+import { getEnergyOverview, getFamilyEnergySandbox, getTodayTasksByCategory, getActivityList, getProductsByDomain, getChildren } from '../../utils/api.js'
+
+export default {
+  components: { FamilyEnergyBar, FamilyRelationGraph, DimensionTasks, DimensionActivities, DimensionProducts },
+  data() {
+    return {
+      childId: null,
+      selfId: null,
+      memberInfo: null,
+      sandboxData: null,
+      dualDimension: null,
+      dimensionTasks: [],
+      dimensionActivities: [],
+      dimensionProducts: []
+    }
+  },
+  computed: {
+    graphMembers: function() {
+      if (!this.sandboxData || !this.sandboxData.members) return []
+      return this.sandboxData.members.map(function(m) {
+        return {
+          id: m.memberId || m.id,
+          nickname: m.name || m.nickname || '成员',
+          memberType: m.memberType || 'child'
+        }
+      })
+    },
+    energyMapForGraph: function() {
+      var map = {}
+      if (this.sandboxData && this.sandboxData.members) {
+        for (var i = 0; i < this.sandboxData.members.length; i++) {
+          var m = this.sandboxData.members[i]
+          map[m.memberId || m.id] = {
+            bodyScore: m.bodyScore || 0,
+            mindScore: m.mindScore || 0,
+            actionScore: m.actionScore || 0
+          }
+        }
+      }
+      return map
+    },
+    intimacyMapForGraph: function() {
+      return {}
+    }
+  },
+  onLoad: function(options) {
+    this.childId = options.childId || uni.getStorageSync('currentChildId') || null
+  },
+  onShow: function() {
+    this.selfId = uni.getStorageSync('userId') || null
+    this.loadChildren()
+    this.loadSandboxData()
+    if (this.childId) {
+      this.loadEnergyData()
+      this.loadDimensionTasks()
+      this.loadDimensionActivities()
+      this.loadDimensionProducts()
+    }
+  },
+  methods: {
+    loadChildren: function() {
+      var self = this
+      getChildren().then(function(res) {
+        if (res.code === 200 && res.data) {
+          for (var i = 0; i < res.data.length; i++) {
+            if (res.data[i].childId === self.childId) {
+              self.memberInfo = res.data[i]
+              break
+            }
+          }
+          if (!self.memberInfo && res.data.length > 0) {
+            self.memberInfo = res.data[0]
+            self.childId = res.data[0].childId
+          }
+        }
+      }).catch(function() {})
+    },
+    loadSandboxData: function() {
+      var self = this
+      getFamilyEnergySandbox().then(function(res) {
+        if (res && res.data) self.sandboxData = res.data
+      }).catch(function() {})
+    },
+    loadEnergyData: function() {
+      var self = this
+      getEnergyOverview(this.childId).then(function(res) {
+        if (res && res.data) {
+          var dims = res.data.dimensions
+          if (dims && dims.length > 0) {
+            for (var i = 0; i < dims.length; i++) {
+              if (dims[i].code === 'action') {
+                self.dualDimension = dims[i]
+                break
+              }
+            }
+          }
+        }
+      }).catch(function() {})
+    },
+    loadDimensionTasks: function() {
+      var self = this
+      getTodayTasksByCategory(this.childId, 'action').then(function(res) {
+        if (res && res.data) {
+          self.dimensionTasks = Array.isArray(res.data) ? res.data.slice(0, 5) : []
+        }
+      }).catch(function() { self.dimensionTasks = [] })
+    },
+    loadDimensionActivities: function() {
+      var self = this
+      getActivityList({ dimensionCode: 'action', page: 1, size: 3 }).then(function(res) {
+        if (res && res.data) {
+          var list = Array.isArray(res.data) ? res.data : (res.data.records || [])
+          self.dimensionActivities = list.slice(0, 3)
+        }
+      }).catch(function() { self.dimensionActivities = [] })
+    },
+    loadDimensionProducts: function() {
+      var self = this
+      getProductsByDomain('action', 1, 4).then(function(res) {
+        if (res && res.data) {
+          var list = Array.isArray(res.data) ? res.data : (res.data.records || [])
+          self.dimensionProducts = list.slice(0, 4)
+        }
+      }).catch(function() { self.dimensionProducts = [] })
+    },
+    onTaskClick: function(task) {
+      uni.navigateTo({ url: '/pages/tasks/tasks' })
+    },
+    goActivityDetail: function(act) {
+      if (act && act.id) uni.navigateTo({ url: '/pages/discover-detail/activity-detail/activity-detail?id=' + act.id })
+    },
+    goMoreActivities: function() {
+      uni.navigateTo({ url: '/pages/discover/index?type=activity&dimension=action' })
+    },
+    goProductDetail: function(prod) {
+      if (prod && prod.id) uni.navigateTo({ url: '/pages/discover-detail/product-detail/product-detail?id=' + prod.id })
+    },
+    goMoreProducts: function() {
+      uni.navigateTo({ url: '/pages/discover/index?type=product&domain=action' })
+    },
+    goTasks: function() { uni.switchTab({ url: '/pages/tasks/tasks' }) },
+    goBack: function() { uni.navigateBack() }
+  }
+}
+</script>
+
+<style scoped>
+.detail-container {
+  min-height: 100vh;
+  background: #f5f7fa;
+  padding-bottom: 40rpx;
+}
+.nav-bar {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  padding: 20rpx 30rpx;
+  background: #fff;
+}
+.nav-back {
+  padding: 10rpx;
+}
+.nav-back-icon {
+  font-size: 36rpx;
+  color: #333;
+}
+.nav-title {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #333;
+  margin-left: 16rpx;
+}
+.member-card {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  margin: 20rpx 30rpx;
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 24rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
+}
+.member-avatar {
+  width: 80rpx;
+  height: 80rpx;
+  border-radius: 40rpx;
+  background: linear-gradient(135deg, #4CAF50, #81C784);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 20rpx;
+}
+.action-avatar {
+  background: linear-gradient(135deg, #10B981, #34D399);
+}
+.avatar-text {
+  font-size: 32rpx;
+  color: #fff;
+  font-weight: bold;
+}
+.member-info {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+}
+.member-name {
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #333;
+}
+.member-role {
+  font-size: 24rpx;
+  color: #999;
+  margin-top: 4rpx;
+}
+.member-score {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+.score-value {
+  font-size: 40rpx;
+  font-weight: bold;
+  color: #4CAF50;
+}
+.action-score {
+  color: #10B981;
+}
+.score-label {
+  font-size: 22rpx;
+  color: #999;
+}
+.bottom-spacer {
+  height: 40rpx;
+}
+</style>

+ 372 - 0
cfc-frontend/pages/action-detail/relationship-questionnaire.vue

@@ -0,0 +1,372 @@
+<template>
+  <view class="container">
+    <view class="page-header">
+      <text class="page-title">关系问卷</text>
+      <text class="page-subtitle" v-if="memberName">为「{{ memberName }}」评估关系质量</text>
+    </view>
+
+    <!-- 加载状态 -->
+    <view v-if="loading" class="loading-wrap">
+      <text class="loading-text">AI 正在生成问卷...</text>
+    </view>
+
+    <!-- 问卷表单 -->
+    <view v-else-if="questions.length > 0" class="questionnaire-form">
+      <view class="question-item" v-for="(q, index) in questions" :key="q.id">
+        <view class="question-header">
+          <text class="question-dimension dimension-tag" :class="q.dimension">{{ dimensionLabels[q.dimension] }}</text>
+          <text class="question-text">{{ q.question }}</text>
+        </view>
+        <view class="option-list">
+          <view class="option-item" v-for="(opt, oi) in q.options" :key="oi"
+                :class="{selected: answers[q.id] === opt}"
+                @click="selectOption(q.id, opt)">
+            <text class="option-letter">{{ optionLetters[oi] }}</text>
+            <text class="option-text">{{ opt }}</text>
+          </view>
+        </view>
+      </view>
+
+      <view class="submit-bar">
+        <button class="btn-submit" @click="onSubmit" :disabled="!allAnswered">提交问卷</button>
+      </view>
+    </view>
+
+    <!-- 无问卷 -->
+    <view v-else class="empty-wrap">
+      <text class="empty-text">暂无可用问卷</text>
+      <button class="btn-generate" @click="generateQuestionnaire">生成新问卷</button>
+    </view>
+
+    <!-- 结果展示 -->
+    <view v-if="showResult" class="result-overlay" @click="showResult = false">
+      <view class="result-card" @click.stop>
+        <text class="result-title">关系质量评估结果</text>
+        <view class="result-scores">
+          <view class="result-score-item">
+            <text class="result-score-label">信任</text>
+            <view class="result-bar-bg">
+              <view class="result-bar-fill trust" :style="{width: (result.trustScore || 0) + '%'}"></view>
+            </view>
+            <text class="result-score-value">{{ Math.round(result.trustScore || 0) }}</text>
+          </view>
+          <view class="result-score-item">
+            <text class="result-score-label">亲密</text>
+            <view class="result-bar-bg">
+              <view class="result-bar-fill intimacy" :style="{width: (result.intimacyScore || 0) + '%'}"></view>
+            </view>
+            <text class="result-score-value">{{ Math.round(result.intimacyScore || 0) }}</text>
+          </view>
+          <view class="result-score-item">
+            <text class="result-score-label">沟通</text>
+            <view class="result-bar-bg">
+              <view class="result-bar-fill communication" :style="{width: (result.communicationScore || 0) + '%'}"></view>
+            </view>
+            <text class="result-score-value">{{ Math.round(result.communicationScore || 0) }}</text>
+          </view>
+        </view>
+        <button class="btn-close" @click="showResult = false">关闭</button>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { generateQuestionnaire, submitQuestionnaire, getQuestionnaireSnapshot } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      memberId: null,
+      memberName: '',
+      snapshotId: null,
+      questions: [],
+      answers: {},
+      loading: false,
+      showResult: false,
+      result: {},
+      dimensionLabels: {
+        trust: '信任',
+        intimacy: '亲密',
+        communication: '沟通'
+      },
+      optionLetters: ['A', 'B', 'C', 'D', 'E']
+    }
+  },
+  computed: {
+    allAnswered() {
+      return this.questions.length > 0 && this.questions.every(function(q) {
+        return this.answers[q.id]
+      }, this)
+    }
+  },
+  onLoad(options) {
+    if (options.memberId) {
+      this.memberId = Number(options.memberId)
+    }
+    this.loadSnapshot()
+  },
+  methods: {
+    async loadSnapshot() {
+      if (!this.memberId) return
+      this.loading = true
+      try {
+        var res = await getQuestionnaireSnapshot(this.memberId)
+        if (res.code === 200 && res.data) {
+          this.snapshotId = res.data.id
+          this.memberName = res.data.memberName
+          try {
+            this.questions = JSON.parse(res.data.aiGeneratedJson || '[]')
+          } catch (e) {
+            this.questions = []
+          }
+        } else {
+          // 没有问卷,生成新的
+          await this.generateQuestionnaire()
+        }
+      } catch (e) {
+        uni.showToast({ title: '加载失败', icon: 'none' })
+      } finally {
+        this.loading = false
+      }
+    },
+    async generateQuestionnaire() {
+      if (!this.memberId) return
+      this.loading = true
+      try {
+        var res = await generateQuestionnaire(this.memberId)
+        if (res.code === 200 && res.data) {
+          this.snapshotId = res.data.id
+          this.memberName = res.data.memberName
+          try {
+            this.questions = JSON.parse(res.data.aiGeneratedJson || '[]')
+          } catch (e) {
+            this.questions = []
+          }
+        } else {
+          uni.showToast({ title: res.message || '生成失败', icon: 'none' })
+        }
+      } catch (e) {
+        uni.showToast({ title: '生成失败', icon: 'none' })
+      } finally {
+        this.loading = false
+      }
+    },
+    selectOption(questionId, option) {
+      this.answers[questionId] = option
+    },
+    async onSubmit() {
+      if (!this.allAnswered || !this.snapshotId) {
+        uni.showToast({ title: '请回答所有问题', icon: 'none' })
+        return
+      }
+      try {
+        var answersJson = JSON.stringify(this.answers)
+        var res = await submitQuestionnaire({
+          snapshotId: this.snapshotId,
+          answersJson: answersJson
+        })
+        if (res.code === 200 && res.data) {
+          this.result = res.data
+          this.showResult = true
+        } else {
+          uni.showToast({ title: res.message || '提交失败', icon: 'none' })
+        }
+      } catch (e) {
+        uni.showToast({ title: '提交失败', icon: 'none' })
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  min-height: 100vh;
+  background: #f5f5f5;
+  padding: 24rpx;
+}
+.page-header {
+  padding: 20rpx 0 30rpx;
+}
+.page-title {
+  font-size: 36rpx;
+  font-weight: bold;
+  color: #333;
+  display: block;
+}
+.page-subtitle {
+  font-size: 26rpx;
+  color: #999;
+  margin-top: 8rpx;
+  display: block;
+}
+.loading-wrap, .empty-wrap {
+  text-align: center;
+  padding: 100rpx 0;
+}
+.loading-text {
+  font-size: 28rpx;
+  color: #999;
+}
+.empty-text {
+  font-size: 28rpx;
+  color: #999;
+  display: block;
+  margin-bottom: 30rpx;
+}
+.btn-generate, .btn-submit {
+  background: #F97316;
+  color: #fff;
+  border: none;
+  border-radius: 12rpx;
+  padding: 24rpx 60rpx;
+  font-size: 30rpx;
+}
+.questionnaire-form {
+  display: flex;
+  flex-direction: column;
+  gap: 24rpx;
+}
+.question-item {
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 28rpx;
+}
+.question-header {
+  margin-bottom: 20rpx;
+}
+.dimension-tag {
+  font-size: 22rpx;
+  padding: 4rpx 12rpx;
+  border-radius: 6rpx;
+  margin-right: 12rpx;
+}
+.dimension-tag.trust { background: #D1FAE5; color: #059669; }
+.dimension-tag.intimacy { background: #FCE7F3; color: #DB2777; }
+.dimension-tag.communication { background: #E0E7FF; color: #4338CA; }
+.question-text {
+  font-size: 30rpx;
+  color: #333;
+  line-height: 1.6;
+  display: block;
+  margin-top: 12rpx;
+}
+.option-list {
+  display: flex;
+  flex-direction: column;
+  gap: 12rpx;
+}
+.option-item {
+  display: flex;
+  align-items: center;
+  gap: 16rpx;
+  padding: 20rpx;
+  border: 2rpx solid #e0e0e0;
+  border-radius: 12rpx;
+  background: #fafafa;
+}
+.option-item.selected {
+  border-color: #F97316;
+  background: #FFF7ED;
+}
+.option-letter {
+  width: 44rpx;
+  height: 44rpx;
+  line-height: 44rpx;
+  text-align: center;
+  background: #f0f0f0;
+  border-radius: 50%;
+  font-size: 24rpx;
+  color: #666;
+}
+.option-item.selected .option-letter {
+  background: #F97316;
+  color: #fff;
+}
+.option-text {
+  flex: 1;
+  font-size: 28rpx;
+  color: #333;
+}
+.submit-bar {
+  margin-top: 20rpx;
+}
+.btn-submit {
+  width: 100%;
+}
+.btn-submit[disabled] {
+  background: #ccc;
+}
+.result-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0,0,0,0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 100;
+}
+.result-card {
+  width: 600rpx;
+  background: #fff;
+  border-radius: 24rpx;
+  padding: 40rpx;
+}
+.result-title {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #333;
+  display: block;
+  text-align: center;
+  margin-bottom: 30rpx;
+}
+.result-scores {
+  display: flex;
+  flex-direction: column;
+  gap: 20rpx;
+  margin-bottom: 30rpx;
+}
+.result-score-item {
+  display: flex;
+  align-items: center;
+  gap: 12rpx;
+}
+.result-score-label {
+  width: 60rpx;
+  font-size: 26rpx;
+  color: #666;
+}
+.result-bar-bg {
+  flex: 1;
+  height: 16rpx;
+  background: #f0f0f0;
+  border-radius: 8rpx;
+  overflow: hidden;
+}
+.result-bar-fill {
+  height: 100%;
+  border-radius: 8rpx;
+}
+.result-bar-fill.trust { background: #10B981; }
+.result-bar-fill.intimacy { background: #FF6B9D; }
+.result-bar-fill.communication { background: #6366F1; }
+.result-score-value {
+  width: 50rpx;
+  font-size: 26rpx;
+  color: #333;
+  text-align: right;
+}
+.btn-close {
+  width: 100%;
+  background: #F97316;
+  color: #fff;
+  border: none;
+  border-radius: 12rpx;
+  padding: 20rpx 0;
+  font-size: 30rpx;
+}
+</style>

+ 8 - 8
cfc-frontend/pages/action/index.vue

@@ -411,7 +411,7 @@ export default {
       })
     },
     onContactClick: function(item) {
-      uni.navigateTo({ url: '/pages/action/contact-detail?id=' + item.id })
+      uni.navigateTo({ url: '/pages/action-detail/contact-detail?id=' + item.id })
     },
     onShowImport: function() {
       this.showImportModal = true
@@ -448,13 +448,13 @@ export default {
       uni.navigateTo({ url: '/pages/tasks/tasks' })
     },
     goActivityDetail: function(act) {
-      if (act && act.id) uni.navigateTo({ url: '/pages/discover/activity-detail/activity-detail?id=' + act.id })
+      uni.navigateTo({ url: '/pages/discover-detail/activity-detail/activity-detail?id=' + act.id })
     },
     goMoreActivities: function() {
       uni.navigateTo({ url: '/pages/discover/index?type=activity&dimension=action' })
     },
     goProductDetail: function(prod) {
-      if (prod && prod.id) uni.navigateTo({ url: '/pages/discover/product-detail/product-detail?id=' + prod.id })
+      uni.navigateTo({ url: '/pages/discover-detail/product-detail/product-detail?id=' + prod.id })
     },
     goMoreProducts: function() {
       uni.navigateTo({ url: '/pages/discover/index?type=product&domain=action' })
@@ -481,7 +481,7 @@ export default {
     goMemberDetail: function(member) {
       if (!member || !member.memberId) return
       uni.navigateTo({
-        url: '/pages/action/member-action-detail?childId=' + member.memberId
+        url: '/pages/action-detail/member-action-detail?childId=' + member.memberId
       })
     },
     goLogin: function() { uni.navigateTo({ url: '/pages/login/login' }) },
@@ -514,7 +514,7 @@ export default {
       }).catch(function() {})
     },
     goArticleDetail: function(id) {
-      uni.navigateTo({ url: '/pages/action/article-detail?id=' + id })
+      uni.navigateTo({ url: '/pages/mind-detail/article-detail?id=' + id })
     },
     loadRelationshipHealth: function() {
       var self = this
@@ -535,13 +535,13 @@ export default {
     },
     goInteractionLog: function(memberId) {
       if (memberId) {
-        uni.navigateTo({ url: '/pages/action/interaction-log?memberId=' + memberId })
+        uni.navigateTo({ url: '/pages/action-detail/interaction-log?memberId=' + memberId })
       } else {
-        uni.navigateTo({ url: '/pages/action/interaction-log' })
+        uni.navigateTo({ url: '/pages/action-detail/interaction-log' })
       }
     },
     goRelationshipQuestionnaire: function() {
-      uni.navigateTo({ url: '/pages/action/relationship-questionnaire' })
+      uni.navigateTo({ url: '/pages/action-detail/relationship-questionnaire' })
     }
   }
 }

+ 43 - 5
cfc-frontend/pages/ai/chat.vue

@@ -7,7 +7,11 @@
       </view>
       <text class="nav-title">{{ isNutrition ? '精准营养助手' : 'AI 家庭助手' }}</text>
       <view class="nav-actions">
-        <text class="nav-action" @click="showConversations = !showConversations">☰</text>
+        <view class="mascot-badge" v-if="mascotCode && !isNutrition" @click="showConversations = !showConversations">
+          <text class="mascot-badge-icon">{{ mascotIcon }}</text>
+          <text class="mascot-badge-name">{{ mascotName }}</text>
+        </view>
+        <text class="nav-action" @click="showConversations = !showConversations" v-else>☰</text>
       </view>
     </view>
 
@@ -34,8 +38,8 @@
       @scrolltoupper="loadMoreMessages" id="messageList">
       <view class="message-list-inner">
         <view class="welcome-card" v-if="messages.length === 0 && !sending">
-          <view class="welcome-icon">{{ isNutrition ? '🥗' : '🤖' }}</view>
-          <text class="welcome-title">{{ isNutrition ? '你好!我是精准营养助手' : '你好!我是你的家庭助手' }}</text>
+          <view class="welcome-icon">{{ mascotIcon || (isNutrition ? '🥗' : '🤖') }}</view>
+          <text class="welcome-title">{{ isNutrition ? '你好!我是精准营养助手' : ('你好!我是' + (mascotName || '家庭助手')) }}</text>
           <text class="welcome-desc">{{ isNutrition ? '我可以基于菌群检测报告,为你提供个性化的营养建议和推荐' : '我可以帮你了解孩子的成长情况,比如:' }}</text>
           <view class="welcome-suggestions">
             <view class="welcome-suggestion" v-if="!isNutrition" @click="quickQuestion('小明最近任务完成怎么样?')">
@@ -68,7 +72,7 @@
           </view>
           <!-- AI 回复 -->
           <view class="message message-ai" v-if="msg.answer">
-            <view class="message-avatar">🤖</view>
+            <view class="message-avatar">{{ mascotIcon || '🤖' }}</view>
             <view class="message-bubble ai-bubble">
               <text class="ai-answer-text">{{ msg.answer }}</text>
               <!-- 推荐卡片 -->
@@ -135,7 +139,10 @@ export default {
       surveyId: null,
       pendingFirstMessage: '',
       mode: '',
-      recommendations: []
+      recommendations: [],
+      mascotCode: '',
+      mascotName: '',
+      mascotIcon: ''
     }
   },
   computed: {
@@ -150,6 +157,19 @@ export default {
     if (options.firstMessage) {
       this.pendingFirstMessage = decodeURIComponent(options.firstMessage)
     }
+    // 加载用户mascot设置
+    var userInfo = uni.getStorageSync('userInfo')
+    if (userInfo) {
+      if (userInfo.mascot === 'xibao') {
+        this.mascotCode = 'xibao'
+        this.mascotName = '浠宝'
+        this.mascotIcon = '🌟'
+      } else if (userInfo.mascot === 'fubao') {
+        this.mascotCode = 'fubao'
+        this.mascotName = '福宝'
+        this.mascotIcon = '💪'
+      }
+    }
     this.loadConversations()
     // 如果有预填消息,自动发送
     var self = this
@@ -695,4 +715,22 @@ export default {
   color: #F97316;
   font-weight: 600;
 }
+
+/* Mascot badge in nav */
+.mascot-badge {
+  display: flex;
+  align-items: center;
+  gap: 4rpx;
+  padding: 6rpx 16rpx;
+  background: #EEF2FF;
+  border-radius: 20rpx;
+  font-size: 22rpx;
+}
+.mascot-badge-icon {
+  font-size: 24rpx;
+}
+.mascot-badge-name {
+  color: #6366F1;
+  font-weight: 500;
+}
 </style>

+ 517 - 0
cfc-frontend/pages/body-detail/checkin.vue

@@ -0,0 +1,517 @@
+<template>
+  <view class="checkin-container">
+    <!-- 顶部导航 -->
+    <view class="nav-bar">
+      <text class="nav-back" @click="goBack">‹ 返回</text>
+      <text class="nav-title">健康打卡</text>
+      <view class="nav-placeholder"></view>
+    </view>
+
+    <!-- 能量奖励提示 -->
+    <view class="energy-tip">
+      <text class="tip-icon">⚡</text>
+      <text class="tip-text">每次打卡可获得 5 能量值</text>
+    </view>
+
+    <!-- 打卡表单 -->
+    <view class="form-section">
+      <view class="form-title">今日打卡</view>
+
+      <!-- 日期选择 -->
+      <view class="form-item">
+        <text class="form-label">打卡日期</text>
+        <view class="date-display" @click="pickDate">
+          <text>{{ checkinDate }}</text>
+          <text class="date-arrow">›</text>
+        </view>
+      </view>
+
+      <!-- 行为选择 -->
+      <view class="form-item">
+        <text class="form-label">健康行为</text>
+        <view class="behaviors-grid">
+          <view
+            class="behavior-chip"
+            v-for="bh in behaviorOptions"
+            :key="bh.value"
+            :class="{ 'behavior-selected': selectedBehaviors.indexOf(bh.value) !== -1 }"
+            @click="toggleBehavior(bh.value)"
+          >
+            <text>{{ bh.label }}</text>
+          </view>
+        </view>
+      </view>
+
+      <!-- 心情选择 -->
+      <view class="form-item">
+        <text class="form-label">今日心情</text>
+        <view class="mood-row">
+          <view
+            class="mood-item"
+            v-for="m in moodOptions"
+            :key="m.value"
+            :class="{ 'mood-selected': selectedMood === m.value }"
+            @click="selectMood(m.value)"
+          >
+            <text class="mood-emoji">{{ m.emoji }}</text>
+            <text class="mood-label">{{ m.label }}</text>
+          </view>
+        </view>
+      </view>
+
+      <!-- 备注 -->
+      <view class="form-item">
+        <text class="form-label">备注(选填)</text>
+        <textarea
+          class="note-input"
+          v-model="note"
+          placeholder="记录今天的健康小故事..."
+          maxlength="200"
+        ></textarea>
+      </view>
+
+      <!-- 提交按钮 -->
+      <view class="submit-btn" @click="doCheckin">
+        <text>提交打卡</text>
+      </view>
+    </view>
+
+    <!-- 打卡记录 -->
+    <view class="history-section">
+      <view class="section-title">打卡记录</view>
+      <view class="history-list">
+        <view class="history-item" v-for="item in checkinList" :key="item.id">
+          <view class="history-left">
+            <text class="history-date">{{ formatDate(item.checkinDate) }}</text>
+            <view class="history-behaviors">
+              <text class="bh-tag" v-for="bh in parseBehaviors(item.behaviors)" :key="bh">{{ bh }}</text>
+            </view>
+          </view>
+          <view class="history-right">
+            <text class="history-mood">{{ getMoodEmoji(item.mood) }}</text>
+            <text class="history-energy">+{{ item.earnedEnergy || 5 }}⚡</text>
+          </view>
+        </view>
+        <view class="history-empty" v-if="checkinList.length === 0 && !loadingHistory">
+          <text>还没有打卡记录,快来打卡吧!</text>
+        </view>
+      </view>
+    </view>
+
+    <view class="bottom-spacer"></view>
+  </view>
+</template>
+
+<script>
+import config from '@/config.js'
+
+const healthRequest = function(url, data) {
+  var token = uni.getStorageSync('token')
+  return new Promise(function(resolve, reject) {
+    uni.request({
+      url: config.API_BASE_URL + url,
+      method: 'POST',
+      data: data,
+      header: {
+        'Content-Type': 'application/json',
+        'Authorization': token ? 'Bearer ' + token : ''
+      },
+      success: function(res) {
+        if (res.data.code === 200) {
+          resolve(res.data)
+        } else {
+          reject(res.data)
+        }
+      },
+      fail: function(err) {
+        reject(err)
+      }
+    })
+  })
+}
+
+export default {
+  data() {
+    return {
+      childId: null,
+      checkinDate: '',
+      selectedBehaviors: [],
+      selectedMood: '',
+      note: '',
+      checkinList: [],
+      loadingHistory: false,
+      behaviorOptions: [
+        { label: '运动30分钟', value: '运动30分钟' },
+        { label: '喝足8杯水', value: '喝足8杯水' },
+        { label: '按时睡觉', value: '按时睡觉' },
+        { label: '均衡饮食', value: '均衡饮食' },
+        { label: '户外活动', value: '户外活动2小时' },
+        { label: '视力保护', value: '视力保护' },
+        { label: '情绪愉快', value: '情绪愉快' },
+        { label: '无零食', value: '无零食' }
+      ],
+      moodOptions: [
+        { label: '开心', value: 'happy', emoji: '😊' },
+        { label: '平静', value: 'normal', emoji: '😐' },
+        { label: '低落', value: 'sad', emoji: '😔' },
+        { label: '烦躁', value: 'angry', emoji: '😤' },
+        { label: '不适', value: 'sick', emoji: '🤒' }
+      ]
+    }
+  },
+  onLoad: function(options) {
+    this.childId = options.childId ? parseInt(options.childId) : null
+    this.checkinDate = this.formatDate(new Date())
+    this.loadHistory()
+  },
+  methods: {
+    pickDate: function() {
+      var self = this
+      var now = new Date()
+      uni.chooseDate({
+        success: function(res) {
+          self.checkinDate = res.date
+        }
+      })
+    },
+    toggleBehavior: function(value) {
+      var idx = this.selectedBehaviors.indexOf(value)
+      if (idx === -1) {
+        this.selectedBehaviors.push(value)
+      } else {
+        this.selectedBehaviors.splice(idx, 1)
+      }
+    },
+    selectMood: function(value) {
+      this.selectedMood = value
+    },
+    doCheckin: function() {
+      if (this.selectedBehaviors.length === 0) {
+        uni.showToast({ title: '请至少选择一项健康行为', icon: 'none' })
+        return
+      }
+      if (!this.childId) {
+        uni.showToast({ title: '孩子ID无效', icon: 'none' })
+        return
+      }
+      var self = this
+      uni.showLoading({ title: '提交中...' })
+      healthRequest('/api/health/checkin/create', {
+        childId: this.childId,
+        checkinDate: this.checkinDate,
+        behaviors: this.selectedBehaviors,
+        note: this.note,
+        mood: this.selectedMood
+      }).then(function(res) {
+        uni.hideLoading()
+        uni.showModal({
+          title: '打卡成功',
+          content: '恭喜获得 5 能量值!',
+          showCancel: false
+        })
+        // Reset form
+        self.selectedBehaviors = []
+        self.selectedMood = ''
+        self.note = ''
+        self.checkinDate = self.formatDate(new Date())
+        // Reload history
+        self.loadHistory()
+      }).catch(function(e) {
+        uni.hideLoading()
+        if (e && e.message && e.message.indexOf('404') !== -1) {
+          uni.showToast({ title: '健康打卡功能即将上线', icon: 'none' })
+        } else {
+          uni.showToast({ title: '打卡失败,请重试', icon: 'none' })
+        }
+      })
+    },
+    loadHistory: function() {
+      if (!this.childId) return
+      var self = this
+      this.loadingHistory = true
+      healthRequest('/api/health/checkin/list', {
+        childId: this.childId
+      }).then(function(res) {
+        self.loadingHistory = false
+        if (res.data) {
+          self.checkinList = res.data
+        }
+      }).catch(function(e) {
+        self.loadingHistory = false
+        console.log('获取打卡记录失败', e)
+      })
+    },
+    parseBehaviors: function(behaviorsStr) {
+      if (!behaviorsStr) return []
+      try {
+        if (typeof behaviorsStr === 'string') {
+          return JSON.parse(behaviorsStr)
+        }
+        return behaviorsStr
+      } catch (e) {
+        if (behaviorsStr.indexOf && behaviorsStr.indexOf('[') === 0) {
+          try { return JSON.parse(behaviorsStr) } catch (e2) { return [behaviorsStr] }
+        }
+        return [behaviorsStr]
+      }
+    },
+    getMoodEmoji: function(mood) {
+      var map = { happy: '😊', normal: '😐', sad: '😔', angry: '😤', sick: '🤒' }
+      return map[mood] || '😐'
+    },
+    formatDate: function(date) {
+      if (!date) return ''
+      if (typeof date === 'string') {
+        try { date = new Date(date) } catch (e) { return date }
+      }
+      var y = date.getFullYear()
+      var m = ('' + (date.getMonth() + 1)).padStart(2, '0')
+      var d = ('' + date.getDate()).padStart(2, '0')
+      return y + '-' + m + '-' + d
+    },
+    goBack: function() {
+      uni.navigateBack()
+    }
+  }
+}
+</script>
+
+<style scoped>
+.checkin-container {
+  min-height: 100vh;
+  background: #f5f7fa;
+  padding-bottom: 60rpx;
+}
+
+/* ===== 导航栏 ===== */
+.nav-bar {
+  position: sticky;
+  top: 0;
+  background: #fff;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  padding: 20rpx 30rpx;
+  box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06);
+  z-index: 100;
+}
+.nav-back {
+  font-size: 32rpx;
+  color: #5B9BD5;
+}
+.nav-title {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #333;
+}
+.nav-placeholder {
+  width: 80rpx;
+}
+
+/* ===== 能量提示 ===== */
+.energy-tip {
+  margin: 20rpx 30rpx;
+  background: linear-gradient(135deg, #FFF8E1, #FFECB3);
+  border-radius: 16rpx;
+  padding: 16rpx 24rpx;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+.tip-icon {
+  font-size: 32rpx;
+  margin-right: 12rpx;
+}
+.tip-text {
+  font-size: 26rpx;
+  color: #E65100;
+}
+
+/* ===== 表单 ===== */
+.form-section {
+  margin: 20rpx 30rpx;
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 24rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
+}
+.form-title {
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #333;
+  margin-bottom: 24rpx;
+}
+.form-item {
+  margin-bottom: 28rpx;
+}
+.form-label {
+  font-size: 26rpx;
+  color: #666;
+  display: block;
+  margin-bottom: 12rpx;
+}
+
+/* 日期 */
+.date-display {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  background: #f5f7fa;
+  border-radius: 12rpx;
+  padding: 16rpx 20rpx;
+  font-size: 28rpx;
+  color: #333;
+}
+.date-arrow {
+  font-size: 32rpx;
+  color: #ccc;
+}
+
+/* 行为选择 */
+.behaviors-grid {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+}
+.behavior-chip {
+  background: #f0f0f0;
+  color: #666;
+  font-size: 24rpx;
+  padding: 12rpx 20rpx;
+  border-radius: 24rpx;
+  margin-right: 16rpx;
+  margin-bottom: 12rpx;
+}
+.behavior-selected {
+  background: #E8F5E9;
+  color: #2E7D32;
+}
+
+/* 心情 */
+.mood-row {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+}
+.mood-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 12rpx 16rpx;
+  border-radius: 12rpx;
+}
+.mood-selected {
+  background: #FFF8E1;
+}
+.mood-emoji {
+  font-size: 40rpx;
+}
+.mood-label {
+  font-size: 20rpx;
+  color: #666;
+  margin-top: 4rpx;
+}
+
+/* 备注 */
+.note-input {
+  width: 100%;
+  box-sizing: border-box;
+  background: #f5f7fa;
+  border-radius: 12rpx;
+  padding: 16rpx;
+  font-size: 28rpx;
+  color: #333;
+  min-height: 120rpx;
+}
+
+/* 提交按钮 */
+.submit-btn {
+  background: linear-gradient(135deg, #4CAF50, #66BB6A);
+  color: #fff;
+  font-size: 32rpx;
+  font-weight: bold;
+  text-align: center;
+  padding: 24rpx;
+  border-radius: 40rpx;
+  margin-top: 16rpx;
+}
+.submit-btn:active {
+  opacity: 0.8;
+}
+
+/* ===== 历史记录 ===== */
+.history-section {
+  margin: 30rpx 30rpx 0;
+}
+.section-title {
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #333;
+  margin-bottom: 16rpx;
+}
+.history-list {
+  display: flex;
+  flex-direction: column;
+}
+.history-item {
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 20rpx;
+  margin-bottom: 12rpx;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04);
+}
+.history-left {
+  flex: 1;
+}
+.history-date {
+  font-size: 26rpx;
+  color: #333;
+  font-weight: 500;
+  display: block;
+  margin-bottom: 8rpx;
+}
+.history-behaviors {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+}
+.bh-tag {
+  background: #E3F2FD;
+  color: #1565C0;
+  font-size: 20rpx;
+  padding: 4rpx 12rpx;
+  border-radius: 12rpx;
+  margin-right: 8rpx;
+  margin-bottom: 4rpx;
+}
+.history-right {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-end;
+}
+.history-mood {
+  font-size: 32rpx;
+}
+.history-energy {
+  font-size: 22rpx;
+  color: #4CAF50;
+  margin-top: 4rpx;
+}
+.history-empty {
+  text-align: center;
+  padding: 40rpx;
+  color: #999;
+  font-size: 26rpx;
+}
+
+/* ===== 底部 ===== */
+.bottom-spacer {
+  height: 60rpx;
+}
+</style>

+ 520 - 0
cfc-frontend/pages/body-detail/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-detail/dimension-questionnaire?childId=' + this.childId
+      })
+    },
+    goDimensions: function() {
+      uni.navigateTo({
+        url: '/pages/body-detail/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-detail/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-detail/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>

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

@@ -0,0 +1,620 @@
+<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-detail/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>

+ 1009 - 0
cfc-frontend/pages/body-detail/health-report.vue

@@ -0,0 +1,1009 @@
+<template>
+  <view class="report-container">
+    <!-- 顶部导航 -->
+    <view class="nav-bar">
+      <text class="nav-back" @click="goBack">‹ 返回</text>
+      <text class="nav-title">健康报告详情</text>
+      <view class="nav-placeholder"></view>
+    </view>
+
+    <!-- 报告头部 -->
+    <view class="report-header" v-if="report">
+      <view class="report-type-tag">{{ reportTypeLabel }}</view>
+      <view class="report-scores">
+        <view class="score-item">
+          <view class="score-circle-lg">
+            <text class="score-num">{{ report.overallScore || '--' }}</text>
+            <text class="score-unit">分</text>
+          </view>
+          <text class="score-label">综合评分</text>
+        </view>
+        <view class="score-item" v-if="report.gutHealthScore">
+          <view class="score-circle-lg score-circle-green">
+            <text class="score-num">{{ report.gutHealthScore }}</text>
+            <text class="score-unit">分</text>
+          </view>
+          <text class="score-label">肠道健康</text>
+        </view>
+      </view>
+      <view class="report-info-row" v-if="report.gutAge || report.gutType">
+        <text class="info-chip" v-if="report.gutAge">肠道年龄:{{ report.gutAge }}</text>
+        <text class="info-chip" v-if="report.gutType">肠道分型:{{ report.gutType }}</text>
+      </view>
+      <text class="report-date">报告日期:{{ formatDate(report.reportDate) }}</text>
+    </view>
+
+    <!-- ===== 评分可视化面板 (仅菌群报告) ===== -->
+    <view class="score-dashboard" v-if="report && report.reportType === 'gut_flora'">
+      <view class="main-scores">
+        <view class="main-score-item" v-for="item in mainScores" :key="item.key">
+          <view class="score-ring-wrap">
+            <svg width="140" height="140" viewBox="0 0 120 120">
+              <circle cx="60" cy="60" r="50" fill="none" stroke="#EEEEEE" stroke-width="8"/>
+              <circle cx="60" cy="60" r="50" fill="none" v-bind:stroke="item.color" stroke-width="8" v-bind:stroke-dasharray="item.circumference" v-bind:stroke-dashoffset="item.offset" stroke-linecap="round" transform="rotate(-90 60 60)"/>
+              <text x="60" y="52" text-anchor="middle" font-size="32" font-weight="bold" fill="#333">{{ item.score }}</text>
+              <text x="60" y="72" text-anchor="middle" font-size="14" fill="#999">分</text>
+            </svg>
+          </view>
+          <text class="main-score-label">{{ item.label }}</text>
+        </view>
+      </view>
+      <view class="flora-score-row" v-if="hasFloraScores">
+        <view class="flora-score-item" v-for="item in floraScores" :key="item.key">
+          <view class="flora-score-circle" v-bind:style="{ background: item.color }">
+            <text class="flora-score-num">{{ item.score }}</text>
+          </view>
+          <text class="flora-score-label">{{ item.label }}</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 指标列表 -->
+    <view class="section">
+      <view class="section-title">
+        <text>指标明细</text>
+        <text class="section-sub">共 {{ indicators.length }} 项</text>
+      </view>
+
+      <!-- 按分类分组 -->
+      <view class="category-group" v-for="group in groupedIndicators" :key="group.name">
+        <view class="category-header">
+          <text class="category-name">{{ group.name }}</text>
+          <text class="category-count">{{ group.items.length }}项</text>
+        </view>
+        <view class="indicator-card" v-for="ind in group.items" :key="ind._idx">
+          <view class="ind-header">
+            <text class="ind-name">{{ ind.indicatorName }}</text>
+            <view class="ind-status" :class="ind._cls">
+              <text>{{ ind._label }}</text>
+            </view>
+          </view>
+          <view class="ind-body">
+            <view class="ind-value-wrap">
+              <text class="ind-value">{{ ind.indicatorValue || '--' }}</text>
+              <text class="ind-unit">{{ ind.unit || '' }}</text>
+            </view>
+            <text class="ind-ref">参考值:{{ ind.refRange || '暂无' }}</text>
+          </view>
+        </view>
+      </view>
+
+      <view class="empty-indicators" v-if="indicators.length === 0 && !loading">
+        <text>暂无指标数据</text>
+      </view>
+    </view>
+
+    <!-- ===== 营养缺乏分析 ===== -->
+    <view class="section" v-if="deficiencyRecords && deficiencyRecords.length > 0">
+      <view class="section-title">
+        <text>🔬 营养缺乏分析</text>
+        <text class="section-sub">{{ deficiencyRecords.length }}项</text>
+      </view>
+      <view class="deficiency-list">
+        <view class="deficiency-card" v-for="dr in deficiencyRecords" :key="dr.nutrient">
+          <view class="deficiency-header">
+            <text class="deficiency-nutrient">{{ dr.nutrient }}</text>
+            <view class="deficiency-type-tag" :class="'dtype-' + dr.deficiencyType">
+              <text>{{ dr.deficiencyTypeLabel }}</text>
+            </view>
+          </view>
+          <view class="deficiency-severity-row">
+            <view class="severity-bar">
+              <view class="severity-fill" :style="{ width: (dr.severity * 100) + '%', background: dr.severityColor }"></view>
+            </view>
+            <text class="severity-val">{{ dr.severity }}</text>
+          </view>
+          <view class="deficiency-meta" v-if="dr.indicatorName">
+            <text class="meta-label">关联指标:</text>
+            <text class="meta-value">{{ dr.indicatorName }} ({{ dr.indicatorValue }})</text>
+          </view>
+          <view class="deficiency-foods" v-if="dr.foodSuggestions">
+            <text class="foods-label">推荐食物:</text>
+            <text class="foods-value">{{ dr.foodSuggestions }}</text>
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <!-- ===== 食材推荐 ===== -->
+    <view class="section" v-if="foods && foods.length > 0">
+      <view class="section-title">
+        <text>🥗 食材推荐</text>
+        <text class="section-sub">共 {{ foods.length }} 项</text>
+      </view>
+      <view class="food-list">
+        <view class="food-card" v-for="food in foods" :key="food.id">
+          <view class="food-header">
+            <text class="food-name">{{ food.name }}</text>
+            <view class="food-score" :class="food.scoreClass">
+              <text>{{ food.score || '--' }}</text>
+            </view>
+          </view>
+          <view class="food-meta" v-if="food.category">
+            <text class="food-category">{{ food.category }}</text>
+          </view>
+          <view class="food-nutrition" v-if="food.energyKj || food.protein || food.fat || food.carbs">
+            <view class="nutrition-item" v-if="food.energyKj">
+              <text class="n-label">能量</text>
+              <text class="n-value">{{ food.energyKj }} kJ</text>
+            </view>
+            <view class="nutrition-item" v-if="food.protein">
+              <text class="n-label">蛋白质</text>
+              <text class="n-value">{{ food.protein }}g</text>
+            </view>
+            <view class="nutrition-item" v-if="food.fat">
+              <text class="n-label">脂肪</text>
+              <text class="n-value">{{ food.fat }}g</text>
+            </view>
+            <view class="nutrition-item" v-if="food.carbs">
+              <text class="n-label">碳水</text>
+              <text class="n-value">{{ food.carbs }}g</text>
+            </view>
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <!-- ===== 五维雷达图 (菌群报告) ===== -->
+    <view class="section" v-if="report && report.reportType === 'gut_flora' && radarDimensions.length > 0">
+      <view class="section-title">
+        <text>五维雷达图</text>
+      </view>
+      <view class="radar-wrap">
+        <canvas canvas-id="radarCanvas" id="radarCanvas" class="radar-canvas" @touchstart.prevent></canvas>
+      </view>
+    </view>
+
+    <!-- 底部占位 -->
+    <view class="bottom-spacer"></view>
+
+    <!-- 营养分析入口按钮 -->
+    <view class="nutrition-fab" @click="goNutritionChat" v-if="report">
+      <text class="fab-icon">🥗</text>
+      <text class="fab-text">精准营养分析</text>
+    </view>
+  </view>
+</template>
+
+<script>
+import config from '@/config.js'
+import { getDeficiencyAnalysis } from '@/utils/api.js'
+
+const healthRequest = function(url, data) {
+  var token = uni.getStorageSync('token')
+  return new Promise(function(resolve, reject) {
+    uni.request({
+      url: config.API_BASE_URL + url,
+      method: 'POST',
+      data: data,
+      header: {
+        'Content-Type': 'application/json',
+        'Authorization': token ? 'Bearer ' + token : ''
+      },
+      success: function(res) {
+        if (res.data.code === 200) {
+          resolve(res.data)
+        } else {
+          reject(res.data)
+        }
+      },
+      fail: function(err) {
+        reject(err)
+      }
+    })
+  })
+}
+
+export default {
+  data() {
+    return {
+      childId: null,
+      reportId: null,
+      report: null,
+      reportDetail: null,
+      indicators: [],
+      deficiencyRecords: [],
+      foods: [],
+      loading: false
+    }
+  },
+  computed: {
+    reportTypeLabel: function() {
+      if (!this.report) return ''
+      return this.report.reportType === 'gut_flora' ? '肠道检测报告' : '体检报告'
+    },
+    // 预计算分组,每个指标附加 _cls (CSS类) 和 _label (显示标签)
+    groupedIndicators: function() {
+      var result = []
+      var groupMap = {}
+      var idx = 0
+      for (var i = 0; i < this.indicators.length; i++) {
+        var ind = this.indicators[i]
+        var cat = ind.category || '其他'
+        // Compute status css class
+        var cls = 'status-normal'
+        var label = '正常'
+        var status = ind.status || ''
+        var s = status.toLowerCase()
+        if (s.indexOf('偏高') !== -1 || s.indexOf('过高') !== -1 || s.indexOf('concern') !== -1) {
+          cls = 'status-warning'
+          label = '偏高'
+        } else if (s.indexOf('偏低') !== -1 || s.indexOf('过低') !== -1 || s.indexOf('注意') !== -1 || s.indexOf('warning') !== -1) {
+          cls = 'status-warn'
+          label = '偏低'
+        } else if (s.indexOf('正常') !== -1 || s.indexOf('normal') !== -1 || s.indexOf('低风险') !== -1) {
+          cls = 'status-normal'
+          label = '正常'
+        } else if (status) {
+          label = status
+        }
+        ind._cls = cls
+        ind._label = label
+        ind._idx = idx
+        idx++
+        // Group
+        if (!groupMap[cat]) {
+          groupMap[cat] = { name: cat, items: [] }
+          result.push(groupMap[cat])
+        }
+        groupMap[cat].items.push(ind)
+      }
+      return result
+    },
+    circumference: function() {
+      return Math.round(2 * Math.PI * 50)
+    },
+    mainScores: function() {
+      if (!this.report) return []
+      var items = []
+      var r = this.report
+      var pairs = [
+        {key:'overall', label:'综合评分', score: r.overallScore, color:'#5B9BD5'},
+        {key:'gut', label:'肠道健康', score: r.gutHealthScore, color:'#4CAF50'},
+        {key:'chronic', label:'慢病控制', score: r.chronicDiseaseScore, color:'#FF9800'},
+        {key:'nutrition', label:'营养均衡', score: r.nutritionScore, color:'#2196F3'}
+      ]
+      var circ = Math.round(2 * Math.PI * 50)
+      for (var i = 0; i < pairs.length; i++) {
+        var p = pairs[i]
+        var s = p.score || 0
+        items.push({key: p.key, label: p.label, score: s, color: p.color, circumference: circ, offset: Math.round(circ - (s / 100) * circ)})
+      }
+      return items
+    },
+    hasFloraScores: function() {
+      if (!this.report) return false
+      var r = this.report
+      return (r.balanceScore != null) || (r.diversityScore != null) || (r.beneficialScore != null) || (r.harmfulScore != null) || (r.coreGenusScore != null)
+    },
+    floraScores: function() {
+      if (!this.report) return []
+      var r = this.report
+      var items = []
+      var pairs = [
+        {key:'balance', label:'菌群平衡', score: r.balanceScore},
+        {key:'diversity', label:'菌群多样性', score: r.diversityScore},
+        {key:'beneficial', label:'有益菌', score: r.beneficialScore},
+        {key:'harmful', label:'有害菌', score: r.harmfulScore},
+        {key:'coreGenus', label:'核心菌属', score: r.coreGenusScore}
+      ]
+      for (var i = 0; i < pairs.length; i++) {
+        var p = pairs[i]
+        if (p.score != null) {
+          var s = p.score
+          var color = '#4CAF50'
+          if (s < 30) { color = '#F44336' }
+          else if (s < 60) { color = '#FFC107' }
+          items.push({key: p.key, label: p.label, score: s, color: color})
+        }
+      }
+      return items
+    },
+    radarDimensions: function() {
+      if (!this.report) return []
+      var r = this.report
+      var dims = []
+      if (r.overallScore != null) dims.push({ label: '综合', value: r.overallScore })
+      if (r.gutHealthScore != null) dims.push({ label: '肠道', value: r.gutHealthScore })
+      if (r.nutritionScore != null) dims.push({ label: '营养', value: r.nutritionScore })
+      if (r.chronicDiseaseScore != null) dims.push({ label: '慢病', value: r.chronicDiseaseScore })
+      if (r.gutBalanceScore != null) dims.push({ label: '菌群平衡', value: r.gutBalanceScore })
+      return dims
+    }
+  },
+  onLoad: function(options) {
+    var childId = options.childId
+    var reportId = options.reportId
+    this.childId = childId ? parseInt(childId) : null
+    this.reportId = reportId ? parseInt(reportId) : null
+    this.loadData()
+  },
+  onReady: function() {
+    if (this.radarDimensions.length >= 3) {
+      this.drawRadarChart()
+    }
+  },
+  methods: {
+    loadData: function() {
+      var self = this
+      uni.showLoading({ title: '加载中...' })
+      // Load report + indicators in parallel
+      var promises = []
+      if (this.reportId) {
+        promises.push(healthRequest('/api/health/report/latest', { childId: this.childId }))
+        promises.push(healthRequest('/api/health/indicator/list', { reportId: this.reportId }))
+      } else if (this.childId) {
+        promises.push(healthRequest('/api/health/report/latest', { childId: this.childId }))
+      }
+      Promise.all(promises.map(function(p) {
+        return p.catch(function(e) { return null })
+      })).then(function(results) {
+        uni.hideLoading()
+        if (results[0] && results[0].data) {
+          self.report = results[0].data
+          self.reportId = self.report.id
+          self.loadDeficiencyRecords()
+          // Load indicators after getting reportId
+          if (self.reportId) {
+            healthRequest('/api/health/indicator/list', { reportId: self.reportId }).then(function(res) {
+              if (res.data) {
+                self.indicators = res.data
+              }
+            }).catch(function(e) {
+              console.log('获取指标失败', e)
+            })
+            healthRequest('/api/health/report/detail', { reportId: self.reportId }).then(function(res) {
+              if (res.data) {
+                self.reportDetail = res.data
+              }
+            }).catch(function(e) {
+              console.log('获取报告详情失败', e)
+            })
+            healthRequest('/api/health/foods', { reportId: self.reportId }).then(function(res) {
+              if (res.data) {
+                self.foods = res.data.map(function(f) {
+                  f.scoreClass = self.getFoodScoreClass(f.score)
+                  return f
+                })
+              }
+            }).catch(function(e) {
+              console.log('获取食材推荐失败', e)
+            })
+          }
+        }
+      }).catch(function(e) {
+        uni.hideLoading()
+        uni.showToast({ title: '加载失败', icon: 'none' })
+      })
+    },
+    goBack: function() {
+      uni.navigateBack()
+    },
+    goNutritionChat: function() {
+      if (!this.reportId) return
+      uni.navigateTo({
+        url: '/pages/ai/chat?mode=nutrition&reportId=' + this.reportId + '&firstMessage=' + encodeURIComponent('请根据这份菌群检测报告,分析我孩子的营养状况并提出建议')
+      })
+    },
+    loadDeficiencyRecords: function() {
+      if (!this.childId) return
+      var self = this
+      getDeficiencyAnalysis(this.childId).then(function(res) {
+          if (res.code === 200 && res.data) {
+          self.deficiencyRecords = res.data.map(function(dr) {
+            dr.deficiencyTypeLabel = self.getDeficiencyTypeLabel(dr.deficiencyType)
+            dr.severityColor = self.getSeverityColor(dr.severity)
+            return dr
+          })
+        }
+      }).catch(function(e) {
+        console.log('获取缺乏分析失败', e)
+      })
+    },
+    getDeficiencyTypeLabel: function(type) {
+      if (type === 'deficiency') return '缺乏'
+      if (type === 'excess') return '偏高'
+      if (type === 'borderline') return '边界值'
+      return '未分类'
+    },
+    getSeverityColor: function(severity) {
+      if (severity >= 0.7) return '#EF4444'
+      if (severity >= 0.3) return '#F59E0B'
+      return '#10B981'
+    },
+    drawRadarChart: function() {
+      var dims = this.radarDimensions
+      if (!dims || dims.length < 3) return
+      var ctx = uni.createCanvasContext('radarCanvas', this)
+      var cw = 300
+      var ch = 300
+      var cx = cw / 2
+      var cy = ch / 2
+      var maxR = 110
+      var n = dims.length
+      var angleStep = (2 * Math.PI) / n
+
+      for (var level = 1; level <= 5; level++) {
+        var r = maxR * (level / 5)
+        ctx.setStrokeStyle('#E5E7EB')
+        ctx.setLineWidth(1)
+        ctx.beginPath()
+        for (var j = 0; j < n; j++) {
+          var a = j * angleStep - Math.PI / 2
+          var x = cx + r * Math.cos(a)
+          var y = cy + r * Math.sin(a)
+          if (j === 0) ctx.moveTo(x, y)
+          else ctx.lineTo(x, y)
+        }
+        ctx.closePath()
+        ctx.stroke()
+      }
+
+      for (var k = 0; k < n; k++) {
+        var a2 = k * angleStep - Math.PI / 2
+        ctx.beginPath()
+        ctx.moveTo(cx, cy)
+        ctx.lineTo(cx + maxR * Math.cos(a2), cy + maxR * Math.sin(a2))
+        ctx.setStrokeStyle('#E5E7EB')
+        ctx.setLineWidth(1)
+        ctx.stroke()
+      }
+
+      ctx.beginPath()
+      for (var m = 0; m < n; m++) {
+        var angle = m * angleStep - Math.PI / 2
+        var val = dims[m].value || 0
+        var rr = maxR * (val / 100)
+        var px = cx + rr * Math.cos(angle)
+        var py = cy + rr * Math.sin(angle)
+        if (m === 0) ctx.moveTo(px, py)
+        else ctx.lineTo(px, py)
+      }
+      ctx.closePath()
+      ctx.setFillStyle('rgba(249, 115, 22, 0.2)')
+      ctx.fill()
+      ctx.setStrokeStyle('#F97316')
+      ctx.setLineWidth(2)
+      ctx.stroke()
+
+      for (var q = 0; q < n; q++) {
+        var angle3 = q * angleStep - Math.PI / 2
+        var lx = cx + (maxR + 20) * Math.cos(angle3)
+        var ly = cy + (maxR + 20) * Math.sin(angle3)
+        ctx.setFontSize(11)
+        ctx.setFillStyle('#6B7280')
+        ctx.setTextAlign('center')
+        ctx.setTextBaseline('middle')
+        ctx.fillText(dims[q].label + ' ' + (dims[q].value || '--'), lx, ly)
+      }
+
+      ctx.draw()
+    },
+    getFoodScoreClass: function(score) {
+      if (score == null) return 'score-unknown'
+      if (score >= 80) return 'score-high'
+      if (score >= 60) return 'score-mid'
+      return 'score-low'
+    },
+    formatDate: function(dateStr) {
+      if (!dateStr) return '--'
+      try {
+        var d = new Date(dateStr)
+        var y = d.getFullYear()
+        var m = ('' + (d.getMonth() + 1)).padStart(2, '0')
+        var day = ('' + d.getDate()).padStart(2, '0')
+        return y + '-' + m + '-' + day
+      } catch (e) {
+        return dateStr
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.report-container {
+  min-height: 100vh;
+  background: #f5f7fa;
+}
+
+/* ===== 导航栏 ===== */
+.nav-bar {
+  position: sticky;
+  top: 0;
+  background: #fff;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  padding: 20rpx 30rpx;
+  box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06);
+  z-index: 100;
+}
+.nav-back {
+  font-size: 32rpx;
+  color: #5B9BD5;
+}
+.nav-title {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #333;
+}
+.nav-placeholder {
+  width: 80rpx;
+}
+
+/* ===== 报告头部 ===== */
+.report-header {
+  background: linear-gradient(135deg, #E3F2FD, #BBDEFB);
+  margin: 20rpx 30rpx;
+  border-radius: 20rpx;
+  padding: 30rpx 24rpx;
+}
+.report-type-tag {
+  display: inline-block;
+  background: #1565C0;
+  color: #fff;
+  font-size: 22rpx;
+  padding: 6rpx 20rpx;
+  border-radius: 20rpx;
+  margin-bottom: 20rpx;
+}
+.report-scores {
+  display: flex;
+  flex-direction: row;
+  margin-bottom: 20rpx;
+}
+.score-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  margin-right: 60rpx;
+}
+.score-circle-lg {
+  width: 140rpx;
+  height: 140rpx;
+  border-radius: 70rpx;
+  background: #fff;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  margin-bottom: 8rpx;
+}
+.score-circle-green {
+  background: #E8F5E9;
+}
+.score-num {
+  font-size: 48rpx;
+  font-weight: bold;
+  color: #1565C0;
+}
+.score-circle-green .score-num {
+  color: #2E7D32;
+}
+.score-unit {
+  font-size: 22rpx;
+  color: #64B5F6;
+}
+.score-circle-green .score-unit {
+  color: #81C784;
+}
+.score-label {
+  font-size: 24rpx;
+  color: #666;
+}
+.report-info-row {
+  display: flex;
+  flex-direction: row;
+  margin-bottom: 16rpx;
+}
+.info-chip {
+  background: #fff;
+  color: #333;
+  font-size: 24rpx;
+  padding: 8rpx 20rpx;
+  border-radius: 20rpx;
+  margin-right: 16rpx;
+}
+.report-date {
+  font-size: 22rpx;
+  color: #888;
+}
+
+/* ===== 指标明细 ===== */
+.section {
+  margin: 20rpx 30rpx;
+}
+.section-title {
+  display: flex;
+  flex-direction: row;
+  align-items: baseline;
+  margin-bottom: 20rpx;
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #333;
+}
+.section-sub {
+  font-size: 22rpx;
+  color: #999;
+  font-weight: normal;
+  margin-left: 12rpx;
+}
+.category-group {
+  margin-bottom: 30rpx;
+}
+.category-header {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 12rpx;
+}
+.category-name {
+  font-size: 26rpx;
+  font-weight: bold;
+  color: #333;
+}
+.category-count {
+  font-size: 22rpx;
+  color: #999;
+}
+.indicator-card {
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 20rpx;
+  margin-bottom: 12rpx;
+  box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04);
+}
+.ind-header {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 12rpx;
+}
+.ind-name {
+  font-size: 28rpx;
+  color: #333;
+  font-weight: 500;
+}
+.ind-status {
+  font-size: 22rpx;
+  padding: 4rpx 16rpx;
+  border-radius: 16rpx;
+}
+.status-normal {
+  background: #E8F5E9;
+  color: #2E7D32;
+}
+.status-warning {
+  background: #FFF3E0;
+  color: #E65100;
+}
+.status-warn {
+  background: #F3E5F5;
+  color: #7B1FA2;
+}
+.ind-body {
+  display: flex;
+  flex-direction: row;
+  align-items: baseline;
+  justify-content: space-between;
+}
+.ind-value-wrap {
+  display: flex;
+  flex-direction: row;
+  align-items: baseline;
+}
+.ind-value {
+  font-size: 36rpx;
+  font-weight: bold;
+  color: #1565C0;
+}
+.ind-unit {
+  font-size: 24rpx;
+  color: #999;
+  margin-left: 8rpx;
+}
+.ind-ref {
+  font-size: 22rpx;
+  color: #bbb;
+}
+.empty-indicators {
+  text-align: center;
+  padding: 60rpx;
+  color: #999;
+  font-size: 28rpx;
+}
+
+/* ===== 底部 ===== */
+.bottom-spacer {
+  height: 130rpx;
+}
+
+/* ===== 营养分析入口 FAB ===== */
+.nutrition-fab {
+  position: fixed;
+  bottom: 40rpx;
+  left: 50%;
+  transform: translateX(-50%);
+  background: linear-gradient(135deg, #43A047, #66BB6A);
+  color: #fff;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  padding: 20rpx 40rpx;
+  border-radius: 50rpx;
+  box-shadow: 0 8rpx 24rpx rgba(67, 160, 71, 0.4);
+  z-index: 200;
+}
+
+.fab-icon {
+  font-size: 36rpx;
+  margin-right: 12rpx;
+}
+
+.fab-text {
+  font-size: 30rpx;
+  font-weight: 600;
+}
+
+.score-dashboard {
+  margin: 20rpx 30rpx;
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 24rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
+}
+.main-scores {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+  justify-content: space-around;
+}
+.main-score-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  margin-bottom: 20rpx;
+  width: 45%;
+}
+.score-ring-wrap {
+  margin-bottom: 8rpx;
+}
+.main-score-label {
+  font-size: 24rpx;
+  color: #666;
+  font-weight: 500;
+}
+.flora-score-row {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-around;
+  margin-top: 16rpx;
+  padding-top: 20rpx;
+  border-top: 2rpx solid #F0F0F0;
+}
+.flora-score-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+.flora-score-circle {
+  width: 80rpx;
+  height: 80rpx;
+  border-radius: 40rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-bottom: 8rpx;
+}
+.flora-score-num {
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #fff;
+}
+.flora-score-label {
+  font-size: 20rpx;
+  color: #888;
+}
+
+/* ===== 营养缺乏分析 ===== */
+.deficiency-list {
+  display: flex;
+  flex-direction: column;
+}
+.deficiency-card {
+  background: #FAFAFA;
+  border-radius: 16rpx;
+  padding: 20rpx;
+  margin-bottom: 12rpx;
+}
+.deficiency-header {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 12rpx;
+}
+.deficiency-nutrient {
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #333;
+}
+.deficiency-type-tag {
+  font-size: 22rpx;
+  padding: 4rpx 16rpx;
+  border-radius: 16rpx;
+  background: #FFF3E0;
+  color: #E65100;
+}
+.dtype-deficiency {
+  background: #FEE2E2;
+  color: #DC2626;
+}
+.dtype-excess {
+  background: #FFF3E0;
+  color: #E65100;
+}
+.dtype-borderline {
+  background: #FEF3C7;
+  color: #D97706;
+}
+.dtype-unclassified {
+  background: #F3F4F6;
+  color: #6B7280;
+}
+.deficiency-severity-row {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  margin-bottom: 8rpx;
+}
+.severity-bar {
+  flex: 1;
+  height: 12rpx;
+  background: #F3F4F6;
+  border-radius: 6rpx;
+  overflow: hidden;
+  margin-right: 16rpx;
+}
+.severity-fill {
+  height: 100%;
+  border-radius: 6rpx;
+}
+.severity-val {
+  font-size: 24rpx;
+  color: #9CA3AF;
+  width: 60rpx;
+  text-align: right;
+}
+.deficiency-meta {
+  font-size: 22rpx;
+  color: #6B7280;
+  margin-bottom: 6rpx;
+}
+.deficiency-foods {
+  font-size: 22rpx;
+  color: #374151;
+  margin-top: 8rpx;
+}
+.foods-label {
+  color: #9CA3AF;
+}
+.foods-value {
+  color: #2E7D32;
+}
+
+/* ===== 雷达图 ===== */
+.radar-wrap {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+.radar-canvas {
+  width: 300px;
+  height: 300px;
+  margin: 0 auto;
+}
+
+/* ===== 食材推荐 ===== */
+.food-list {
+  display: flex;
+  flex-direction: column;
+}
+.food-card {
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 20rpx;
+  margin-bottom: 12rpx;
+  box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04);
+}
+.food-header {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 8rpx;
+}
+.food-name {
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #333;
+}
+.food-score {
+  font-size: 24rpx;
+  padding: 4rpx 16rpx;
+  border-radius: 16rpx;
+  font-weight: 600;
+}
+.score-high {
+  background: #DCFCE7;
+  color: #16A34A;
+}
+.score-mid {
+  background: #FEF3C7;
+  color: #D97706;
+}
+.score-low {
+  background: #FEE2E2;
+  color: #DC2626;
+}
+.score-unknown {
+  background: #F3F4F6;
+  color: #6B7280;
+}
+.food-meta {
+  margin-bottom: 8rpx;
+}
+.food-category {
+  font-size: 22rpx;
+  color: #6B7280;
+  background: #F3F4F6;
+  padding: 2rpx 12rpx;
+  border-radius: 12rpx;
+}
+.food-nutrition {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+  gap: 16rpx;
+  margin-top: 8rpx;
+}
+.nutrition-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  min-width: 100rpx;
+}
+.n-label {
+  font-size: 20rpx;
+  color: #9CA3AF;
+}
+.n-value {
+  font-size: 24rpx;
+  color: #374151;
+  font-weight: 500;
+}
+</style>

+ 302 - 0
cfc-frontend/pages/body-detail/member-body-detail.vue

@@ -0,0 +1,302 @@
+<template>
+  <view class="detail-container">
+    <!-- 顶部导航 -->
+    <view class="nav-bar">
+      <view class="nav-back" @click="goBack">
+        <text class="nav-back-icon">&#x2190;</text>
+      </view>
+      <text class="nav-title">身 · 成员详情</text>
+    </view>
+
+    <!-- 五维能量条 -->
+    <FamilyEnergyBar
+      dimensionCode="body"
+      :sandboxData="sandboxData"
+      :dualDimension="dualDimension" />
+
+    <!-- 成员信息卡 -->
+    <view class="member-card" v-if="memberInfo">
+      <view class="member-avatar">
+        <text class="avatar-text">{{ memberInfo.name && memberInfo.name.charAt(0) || '孩' }}</text>
+      </view>
+      <view class="member-info">
+        <text class="member-name">{{ memberInfo.name || '孩子' }}</text>
+        <text class="member-role">身体维度</text>
+      </view>
+      <view class="member-score" v-if="dualDimension">
+        <text class="score-value">{{ dualDimension.energy || 0 }}</text>
+        <text class="score-label">能量值</text>
+      </view>
+    </view>
+
+    <!-- 家庭成员关系图谱(只读) -->
+    <FamilyRelationGraph
+      dimensionCode="body"
+      :selfId="selfId"
+      :members="graphMembers"
+      :energyMap="energyMapForGraph"
+      :intimacyMap="intimacyMapForGraph"
+      :interactive="false" />
+
+    <!-- 今日任务 -->
+    <DimensionTasks
+      dimensionCode="body"
+      :tasks="dimensionTasks"
+      @taskClick="onTaskClick"
+      @moreTasks="goTasks" />
+
+    <!-- 活动 -->
+    <DimensionActivities
+      dimensionCode="body"
+      :activities="dimensionActivities"
+      @activityClick="goActivityDetail"
+      @moreActivities="goMoreActivities" />
+
+    <!-- 商品 -->
+    <DimensionProducts
+      dimensionCode="body"
+      :products="dimensionProducts"
+      @productClick="goProductDetail"
+      @moreProducts="goMoreProducts" />
+
+    <!-- 底部占位 -->
+    <view class="bottom-spacer"></view>
+  </view>
+</template>
+
+<script>
+import FamilyEnergyBar from '../../components/FamilyEnergyBar.vue'
+import FamilyRelationGraph from '../../components/FamilyRelationGraph.vue'
+import DimensionTasks from '../../components/DimensionTasks.vue'
+import DimensionActivities from '../../components/DimensionActivities.vue'
+import DimensionProducts from '../../components/DimensionProducts.vue'
+import { getEnergyOverview, getFamilyEnergySandbox, getTodayTasksByCategory, getActivityList, getProductsByDomain, getChildren } from '../../utils/api.js'
+
+export default {
+  components: { FamilyEnergyBar, FamilyRelationGraph, DimensionTasks, DimensionActivities, DimensionProducts },
+  data() {
+    return {
+      childId: null,
+      selfId: null,
+      memberInfo: null,
+      sandboxData: null,
+      dualDimension: null,
+      dimensionTasks: [],
+      dimensionActivities: [],
+      dimensionProducts: []
+    }
+  },
+  computed: {
+    graphMembers: function() {
+      if (!this.sandboxData || !this.sandboxData.members) return []
+      return this.sandboxData.members.map(function(m) {
+        return {
+          id: m.memberId || m.id,
+          nickname: m.name || m.nickname || '成员',
+          memberType: m.memberType || 'child'
+        }
+      })
+    },
+    energyMapForGraph: function() {
+      var map = {}
+      if (this.sandboxData && this.sandboxData.members) {
+        for (var i = 0; i < this.sandboxData.members.length; i++) {
+          var m = this.sandboxData.members[i]
+          map[m.memberId || m.id] = {
+            bodyScore: m.bodyScore || 0,
+            mindScore: m.mindScore || 0,
+            actionScore: m.actionScore || 0
+          }
+        }
+      }
+      return map
+    },
+    intimacyMapForGraph: function() {
+      return {}
+    }
+  },
+  onLoad: function(options) {
+    this.childId = options.childId || uni.getStorageSync('currentChildId') || null
+  },
+  onShow: function() {
+    this.selfId = uni.getStorageSync('userId') || null
+    this.loadChildren()
+    this.loadSandboxData()
+    if (this.childId) {
+      this.loadEnergyData()
+      this.loadDimensionTasks()
+      this.loadDimensionActivities()
+      this.loadDimensionProducts()
+    }
+  },
+  methods: {
+    loadChildren: function() {
+      var self = this
+      getChildren().then(function(res) {
+        if (res.code === 200 && res.data) {
+          for (var i = 0; i < res.data.length; i++) {
+            if (res.data[i].childId === self.childId) {
+              self.memberInfo = res.data[i]
+              break
+            }
+          }
+          if (!self.memberInfo && res.data.length > 0) {
+            self.memberInfo = res.data[0]
+            self.childId = res.data[0].childId
+          }
+        }
+      }).catch(function() {})
+    },
+    loadSandboxData: function() {
+      var self = this
+      getFamilyEnergySandbox().then(function(res) {
+        if (res && res.data) self.sandboxData = res.data
+      }).catch(function() {})
+    },
+    loadEnergyData: function() {
+      var self = this
+      getEnergyOverview(this.childId).then(function(res) {
+        if (res && res.data) {
+          var dims = res.data.dimensions
+          if (dims && dims.length > 0) {
+            for (var i = 0; i < dims.length; i++) {
+              if (dims[i].code === 'body') {
+                self.dualDimension = dims[i]
+                break
+              }
+            }
+          }
+        }
+      }).catch(function() {})
+    },
+    loadDimensionTasks: function() {
+      var self = this
+      getTodayTasksByCategory(this.childId, 'body').then(function(res) {
+        if (res && res.data) {
+          self.dimensionTasks = Array.isArray(res.data) ? res.data.slice(0, 5) : []
+        }
+      }).catch(function() { self.dimensionTasks = [] })
+    },
+    loadDimensionActivities: function() {
+      var self = this
+      getActivityList({ dimensionCode: 'body', page: 1, size: 3 }).then(function(res) {
+        if (res && res.data) {
+          var list = Array.isArray(res.data) ? res.data : (res.data.records || [])
+          self.dimensionActivities = list.slice(0, 3)
+        }
+      }).catch(function() { self.dimensionActivities = [] })
+    },
+    loadDimensionProducts: function() {
+      var self = this
+      getProductsByDomain('body', 1, 4).then(function(res) {
+        if (res && res.data) {
+          var list = Array.isArray(res.data) ? res.data : (res.data.records || [])
+          self.dimensionProducts = list.slice(0, 4)
+        }
+      }).catch(function() { self.dimensionProducts = [] })
+    },
+    onTaskClick: function(task) {
+      uni.navigateTo({ url: '/pages/tasks/tasks' })
+    },
+    goActivityDetail: function(act) {
+      if (act && act.id) uni.navigateTo({ url: '/pages/discover-detail/activity-detail/activity-detail?id=' + act.id })
+    },
+    goMoreActivities: function() {
+      uni.navigateTo({ url: '/pages/discover/index?type=activity&dimension=body' })
+    },
+    goProductDetail: function(prod) {
+      if (prod && prod.id) uni.navigateTo({ url: '/pages/discover-detail/product-detail/product-detail?id=' + prod.id })
+    },
+    goMoreProducts: function() {
+      uni.navigateTo({ url: '/pages/discover/index?type=product&domain=body' })
+    },
+    goTasks: function() { uni.switchTab({ url: '/pages/tasks/tasks' }) },
+    goBack: function() { uni.navigateBack() }
+  }
+}
+</script>
+
+<style scoped>
+.detail-container {
+  min-height: 100vh;
+  background: #f5f7fa;
+  padding-bottom: 40rpx;
+}
+.nav-bar {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  padding: 20rpx 30rpx;
+  background: #fff;
+}
+.nav-back {
+  padding: 10rpx;
+}
+.nav-back-icon {
+  font-size: 36rpx;
+  color: #333;
+}
+.nav-title {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #333;
+  margin-left: 16rpx;
+}
+.member-card {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  margin: 20rpx 30rpx;
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 24rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
+}
+.member-avatar {
+  width: 80rpx;
+  height: 80rpx;
+  border-radius: 40rpx;
+  background: linear-gradient(135deg, #4CAF50, #81C784);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 20rpx;
+}
+.avatar-text {
+  font-size: 32rpx;
+  color: #fff;
+  font-weight: bold;
+}
+.member-info {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+}
+.member-name {
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #333;
+}
+.member-role {
+  font-size: 24rpx;
+  color: #999;
+  margin-top: 4rpx;
+}
+.member-score {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+.score-value {
+  font-size: 40rpx;
+  font-weight: bold;
+  color: #4CAF50;
+}
+.score-label {
+  font-size: 22rpx;
+  color: #999;
+}
+.bottom-spacer {
+  height: 40rpx;
+}
+</style>

+ 6 - 6
cfc-frontend/pages/body/index.vue

@@ -513,13 +513,13 @@ export default {
       uni.navigateTo({ url: '/pages/tasks/tasks' })
     },
     goActivityDetail: function(act) {
-      if (act && act.id) uni.navigateTo({ url: '/pages/discover/activity-detail/activity-detail?id=' + act.id })
+      uni.navigateTo({ url: '/pages/discover-detail/activity-detail/activity-detail?id=' + act.id })
     },
     goMoreActivities: function() {
       uni.navigateTo({ url: '/pages/discover/index?type=activity&dimension=body' })
     },
     goProductDetail: function(prod) {
-      if (prod && prod.id) uni.navigateTo({ url: '/pages/discover/product-detail/product-detail?id=' + prod.id })
+      uni.navigateTo({ url: '/pages/discover-detail/product-detail/product-detail?id=' + prod.id })
     },
     goMoreProducts: function() {
       uni.navigateTo({ url: '/pages/discover/index?type=product&domain=body' })
@@ -528,7 +528,7 @@ export default {
     goMemberDetail: function(member) {
       if (!member || !member.memberId) return
       uni.navigateTo({
-        url: '/pages/body/member-body-detail?childId=' + member.memberId
+        url: '/pages/body-detail/member-body-detail?childId=' + member.memberId
       })
     },
     sectionVisible: function(key) {
@@ -550,7 +550,7 @@ export default {
           uni.showToast({ title: '请先选择孩子', icon: 'none' })
           return
         }
-        uni.navigateTo({ url: '/pages/body/health-dimensions?childId=' + this.activeChildId })
+        uni.navigateTo({ url: '/pages/body-detail/health-dimensions?childId=' + this.activeChildId })
         return
       }
       if (item.page) uni.navigateTo({ url: item.page })
@@ -560,7 +560,7 @@ export default {
         uni.showToast({ title: '请先选择孩子', icon: 'none' })
         return
       }
-      uni.navigateTo({ url: '/pages/body/health-report?childId=' + this.activeChildId })
+      uni.navigateTo({ url: '/pages/body-detail/health-report?childId=' + this.activeChildId })
     },
     goDimensionDetail: function(dimKey) {
       uni.navigateTo({ url: '/pages/body/dimension-detail?dimension=' + dimKey + '&childId=' + this.activeChildId })
@@ -570,7 +570,7 @@ export default {
         uni.showToast({ title: '请先选择孩子', icon: 'none' })
         return
       }
-      uni.navigateTo({ url: '/pages/body/checkin?childId=' + this.activeChildId })
+      uni.navigateTo({ url: '/pages/body-detail/checkin?childId=' + this.activeChildId })
     },
     goToNutritionProfile: function() {
       if (!this.activeChildId) {

+ 371 - 0
cfc-frontend/pages/discover-detail/activity-detail/activity-detail.vue

@@ -0,0 +1,371 @@
+<template>
+  <view class="container">
+    <!-- 加载状态 -->
+    <view v-if="loading" class="loading-wrap">
+      <view class="loading-spinner"></view>
+      <text class="loading-text">加载中...</text>
+    </view>
+
+    <!-- 错误状态 -->
+    <view v-else-if="error" class="error-wrap">
+      <text class="error-icon">📋</text>
+      <text class="error-text">{{ errorMsg }}</text>
+      <button class="retry-btn" @click="loadDetail(activityId)">重新加载</button>
+    </view>
+
+    <!-- 活动内容 -->
+    <scroll-view v-else-if="activity.id" scroll-y class="content">
+      <!-- 封面图 -->
+      <image
+        v-if="activity.coverImage"
+        class="cover"
+        :src="activity.coverImage"
+        mode="widthFix"
+      />
+      <image v-else class="cover placeholder-cover" src="/static/default-activity.png" mode="aspectFill" />
+
+      <view class="info-section">
+        <text class="activity-title">{{ activity.title }}</text>
+
+        <!-- 活动状态标签 -->
+        <view class="status-row">
+          <text class="status-tag" :class="'status-' + (activity.status || 'upcoming')">{{ statusText(activity.status) }}</text>
+        </view>
+
+        <!-- 活动元信息 -->
+        <view class="meta-list">
+          <view class="meta-item" v-if="activity.startTime">
+            <text class="meta-icon">🕐</text>
+            <text class="meta-text">{{ activity.startTime }}{{ activity.endTime ? ' ~ ' + activity.endTime : '' }}</text>
+          </view>
+          <view class="meta-item" v-if="activity.location">
+            <text class="meta-icon">📍</text>
+            <text class="meta-text">{{ activity.location }}</text>
+          </view>
+          <view class="meta-item" v-if="activity.maxParticipants">
+            <text class="meta-icon">👥</text>
+            <text class="meta-text">{{ activity.currentParticipants || 0 }} / {{ activity.maxParticipants }} 人</text>
+          </view>
+          <view class="meta-item">
+            <text class="meta-icon">💰</text>
+            <text class="meta-text">{{ activity.priceLabel || (activity.price > 0 ? '¥' + activity.price : '免费') }}</text>
+          </view>
+        </view>
+
+        <!-- 活动简介 -->
+        <view class="desc-section" v-if="activity.description">
+          <text class="section-title">活动介绍</text>
+          <text class="desc-text">{{ activity.description }}</text>
+        </view>
+      </view>
+
+      <!-- 底部操作栏 -->
+      <view class="bottom-bar">
+        <view class="price-info">
+          <text class="bottom-type">活动</text>
+          <text class="bottom-vendor" v-if="activity.vendorName">{{ activity.vendorName }}</text>
+        </view>
+        <view class="action-btns">
+          <button
+            class="btn-checkin"
+            :class="{ 'btn-checkin-done': checkinDone, 'btn-checkin-loading': checkinLoading }"
+            :disabled="checkinLoading || checkinDone"
+            @click="onCheckin"
+          >
+            <text v-if="checkinLoading">签到中...</text>
+            <text v-else-if="checkinDone">已签到 ✓</text>
+            <text v-else>签到</text>
+          </button>
+        </view>
+      </view>
+    </scroll-view>
+
+    <!-- 空状态 -->
+    <view v-else class="empty-wrap">
+      <text class="empty-text">活动不存在或已下架</text>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getActivityDetail, activityCheckin } from '@/utils/api.js'
+
+export default {
+  data() {
+    return {
+      activityId: null,
+      activity: {},
+      loading: false,
+      error: false,
+      errorMsg: '',
+      checkinLoading: false,
+      checkinDone: false,
+      checkinPoints: 0
+    }
+  },
+  onLoad: function(options) {
+    if (options && options.id) {
+      this.activityId = options.id
+      this.loadDetail(options.id)
+    } else {
+      this.error = true
+      this.errorMsg = '缺少活动ID'
+    }
+
+    // 检查是否已经签到(从路由参数获取)
+    if (options && options.checkinDone === '1') {
+      this.checkinDone = true
+    }
+  },
+  methods: {
+    loadDetail: function(id) {
+      var self = this
+      self.loading = true
+      self.error = false
+      getActivityDetail(id).then(function(res) {
+        self.loading = false
+        if (res && res.code === 200 && res.data) {
+          self.activity = res.data
+        } else {
+          self.error = true
+          self.errorMsg = (res && res.message) || '加载失败'
+        }
+      }).catch(function(err) {
+        self.loading = false
+        self.error = true
+        self.errorMsg = '网络异常,请稍后重试'
+      })
+    },
+    statusText: function(status) {
+      var map = { draft: '草稿', published: '进行中', ended: '已结束' }
+      return map[status] || '未知'
+    },
+    onCheckin: function() {
+      var self = this
+      if (self.checkinLoading || self.checkinDone) return
+
+      var childId = uni.getStorageSync('currentChildId')
+      if (!childId) {
+        uni.showToast({ title: '请选择孩子身份', icon: 'none' })
+        return
+      }
+
+      self.checkinLoading = true
+      activityCheckin({ id: self.activity.id, childId: childId }).then(function(res) {
+        self.checkinLoading = false
+        if (res && res.code === 200 && res.data) {
+          if (res.data.alreadyCompleted) {
+            self.checkinDone = true
+            uni.showToast({ title: '已签到', icon: 'success' })
+          } else if (res.data.pointsEarned && res.data.pointsEarned > 0) {
+            self.checkinDone = true
+            self.checkinPoints = res.data.pointsEarned
+            uni.showToast({ title: '签到成功 +' + res.data.pointsEarned + '积分', icon: 'success' })
+          } else {
+            self.checkinDone = true
+            uni.showToast({ title: '签到成功', icon: 'success' })
+          }
+        } else {
+          var msg = (res && res.message) || '签到失败'
+          uni.showToast({ title: msg, icon: 'none' })
+        }
+      }).catch(function(err) {
+        self.checkinLoading = false
+        uni.showToast({ title: '网络异常', icon: 'none' })
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  min-height: 100vh;
+  background: #FFF7ED;
+}
+
+/* 加载 / 错误 / 空状态 */
+.loading-wrap,
+.error-wrap,
+.empty-wrap {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 120rpx 40rpx;
+}
+.loading-spinner {
+  width: 60rpx;
+  height: 60rpx;
+  border: 4rpx solid #FDE68A;
+  border-top-color: #F97316;
+  border-radius: 50%;
+  animation: spin 0.8s linear infinite;
+  margin-bottom: 20rpx;
+}
+@keyframes spin {
+  to { transform: rotate(360deg); }
+}
+.loading-text,
+.error-text,
+.empty-text {
+  font-size: 28rpx;
+  color: #999;
+  margin-bottom: 20rpx;
+}
+.error-icon {
+  font-size: 60rpx;
+  margin-bottom: 16rpx;
+}
+.retry-btn {
+  background: #F97316;
+  color: #fff;
+  border: none;
+  border-radius: 40rpx;
+  padding: 12rpx 40rpx;
+  font-size: 26rpx;
+  line-height: 1.5;
+}
+
+/* 内容区 */
+.content {
+  height: 100vh;
+  padding-bottom: 120rpx;
+}
+.cover {
+  width: 100%;
+  display: block;
+}
+.placeholder-cover {
+  height: 400rpx;
+  background: #f0f0f0;
+}
+
+/* 信息区 */
+.info-section {
+  padding: 30rpx;
+}
+.activity-title {
+  font-size: 36rpx;
+  font-weight: bold;
+  color: #1f2937;
+  line-height: 1.4;
+}
+.status-row {
+  margin-top: 16rpx;
+}
+.status-tag {
+  display: inline-block;
+  font-size: 22rpx;
+  padding: 4rpx 16rpx;
+  border-radius: 8rpx;
+}
+.status-draft { background: #F5F5F5; color: #999; }
+.status-published { background: #F0FDF4; color: #22C55E; }
+.status-ended { background: #F5F5F5; color: #999; }
+.status-upcoming { background: #FFF7ED; color: #F97316; }
+
+/* 元信息列表 */
+.meta-list {
+  margin-top: 24rpx;
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 24rpx;
+}
+.meta-item {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  margin-bottom: 16rpx;
+}
+.meta-item:last-child {
+  margin-bottom: 0;
+}
+.meta-icon {
+  font-size: 28rpx;
+  margin-right: 12rpx;
+  width: 32rpx;
+  text-align: center;
+}
+.meta-text {
+  font-size: 26rpx;
+  color: #4b5563;
+  flex: 1;
+  line-height: 1.4;
+}
+
+/* 活动介绍 */
+.desc-section {
+  margin-top: 30rpx;
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 24rpx;
+}
+.section-title {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #1f2937;
+  display: block;
+  margin-bottom: 12rpx;
+}
+.desc-text {
+  font-size: 26rpx;
+  color: #4b5563;
+  line-height: 1.6;
+}
+
+/* 底部操作栏 */
+.bottom-bar {
+  position: fixed;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: #fff;
+  padding: 20rpx 30rpx;
+  padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  box-shadow: 0 -2rpx 12rpx rgba(0,0,0,0.06);
+}
+.price-info {
+  display: flex;
+  flex-direction: column;
+}
+.bottom-type {
+  font-size: 22rpx;
+  color: #999;
+}
+.bottom-vendor {
+  font-size: 22rpx;
+  color: #999;
+  margin-top: 4rpx;
+}
+.action-btns {
+  display: flex;
+  flex-direction: row;
+  gap: 16rpx;
+}
+.btn-checkin {
+  background: linear-gradient(135deg, #F97316, #FB923C);
+  color: #fff;
+  border: none;
+  border-radius: 40rpx;
+  padding: 16rpx 48rpx;
+  font-size: 28rpx;
+  font-weight: 600;
+  line-height: 1.5;
+  min-width: 160rpx;
+  text-align: center;
+}
+.btn-checkin:active {
+  opacity: 0.85;
+}
+.btn-checkin-done {
+  background: #22C55E;
+}
+.btn-checkin-loading {
+  opacity: 0.7;
+}
+</style>

+ 327 - 0
cfc-frontend/pages/discover-detail/product-detail/product-detail.vue

@@ -0,0 +1,327 @@
+<template>
+  <view class="container">
+    <view v-if="loading" class="loading-wrap">
+      <text class="loading-text">加载中...</text>
+    </view>
+    <scroll-view v-else-if="product.id" scroll-y class="content">
+      <image
+        class="cover"
+        :src="product.coverImage || '/static/default-product.png'"
+        mode="widthFix"
+      />
+      <view class="info-section">
+        <view class="name-row">
+          <text class="product-name">{{ product.name }}</text>
+        </view>
+        <view class="price-row">
+          <text class="price">{{ formatPriceWithSymbol(product.price) }}</text>
+          <text v-if="product.memberPrice" class="member-price">会员价 {{ formatPriceWithSymbol(product.memberPrice) }}</text>
+        </view>
+        <view class="meta-row">
+          <text class="meta-item" v-if="product.productType">{{ typeLabel }}</text>
+          <text class="meta-item" v-if="product.domain">{{ domainLabel }}</text>
+          <text class="meta-item">库存 {{ product.stock || 0 }}</text>
+        </view>
+        <view class="vendor-row">
+          <text class="vendor-label">供应商:</text>
+          <text class="vendor-name">{{ product.vendorName || '平台官方' }}</text>
+        </view>
+      </view>
+
+      <view class="desc-section">
+        <text class="section-title">商品介绍</text>
+        <text class="desc-text">{{ product.description || '暂无介绍' }}</text>
+      </view>
+
+      <view v-if="product.intro" class="intro-section">
+        <text class="section-title">详细介绍</text>
+        <text class="intro-text">{{ product.intro }}</text>
+      </view>
+
+      <view class="bottom-bar">
+        <view class="price-info">
+          <text class="bottom-price">{{ formatPriceWithSymbol(product.price) }}</text>
+          <text v-if="product.memberPrice" class="bottom-member">会员{{ formatPriceWithSymbol(product.memberPrice) }}</text>
+        </view>
+        <view class="action-btns">
+          <button class="btn-buy" @click="onBuy">立即购买</button>
+        </view>
+      </view>
+    </scroll-view>
+    <view v-else class="empty-wrap">
+      <text class="empty-text">商品不存在或已下架</text>
+    </view>
+  </view>
+</template>
+
+<script>
+import { productDetail, productOrderCreate, productOrderPay } from '@/utils/api.js'
+
+export default {
+  data() {
+    return {
+      product: {},
+      loading: false
+    }
+  },
+  computed: {
+    typeLabel() {
+      const map = {
+        activity: '活动',
+        course: '课程',
+        physical: '实物',
+        digital: '数字',
+        service: '服务'
+      }
+      return map[this.product.productType] || this.product.productType
+    },
+    domainLabel() {
+      const map = {
+        action: '行动',
+        mind: '心智',
+        body: '身体',
+        wisdom: '智慧',
+        wealth: '财富'
+      }
+      return map[this.product.domain] || this.product.domain
+    }
+  },
+  onLoad(options) {
+    if (options.id) {
+      this.loadDetail(options.id)
+    }
+  },
+  methods: {
+    loadDetail(id) {
+      this.loading = true
+      productDetail({ id: parseInt(id) }).then(res => {
+        this.loading = false
+        if (res.code === 200 && res.data) {
+          this.product = res.data
+        }
+      }).catch(() => {
+        this.loading = false
+      })
+    },
+    onBuy() {
+      const userId = uni.getStorageSync('userId')
+      if (!userId) {
+        uni.navigateTo({ url: '/pages/login/login' })
+        return
+      }
+      uni.showLoading({ title: '创建订单...' })
+      productOrderCreate({
+        productId: this.product.id,
+        quantity: 1,
+        paymentMethod: 'wechat'
+      }).then(res => {
+        uni.hideLoading()
+        if (res.code === 200 && res.data) {
+          const orderNo = res.data.orderNo
+          uni.showModal({
+            title: '订单已创建',
+            content: '订单号:' + orderNo + ',确定支付吗?',
+            success: (confirm) => {
+              if (confirm.confirm) {
+                this.doPay(orderNo)
+              }
+            }
+          })
+        } else {
+          uni.showToast({ title: res.message || '创建订单失败', icon: 'none' })
+        }
+      }).catch(() => {
+        uni.hideLoading()
+        uni.showToast({ title: '创建订单失败', icon: 'none' })
+      })
+    },
+    doPay(orderNo) {
+      uni.showLoading({ title: '支付中...' })
+      productOrderPay({ orderNo }).then(res => {
+        uni.hideLoading()
+        if (res.code === 200) {
+          uni.showToast({ title: '支付成功', icon: 'success' })
+          setTimeout(() => {
+            uni.navigateTo({ url: '/pages/shop/order-list/order-list' })
+          }, 1500)
+        } else {
+          uni.showToast({ title: res.message || '支付失败', icon: 'none' })
+        }
+      }).catch(() => {
+        uni.hideLoading()
+        uni.showToast({ title: '支付失败', icon: 'none' })
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  min-height: 100vh;
+  background: #f5f5f5;
+}
+.loading-wrap,
+.empty-wrap {
+  display: flex;
+  justify-content: center;
+  padding-top: 200rpx;
+}
+.loading-text,
+.empty-text {
+  font-size: 28rpx;
+  color: #999;
+}
+.content {
+  height: calc(100vh - 120rpx);
+  padding-bottom: 120rpx;
+}
+.cover {
+  width: 100%;
+  background: #f0f0f0;
+}
+.info-section {
+  background: #fff;
+  padding: 30rpx;
+  margin-bottom: 20rpx;
+}
+.name-row {
+  margin-bottom: 16rpx;
+}
+.product-name {
+  font-size: 34rpx;
+  font-weight: bold;
+  color: #333;
+  line-height: 1.4;
+}
+.price-row {
+  display: flex;
+  align-items: baseline;
+  margin-bottom: 16rpx;
+}
+.price {
+  font-size: 40rpx;
+  color: #F97316;
+  font-weight: bold;
+  margin-right: 20rpx;
+}
+.member-price {
+  font-size: 26rpx;
+  color: #999;
+}
+.meta-row {
+  display: flex;
+  flex-wrap: wrap;
+  margin-bottom: 12rpx;
+}
+.meta-item {
+  font-size: 22rpx;
+  color: #999;
+  background: #f5f5f5;
+  padding: 4rpx 16rpx;
+  border-radius: 4rpx;
+  margin-right: 12rpx;
+  margin-bottom: 8rpx;
+}
+.vendor-row {
+  display: flex;
+  align-items: center;
+}
+.vendor-label {
+  font-size: 24rpx;
+  color: #999;
+}
+.vendor-name {
+  font-size: 24rpx;
+  color: #666;
+}
+.desc-section,
+.intro-section {
+  background: #fff;
+  padding: 30rpx;
+  margin-bottom: 20rpx;
+}
+.section-title {
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #333;
+  margin-bottom: 16rpx;
+  display: block;
+}
+.desc-text,
+.intro-text {
+  font-size: 26rpx;
+  color: #666;
+  line-height: 1.6;
+  display: block;
+}
+.bottom-bar {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  height: 100rpx;
+  background: #fff;
+  border-top: 1rpx solid #eee;
+  display: flex;
+  align-items: center;
+  padding: 0 30rpx;
+  justify-content: space-between;
+  z-index: 100;
+}
+.price-info {
+  display: flex;
+  align-items: baseline;
+}
+.bottom-price {
+  font-size: 36rpx;
+  color: #F97316;
+  font-weight: bold;
+}
+.bottom-member {
+  font-size: 24rpx;
+  color: #999;
+  margin-left: 10rpx;
+}
+.action-btns {
+  display: flex;
+  align-items: center;
+}
+.btn-buy {
+  background: linear-gradient(135deg, #F97316, #FB923C);
+  color: #fff;
+  font-size: 28rpx;
+  font-weight: bold;
+  padding: 0 60rpx;
+  height: 72rpx;
+  line-height: 72rpx;
+  border-radius: 36rpx;
+  border: none;
+}
+.btn-buy::after {
+  border: none;
+}
+.action-extra {
+  display: flex;
+  align-items: center;
+  margin-right: 20rpx;
+}
+.btn-favorite {
+  font-size: 40rpx;
+  margin-right: 16rpx;
+  padding: 0 8rpx;
+}
+.btn-cart {
+  background: #fff;
+  color: #F97316;
+  border: 2rpx solid #F97316;
+  font-size: 26rpx;
+  padding: 0 24rpx;
+  height: 64rpx;
+  line-height: 64rpx;
+  border-radius: 32rpx;
+}
+.btn-cart::after {
+  border: none;
+}
+</style>

+ 0 - 1
cfc-frontend/pages/health/report-survey.vue

@@ -318,7 +318,6 @@ export default {
       }
     }
   }
-  }
 }
 </script>
 

+ 401 - 0
cfc-frontend/pages/mind-detail/article-detail.vue

@@ -0,0 +1,401 @@
+<template>
+  <view class="detail-container">
+    <!-- 加载状态 -->
+    <view v-if="loading" class="loading-wrap">
+      <view class="loading-spinner"></view>
+      <text class="loading-text">加载中...</text>
+    </view>
+
+    <!-- 错误状态 -->
+    <view v-else-if="error" class="error-wrap">
+      <text class="error-icon">📄</text>
+      <text class="error-text">{{ errorMsg }}</text>
+      <button class="retry-btn" @click="loadDetail(articleId)">重新加载</button>
+    </view>
+
+    <!-- 文章内容 -->
+    <view v-else-if="article" class="detail-content">
+      <!-- 封面图 -->
+      <image
+        v-if="article.coverImage"
+        class="detail-cover"
+        :src="article.coverImage"
+        mode="widthFix"
+      />
+
+      <!-- 文章标题 -->
+      <text class="detail-title">{{ article.title }}</text>
+
+      <!-- 元信息 -->
+      <view class="detail-meta">
+        <text class="meta-author">{{ article.author || '浠艾福' }}</text>
+        <text class="meta-separator">|</text>
+        <text class="meta-date">{{ getFormattedDate() }}</text>
+        <text class="meta-separator">|</text>
+        <text class="meta-read">{{ article.readTime || 3 }} 分钟阅读</text>
+      </view>
+
+      <!-- 分类标签 -->
+      <view class="detail-category-row">
+        <text class="detail-category">{{ article.categoryName || '' }}</text>
+      </view>
+
+      <!-- 文章标签 -->
+      <view v-if="tagList.length" class="detail-tags">
+        <text v-for="(tag, idx) in tagList" :key="idx" class="tag-item">{{ tag }}</text>
+      </view>
+
+      <!-- 分割线 -->
+      <view class="divider"></view>
+
+      <!-- 文章正文(富文本) -->
+      <view class="detail-body">
+        <rich-text :nodes="article.content" />
+      </view>
+
+      <!-- 底部完成按钮 -->
+      <view class="detail-footer">
+        <!-- 定时器显示 -->
+        <view class="reading-timer" v-if="readingSeconds >= 0">
+          <text class="timer-icon">⏱</text>
+          <text class="timer-text">{{ formatTime(readingSeconds) }}</text>
+          <text class="timer-hint" v-if="readingSeconds < 60">阅读{{ 60 - readingSeconds }}秒可获取积分</text>
+          <text class="timer-hint timer-ready" v-else>已达标,可领取积分</text>
+        </view>
+        <!-- 完成按钮 -->
+        <button
+          class="read-btn"
+          :class="{ 'read-btn-active': readingSeconds >= 60 }"
+          @click="onReadComplete"
+        >阅读完成</button>
+      </view>
+    </view>
+
+    <!-- 空状态 -->
+    <view v-else class="error-wrap">
+      <text class="error-icon">📄</text>
+      <text class="error-text">文章不存在</text>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getArticleDetail, recordArticleRead } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      articleId: '',
+      article: null,
+      loading: true,
+      error: false,
+      errorMsg: '',
+      tagList: [],
+      readingSeconds: 0,
+      readingTimer: null,
+      readRecorded: false
+    }
+  },
+  onLoad(options) {
+    if (options && options.id) {
+      this.articleId = options.id
+      this.loadDetail(options.id)
+      this.startReadingTimer()
+    } else {
+      this.error = true
+      this.errorMsg = '参数错误'
+      this.loading = false
+    }
+  },
+  onHide() {
+    this.stopReadingTimer()
+  },
+  onUnload() {
+    this.stopReadingTimer()
+  },
+  methods: {
+    async loadDetail(id) {
+      this.loading = true
+      this.error = false
+      try {
+        const res = await getArticleDetail({ id: id })
+        if (res.code === 200 && res.data) {
+          this.article = res.data
+          // 解析标签 JSON 字符串
+          if (res.data.tags) {
+            try {
+              let parsed = JSON.parse(res.data.tags)
+              this.tagList = Array.isArray(parsed) ? parsed : []
+            } catch (e) {
+              this.tagList = []
+            }
+          }
+        } else {
+          this.error = true
+          this.errorMsg = '文章不存在或无权限查看'
+        }
+      } catch (e) {
+        this.error = true
+        this.errorMsg = '加载失败,请稍后重试'
+      } finally {
+        this.loading = false
+      }
+    },
+    getFormattedDate() {
+      if (this.article && this.article.publishedAt) {
+        return this.article.publishedAt.slice(0, 10)
+      }
+      return ''
+    },
+    startReadingTimer() {
+      var self = this
+      this.readingTimer = setInterval(function() {
+        self.readingSeconds = self.readingSeconds + 1
+        // Auto-submit when reaching 60 seconds (threshold)
+        // But don't auto-submit - just indicate readiness
+      }, 1000)
+    },
+    stopReadingTimer() {
+      if (this.readingTimer) {
+        clearInterval(this.readingTimer)
+        this.readingTimer = null
+      }
+    },
+    formatTime(seconds) {
+      var m = Math.floor(seconds / 60)
+      var s = seconds % 60
+      return (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s)
+    },
+    async onReadComplete() {
+      if (this.readRecorded) {
+        uni.navigateBack()
+        return
+      }
+      this.stopReadingTimer()
+      try {
+        var childId = uni.getStorageSync('currentChildId')
+        var res = await recordArticleRead({
+          id: this.article.id,
+          durationSeconds: this.readingSeconds,
+          childId: childId || undefined
+        })
+        this.readRecorded = true
+        if (res.data && res.data.pointsEarned > 0) {
+          uni.showToast({ title: '获得' + res.data.pointsEarned + '积分!', icon: 'success' })
+        } else if (res.data && res.data.belowThreshold) {
+          uni.showToast({ title: '阅读时间未达标', icon: 'none' })
+        } else if (res.data && res.data.alreadyCompleted) {
+          uni.showToast({ title: '已读过此文', icon: 'none' })
+        } else {
+          uni.showToast({ title: '阅读记录已保存', icon: 'success' })
+        }
+      } catch (e) {
+        uni.showToast({ title: '记录保存失败', icon: 'none' })
+      }
+      var self = this
+      setTimeout(function() {
+        uni.navigateBack()
+      }, 1500)
+    }
+  }
+}
+</script>
+
+<style scoped>
+.detail-container {
+  min-height: 100vh;
+  background: #fff;
+  padding-bottom: 120rpx;
+}
+
+/* ===== 加载状态 ===== */
+.loading-wrap {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding-top: 300rpx;
+}
+.loading-spinner {
+  width: 60rpx;
+  height: 60rpx;
+  border: 4rpx solid #e0e0e0;
+  border-top-color: #5B9BD5;
+  border-radius: 50%;
+  animation: spin 0.8s linear infinite;
+  margin-bottom: 20rpx;
+}
+@keyframes spin {
+  0% { transform: rotate(0deg); }
+  100% { transform: rotate(360deg); }
+}
+.loading-text {
+  font-size: 26rpx;
+  color: #999;
+}
+
+/* ===== 错误状态 ===== */
+.error-wrap {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding-top: 300rpx;
+}
+.error-icon {
+  font-size: 100rpx;
+  margin-bottom: 24rpx;
+}
+.error-text {
+  font-size: 28rpx;
+  color: #999;
+  margin-bottom: 30rpx;
+}
+.retry-btn {
+  width: 240rpx;
+  height: 72rpx;
+  line-height: 72rpx;
+  background: #5B9BD5;
+  color: #fff;
+  font-size: 28rpx;
+  border-radius: 36rpx;
+  text-align: center;
+  border: none;
+}
+.retry-btn::after {
+  border: none;
+}
+
+/* ===== 内容区 ===== */
+.detail-cover {
+  width: 100%;
+  max-height: 500rpx;
+  display: block;
+}
+.detail-title {
+  display: block;
+  font-size: 36rpx;
+  font-weight: bold;
+  color: #333;
+  line-height: 1.4;
+  padding: 30rpx 30rpx 0;
+}
+
+/* ===== 元信息 ===== */
+.detail-meta {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  padding: 16rpx 30rpx 0;
+  font-size: 22rpx;
+  color: #999;
+}
+.meta-author {
+  color: #5B9BD5;
+}
+.meta-separator {
+  margin: 0 12rpx;
+  color: #ddd;
+}
+.meta-date,
+.meta-read {
+  color: #999;
+}
+
+/* ===== 分类标签 ===== */
+.detail-category-row {
+  padding: 16rpx 30rpx 0;
+}
+.detail-category {
+  display: inline-block;
+  font-size: 20rpx;
+  color: #5B9BD5;
+  background: rgba(91, 155, 213, 0.1);
+  padding: 4rpx 16rpx;
+  border-radius: 8rpx;
+}
+
+/* ===== 标签 ===== */
+.detail-tags {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+  padding: 12rpx 30rpx 0;
+  gap: 12rpx;
+}
+.tag-item {
+  font-size: 20rpx;
+  color: #8B5CF6;
+  background: rgba(139, 92, 246, 0.08);
+  padding: 4rpx 14rpx;
+  border-radius: 8rpx;
+}
+
+/* ===== 分割线 ===== */
+.divider {
+  height: 1rpx;
+  background: #eee;
+  margin: 24rpx 30rpx;
+}
+
+/* ===== 正文 ===== */
+.detail-body {
+  padding: 0 30rpx;
+  font-size: 28rpx;
+  color: #444;
+  line-height: 1.8;
+}
+.detail-body rich-text {
+  word-break: break-word;
+}
+
+/* ===== 定时器 ===== */
+.reading-timer {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: center;
+  padding: 16rpx 0 20rpx;
+}
+.timer-icon {
+  font-size: 32rpx;
+  margin-right: 8rpx;
+}
+.timer-text {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #333;
+  margin-right: 16rpx;
+}
+.timer-hint {
+  font-size: 22rpx;
+  color: #999;
+}
+.timer-ready {
+  color: #22c55e;
+  font-weight: bold;
+}
+
+/* ===== 底部按钮 ===== */
+.detail-footer {
+  padding: 40rpx 60rpx;
+}
+.read-btn {
+  width: 100%;
+  height: 88rpx;
+  line-height: 88rpx;
+  background: #ddd;
+  color: #fff;
+  font-size: 32rpx;
+  font-weight: bold;
+  border-radius: 44rpx;
+  text-align: center;
+  border: none;
+  transition: background 0.3s;
+}
+.read-btn-active {
+  background: linear-gradient(135deg, #5B9BD5, #3A7CC4);
+}
+.read-btn::after {
+  border: none;
+}
+</style>

+ 348 - 0
cfc-frontend/pages/mind-detail/articles.vue

@@ -0,0 +1,348 @@
+<template>
+  <view class="container">
+    <!-- 品牌头部 -->
+    <PageBanner theme="mind-wisdom"
+      tagline="探索内心世界,培养健康心智"
+      quote="知己知彼,百战不殆" />
+
+    <!-- 分类筛选 -->
+    <view class="filter-section">
+      <scroll-view scroll-x enable-flex show-scrollbar="false" class="category-scroll">
+        <view class="filter-chips">
+          <view
+            v-for="cat in categoryList"
+            :key="cat.value"
+            :class="['filter-chip', currentCategory === cat.value ? 'active' : '']"
+            @click="onCategoryChange(cat.value)"
+          >
+            {{ cat.label }}
+          </view>
+        </view>
+      </scroll-view>
+    </view>
+
+    <!-- 文章列表 -->
+    <scroll-view scroll-y class="article-list" @scrolltolower="onLoadMore">
+      <view v-if="loading && articles.length === 0" class="loading-wrap">
+        <text class="loading-text">加载中...</text>
+      </view>
+      <view v-else-if="articles.length === 0" class="empty-wrap">
+        <text class="empty-icon">📝</text>
+        <text class="empty-text">暂无相关文章</text>
+      </view>
+      <view v-else class="article-grid">
+        <view
+          v-for="item in articles"
+          :key="item.id"
+          class="article-card"
+          @click="goDetail(item.id)"
+        >
+          <image
+            class="article-cover"
+            :src="item.coverImage || '/static/default-article.png'"
+            mode="aspectFill"
+          />
+          <view class="article-body">
+            <view class="article-meta">
+              <text class="article-category">{{ item.categoryName || '心理健康' }}</text>
+              <text class="article-date">{{ item.publishDate || '' }}</text>
+            </view>
+            <text class="article-title">{{ item.title }}</text>
+            <text class="article-summary">{{ item.summary || item.content }}</text>
+            <view class="article-footer">
+              <text class="article-author">{{ item.author || '浠艾福' }}</text>
+              <text class="article-read-count">阅读 {{ item.readCount || 0 }}</text>
+            </view>
+          </view>
+        </view>
+      </view>
+      <view v-if="loadingMore" class="loading-more">
+        <text class="loading-text">加载中...</text>
+      </view>
+      <view v-if="noMore && articles.length > 0" class="no-more">
+        <text class="no-more-text">— 没有更多了 —</text>
+      </view>
+      <!-- 未登录提示 -->
+      <view v-if="!isLoggedIn" class="login-hint-bar">
+        <text class="login-hint-text">🔒 登录后解锁全部文章</text>
+      </view>
+      <!-- 底部占位 -->
+      <view class="bottom-spacer"></view>
+    </scroll-view>
+
+
+  </view>
+</template>
+
+<script>
+import PageBanner from '../../components/PageBanner.vue'
+import { getArticleCategories, getArticleList } from '../../utils/api.js'
+
+export default {
+  components: { PageBanner },
+  data() {
+    return {
+      categoryList: [{ id: '', name: '全部' }],
+      categoryMap: {},
+      currentCategory: '',
+      articles: [],
+      page: 1,
+      size: 10,
+      total: 0,
+      loading: false,
+      noMore: false,
+      isLoggedIn: false
+    }
+  },
+  onLoad() {
+    this.isLoggedIn = !!uni.getStorageSync('token')
+    this.loadCategories()
+  },
+  onShow() {
+    this.isLoggedIn = !!uni.getStorageSync('token')
+  },
+  methods: {
+    async loadCategories() {
+      try {
+        const res = await getArticleCategories()
+        if (res.code === 200 && res.data) {
+          const cats = Array.isArray(res.data) ? res.data : []
+          let map = {}
+          cats.forEach(function(c) { map[c.id] = c.name })
+          this.categoryMap = map
+          this.categoryList = [{ id: '', name: '全部' }].concat(cats)
+        }
+      } catch (e) {
+        // 默认分类列表
+        this.categoryList = [
+          { id: '', name: '全部' },
+          { id: 1, name: '心理健康' },
+          { id: 2, name: '情绪管理' }
+        ]
+      }
+      // 分类加载完成后加载文章
+      this.loadArticles()
+    },
+    onCategoryChange(id) {
+      this.currentCategory = id
+      this.page = 1
+      this.articles = []
+      this.noMore = false
+      this.loadArticles()
+    },
+    async loadArticles() {
+      if (this.loading) return
+      this.loading = true
+      try {
+        let params = { page: this.page, size: this.size }
+        if (this.currentCategory) {
+          params.categoryId = this.currentCategory
+        }
+        const res = await getArticleList(params)
+        if (res.code === 200 && res.data) {
+          let pageData = res.data
+          let list = pageData.records || []
+          let mapped = list.map(function(item) {
+            return {
+              id: item.id,
+              title: item.title || '',
+              summary: item.summary || '',
+              content: item.content || '',
+              coverImage: item.coverImage || '',
+              author: item.author || '浠艾福',
+              categoryName: item.categoryName || item.category || '',
+              publishDate: item.publishedAt ? item.publishedAt.slice(0, 10) : '',
+              readCount: item.viewCount || 0
+            }
+          })
+          if (this.page === 1) {
+            this.articles = mapped
+          } else {
+            this.articles = this.articles.concat(mapped)
+          }
+          this.total = pageData.total || 0
+          this.noMore = this.articles.length >= this.total
+        } else {
+          if (this.page === 1) {
+            this.articles = []
+          }
+        }
+      } catch (e) {
+        uni.showToast({ title: '加载失败', icon: 'none' })
+      } finally {
+        this.loading = false
+      }
+    },
+    onLoadMore() {
+      if (this.noMore || this.loading) return
+      this.page++
+      this.loadArticles()
+    },
+    goDetail(id) {
+      if (this.isLoggedIn) {
+        uni.navigateTo({ url: '/pages/mind/article-detail?id=' + id })
+      } else {
+        uni.navigateTo({ url: '/pages/login/login?redirect=' + encodeURIComponent('/pages/mind/articles') })
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  display: flex;
+  flex-direction: column;
+  min-height: 100vh;
+  background: #f5f7fa;
+}
+
+/* 分类筛选 */
+.filter-section {
+  background: #fff;
+  padding: 16rpx 0 12rpx;
+  border-bottom: 1rpx solid #eee;
+}
+.category-scroll {
+  white-space: nowrap;
+}
+.filter-chips {
+  display: flex;
+  padding: 0 20rpx;
+  gap: 16rpx;
+}
+.filter-chip {
+  display: inline-block;
+  padding: 8rpx 24rpx;
+  font-size: 24rpx;
+  color: #666;
+  background: #f0f0f0;
+  border-radius: 30rpx;
+  flex-shrink: 0;
+}
+.filter-chip.active {
+  background: #5B9BD5;
+  color: #fff;
+}
+
+/* 文章列表 */
+.article-list {
+  flex: 1;
+  height: calc(100vh - 220rpx);
+}
+.loading-wrap,
+.empty-wrap {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding-top: 120rpx;
+}
+.loading-text {
+  font-size: 26rpx;
+  color: #999;
+}
+.empty-icon {
+  font-size: 100rpx;
+  margin-bottom: 24rpx;
+}
+.empty-text {
+  font-size: 28rpx;
+  color: #999;
+}
+.article-grid {
+  padding: 20rpx 24rpx;
+}
+.article-card {
+  background: #fff;
+  border-radius: 16rpx;
+  overflow: hidden;
+  margin-bottom: 20rpx;
+  box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06);
+}
+.article-cover {
+  width: 100%;
+  height: 280rpx;
+  background: linear-gradient(135deg, #667eea, #764ba2);
+}
+.article-body {
+  padding: 20rpx;
+}
+.article-meta {
+  display: flex;
+  align-items: center;
+  margin-bottom: 10rpx;
+}
+.article-category {
+  font-size: 20rpx;
+  color: #5B9BD5;
+  background: rgba(91,155,213,0.1);
+  padding: 2rpx 12rpx;
+  border-radius: 8rpx;
+  margin-right: 12rpx;
+}
+.article-date {
+  font-size: 20rpx;
+  color: #999;
+}
+.article-title {
+  display: block;
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #333;
+  margin-bottom: 8rpx;
+  line-height: 1.4;
+}
+.article-summary {
+  display: block;
+  font-size: 24rpx;
+  color: #666;
+  line-height: 1.5;
+  margin-bottom: 12rpx;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: -webkit-box;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
+}
+.article-footer {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+.article-author {
+  font-size: 20rpx;
+  color: #999;
+}
+.article-read-count {
+  font-size: 20rpx;
+  color: #bbb;
+}
+.loading-more,
+.no-more {
+  text-align: center;
+  padding: 30rpx;
+}
+.no-more-text {
+  font-size: 24rpx;
+  color: #ccc;
+}
+
+/* 未登录提示 */
+.login-hint-bar {
+  text-align: center;
+  padding: 20rpx;
+  background: #fef9e7;
+  border-top: 1rpx solid #f0e6c0;
+  margin: 20rpx 24rpx 0;
+  border-radius: 12rpx;
+}
+.login-hint-text {
+  font-size: 24rpx;
+  color: #b8860b;
+}
+
+/* 底部占位 */
+.bottom-spacer {
+  height: 120rpx;
+}
+</style>

+ 679 - 0
cfc-frontend/pages/mind-detail/emotion-checkin.vue

@@ -0,0 +1,679 @@
+<template>
+  <view class="checkin-container">
+    <!-- 顶部:日期 + 连续打卡 -->
+    <view class="checkin-header">
+      <text class="checkin-date">{{ todayDisplay }}</text>
+      <view class="streak-badge" v-if="stats && stats.currentStreak > 0">
+        <text class="streak-icon">&#x1F525;</text>
+        <text class="streak-text">连续{{ stats.currentStreak }}天</text>
+      </view>
+    </view>
+
+    <!-- 天气选择 -->
+    <view class="section">
+      <text class="section-title">今天心情怎么样?</text>
+      <view class="weather-grid">
+        <view
+          v-for="w in weatherOptions"
+          :key="w.value"
+          :class="['weather-btn', selectedWeather === w.value ? 'weather-active' : '']"
+          :style="{ borderColor: selectedWeather === w.value ? w.color : 'transparent' }"
+          @click="selectedWeather = w.value">
+          <text class="weather-icon">{{ w.icon }}</text>
+          <text class="weather-label">{{ w.label }}</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 情绪标签 -->
+    <view class="section">
+      <text class="section-title">情绪标签(可多选)</text>
+      <view class="tag-grid">
+        <view
+          v-for="tag in emotionTags"
+          :key="tag"
+          :class="['tag-btn', selectedTags.indexOf(tag) !== -1 ? 'tag-active' : '']"
+          @click="toggleTag(tag)">
+          <text>{{ tag }}</text>
+        </view>
+      </view>
+      <view class="custom-tag-row" v-if="showCustomTagInput">
+        <input class="custom-tag-input" v-model="customTag" placeholder="输入自定义情绪" />
+        <text class="custom-tag-add" @click="addCustomTag">添加</text>
+      </view>
+      <text class="custom-tag-trigger" v-if="!showCustomTagInput" @click="showCustomTagInput = true">+ 自定义标签</text>
+    </view>
+
+    <!-- 拍照 -->
+    <view class="section">
+      <text class="section-title">拍一张照片记录心情</text>
+      <view class="photo-area" @click="takePhoto">
+        <image v-if="photoPath" :src="photoPath" mode="aspectFill" class="photo-preview" />
+        <view v-else class="photo-placeholder">
+          <text class="photo-icon">&#x1F4F7;</text>
+          <text class="photo-hint">点击拍照</text>
+        </view>
+      </view>
+      <text class="photo-note">照片仅对自己可见</text>
+    </view>
+
+    <!-- AI 情绪识别 -->
+    <view class="section" v-if="photoPath">
+      <text class="section-title">\uD83E\uDD16 AI 情绪识别</text>
+      <view class="ai-recognize-btn" v-if="!aiResult && !aiAnalyzing" @click="analyzeEmotion">
+        <text class="ai-btn-icon">\uD83D\uDD0D</text>
+        <text class="ai-btn-text">识别照片中的情绪</text>
+      </view>
+      <view class="ai-analyzing" v-if="aiAnalyzing">
+        <text class="ai-loading-text">AI 分析中...</text>
+      </view>
+      <view class="ai-result" v-if="aiResult">
+        <text class="ai-result-title">识别结果</text>
+        <view class="ai-emotion-list">
+          <view class="ai-emotion-item" v-for="em in aiResult" :key="em.label">
+            <text class="ai-emotion-label">{{ em.label }}</text>
+            <view class="ai-emotion-bar-track">
+              <view class="ai-emotion-bar-fill" :style="{ width: em.confidence + '%', background: em.color }"></view>
+            </view>
+            <text class="ai-emotion-value">{{ em.confidence }}%</text>
+          </view>
+        </view>
+        <view class="ai-apply-tags" @click="applyAiTags">
+          <text class="ai-apply-text">应用为情绪标签</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 文字记录 -->
+    <view class="section">
+      <text class="section-title">记录一下(选填)</text>
+      <textarea
+        class="note-input"
+        v-model="note"
+        placeholder="今天发生了什么?"
+        maxlength="500" />
+    </view>
+
+    <!-- 提交按钮 -->
+    <view class="submit-area">
+      <button
+        class="submit-btn"
+        :class="{ 'submit-disabled': !canSubmit }"
+        :disabled="!canSubmit"
+        @click="submitCheckin">
+        <text class="submit-icon">&#x2764;</text>
+        <text>记录心情</text>
+      </button>
+    </view>
+
+    <!-- 提交中 loading -->
+    <view class="loading-mask" v-if="submitting">
+      <view class="loading-box">
+        <text class="loading-text">记录中...</text>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import config from '../../config'
+
+export default {
+  data() {
+    return {
+      selectedWeather: '',
+      selectedTags: [],
+      customTag: '',
+      showCustomTagInput: false,
+      photoPath: '',
+      note: '',
+      submitting: false,
+      aiAnalyzing: false,
+      aiResult: null,
+      aiEmotionColors: {
+        '开心': '#10B981',
+        '平静': '#3B82F6',
+        '惊喜': '#8B5CF6',
+        '难过': '#6B9BD2',
+        '生气': '#EF4444',
+        '紧张': '#F59E0B',
+        '疲惫': '#9CA3AF',
+        '害怕': '#7C3AED'
+      },
+      stats: null,
+      weatherOptions: [
+        { value: 'sunny', icon: '\u2600\uFE0F', label: '心情很好', color: '#FFB347' },
+        { value: 'cloudy', icon: '\u26C5', label: '一般般', color: '#A0A0A0' },
+        { value: 'rainy', icon: '\uD83C\uDF27\uFE0F', label: '有点难过', color: '#6B9BD2' },
+        { value: 'stormy', icon: '\u26A1', label: '很烦躁', color: '#8B5CF6' },
+        { value: 'rainbow', icon: '\uD83C\uDF08', label: '特别开心', color: '#FF6B9D' }
+      ],
+      emotionTags: ['开心', '期待', '平静', '惊喜', '难过', '生气', '紧张', '无聊', '疲惫', '害怕']
+    }
+  },
+
+  computed: {
+    todayDisplay() {
+      const now = new Date()
+      const y = now.getFullYear()
+      const m = now.getMonth() + 1
+      const d = now.getDate()
+      const weekdays = ['日', '一', '二', '三', '四', '五', '六']
+      return y + '年' + m + '月' + d + '日 周' + weekdays[now.getDay()]
+    },
+
+    canSubmit() {
+      return !!this.selectedWeather
+    }
+  },
+
+  onLoad() {
+    this.loadStats()
+  },
+
+  methods: {
+    loadStats() {
+      const app = getApp()
+      const childId = app.globalData.currentChildId
+      if (!childId) return
+      uni.request({
+        url: config.API_BASE_URL + '/api/mind/checkin/stats',
+        method: 'POST',
+        data: { childId },
+        success: (res) => {
+          if (res.data && res.data.code === 200 && res.data.data) {
+            this.stats = res.data.data
+          }
+        }
+      })
+    },
+
+    toggleTag(tag) {
+      const idx = this.selectedTags.indexOf(tag)
+      if (idx !== -1) {
+        this.selectedTags.splice(idx, 1)
+      } else {
+        this.selectedTags.push(tag)
+      }
+    },
+
+    addCustomTag() {
+      const tag = this.customTag.trim()
+      if (tag && this.emotionTags.indexOf(tag) === -1 && this.selectedTags.indexOf(tag) === -1) {
+        this.selectedTags.push(tag)
+      }
+      this.customTag = ''
+      this.showCustomTagInput = false
+    },
+
+    takePhoto() {
+      uni.chooseImage({
+        count: 1,
+        sourceType: ['camera', 'album'],
+        success: (res) => {
+          this.photoPath = res.tempFilePaths[0]
+          this.aiResult = null
+        }
+      })
+    },
+
+    analyzeEmotion() {
+      this.aiAnalyzing = true
+      this.aiResult = null
+      // Simulate AI emotion analysis (in production, call a vision API)
+      var self = this
+      setTimeout(function() {
+        self.aiAnalyzing = false
+        // Simulated result based on the selected weather/mood as hint
+        var baseEmotions = [
+          { label: '开心', confidence: 45, color: '#10B981' },
+          { label: '平静', confidence: 30, color: '#3B82F6' },
+          { label: '惊喜', confidence: 15, color: '#8B5CF6' },
+          { label: '疲惫', confidence: 10, color: '#9CA3AF' }
+        ]
+        // Adjust based on weather selection if available
+        if (self.selectedWeather === 'rainy') {
+          baseEmotions = [
+            { label: '难过', confidence: 55, color: '#6B9BD2' },
+            { label: '疲惫', confidence: 25, color: '#9CA3AF' },
+            { label: '平静', confidence: 12, color: '#3B82F6' },
+            { label: '生气', confidence: 8, color: '#EF4444' }
+          ]
+        } else if (self.selectedWeather === 'stormy') {
+          baseEmotions = [
+            { label: '生气', confidence: 48, color: '#EF4444' },
+            { label: '紧张', confidence: 32, color: '#F59E0B' },
+            { label: '害怕', confidence: 12, color: '#7C3AED' },
+            { label: '开心', confidence: 8, color: '#10B981' }
+          ]
+        } else if (self.selectedWeather === 'rainbow') {
+          baseEmotions = [
+            { label: '开心', confidence: 60, color: '#10B981' },
+            { label: '惊喜', confidence: 25, color: '#8B5CF6' },
+            { label: '平静', confidence: 10, color: '#3B82F6' },
+            { label: '期待', confidence: 5, color: '#F59E0B' }
+          ]
+        }
+        self.aiResult = baseEmotions
+      }, 1200)
+    },
+
+    applyAiTags() {
+      if (!this.aiResult) return
+      var detected = []
+      for (var i = 0; i < this.aiResult.length; i++) {
+        var em = this.aiResult[i]
+        // Only apply high-confidence emotions as tags
+        if (em.confidence >= 20 && this.emotionTags.indexOf(em.label) !== -1) {
+          detected.push(em.label)
+        }
+      }
+      // Merge with existing selections (no duplicates)
+      var merged = this.selectedTags.slice()
+      for (var j = 0; j < detected.length; j++) {
+        if (merged.indexOf(detected[j]) === -1) {
+          merged.push(detected[j])
+        }
+      }
+      this.selectedTags = merged
+      uni.showToast({ title: '已应用情绪标签', icon: 'success' })
+    },
+
+    submitCheckin() {
+      if (!this.canSubmit || this.submitting) return
+      this.submitting = true
+
+      const app = getApp()
+      const childId = app.globalData.currentChildId
+
+      // Upload photo first if exists, then submit
+      const doSubmit = (photoUrl) => {
+        uni.request({
+          url: config.API_BASE_URL + '/api/mind/checkin/create',
+          method: 'POST',
+          data: {
+            childId: childId,
+            moodWeather: this.selectedWeather,
+            emotionTags: JSON.stringify(this.selectedTags),
+            photoUrl: photoUrl || '',
+            note: this.note
+          },
+          success: (res) => {
+            this.submitting = false
+            if (res.data && res.data.code === 200) {
+              uni.showToast({
+                title: '+5 \u5FC3\u80FD\u91CF',
+                icon: 'none',
+                duration: 2000
+              })
+              setTimeout(() => {
+                uni.navigateTo({ url: '/pages/mind/emotion-trend' })
+              }, 1500)
+            } else {
+              uni.showToast({ title: res.data && res.data.message || '提交失败', icon: 'none' })
+            }
+          },
+          fail: () => {
+            this.submitting = false
+            uni.showToast({ title: '网络错误', icon: 'none' })
+          }
+        })
+      }
+
+      if (this.photoPath) {
+        uni.uploadFile({
+          url: config.API_BASE_URL + '/api/upload',
+          filePath: this.photoPath,
+          name: 'file',
+          success: (uploadRes) => {
+            let photoUrl = ''
+            try {
+              const data = JSON.parse(uploadRes.data)
+              photoUrl = data.data || data.url || ''
+            } catch (e) {
+              photoUrl = uploadRes.data
+            }
+            doSubmit(photoUrl)
+          },
+          fail: () => doSubmit('')
+        })
+      } else {
+        doSubmit('')
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.checkin-container {
+  min-height: 100vh;
+  background: linear-gradient(180deg, #FFF0F5 0%, #FFFFFF 100%);
+  padding-bottom: 40rpx;
+}
+
+.checkin-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 30rpx 32rpx;
+}
+
+.checkin-date {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #333;
+}
+
+.streak-badge {
+  display: flex;
+  align-items: center;
+  background: linear-gradient(135deg, #FF6B9D, #FF8CB3);
+  border-radius: 30rpx;
+  padding: 8rpx 24rpx;
+}
+
+.streak-icon {
+  font-size: 28rpx;
+  margin-right: 8rpx;
+}
+
+.streak-text {
+  font-size: 24rpx;
+  color: #fff;
+  font-weight: 500;
+}
+
+.section {
+  margin: 20rpx 32rpx;
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 28rpx;
+  box-shadow: 0 2rpx 12rpx rgba(255, 107, 157, 0.08);
+}
+
+.section-title {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #333;
+  margin-bottom: 20rpx;
+  display: block;
+}
+
+.weather-grid {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 16rpx;
+  justify-content: center;
+}
+
+.weather-btn {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  width: 120rpx;
+  padding: 16rpx 8rpx;
+  border-radius: 16rpx;
+  border: 3rpx solid transparent;
+  background: #FAFAFA;
+  transition: all 0.2s;
+}
+
+.weather-active {
+  background: #FFF0F5;
+}
+
+.weather-icon {
+  font-size: 44rpx;
+  margin-bottom: 8rpx;
+}
+
+.weather-label {
+  font-size: 22rpx;
+  color: #666;
+}
+
+.tag-grid {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 16rpx;
+}
+
+.tag-btn {
+  padding: 12rpx 28rpx;
+  border-radius: 30rpx;
+  background: #F5F5F5;
+  font-size: 26rpx;
+  color: #666;
+  border: 2rpx solid transparent;
+}
+
+.tag-active {
+  background: #FFF0F5;
+  color: #FF6B9D;
+  border-color: #FF6B9D;
+}
+
+.custom-tag-row {
+  display: flex;
+  align-items: center;
+  margin-top: 16rpx;
+  gap: 16rpx;
+}
+
+.custom-tag-input {
+  flex: 1;
+  height: 60rpx;
+  border: 2rpx solid #E0E0E0;
+  border-radius: 12rpx;
+  padding: 0 16rpx;
+  font-size: 26rpx;
+}
+
+.custom-tag-add {
+  padding: 12rpx 24rpx;
+  background: #FF6B9D;
+  color: #fff;
+  border-radius: 12rpx;
+  font-size: 26rpx;
+}
+
+.custom-tag-trigger {
+  display: inline-block;
+  margin-top: 16rpx;
+  color: #FF6B9D;
+  font-size: 26rpx;
+}
+
+.photo-area {
+  width: 200rpx;
+  height: 200rpx;
+  border-radius: 16rpx;
+  overflow: hidden;
+  background: #F5F5F5;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.photo-preview {
+  width: 100%;
+  height: 100%;
+}
+
+.photo-placeholder {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.photo-icon {
+  font-size: 60rpx;
+}
+
+.photo-hint {
+  font-size: 24rpx;
+  color: #999;
+  margin-top: 8rpx;
+}
+
+.photo-note {
+  display: block;
+  font-size: 22rpx;
+  color: #999;
+  margin-top: 12rpx;
+}
+
+/* ===== AI 情绪识别 ===== */
+.ai-recognize-btn {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 20rpx;
+  background: linear-gradient(135deg, #8B5CF6, #A78BFA);
+  border-radius: 16rpx;
+  cursor: pointer;
+}
+.ai-btn-icon {
+  font-size: 32rpx;
+  margin-right: 10rpx;
+}
+.ai-btn-text {
+  font-size: 26rpx;
+  color: #fff;
+  font-weight: 500;
+}
+.ai-recognize-btn:active {
+  opacity: 0.8;
+}
+.ai-analyzing {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 30rpx 0;
+}
+.ai-loading-text {
+  font-size: 26rpx;
+  color: #8B5CF6;
+}
+.ai-result {
+  margin-top: 16rpx;
+}
+.ai-result-title {
+  font-size: 24rpx;
+  color: #666;
+  margin-bottom: 12rpx;
+  display: block;
+}
+.ai-emotion-list {
+  display: flex;
+  flex-direction: column;
+  gap: 12rpx;
+}
+.ai-emotion-item {
+  display: flex;
+  align-items: center;
+  gap: 12rpx;
+}
+.ai-emotion-label {
+  font-size: 24rpx;
+  color: #333;
+  width: 64rpx;
+  flex-shrink: 0;
+}
+.ai-emotion-bar-track {
+  flex: 1;
+  height: 16rpx;
+  background: #F0F0F0;
+  border-radius: 8rpx;
+  overflow: hidden;
+}
+.ai-emotion-bar-fill {
+  height: 100%;
+  border-radius: 8rpx;
+  transition: width 0.5s;
+}
+.ai-emotion-value {
+  font-size: 22rpx;
+  color: #999;
+  width: 56rpx;
+  text-align: right;
+}
+.ai-apply-tags {
+  margin-top: 20rpx;
+  padding: 16rpx;
+  background: #F5F0FF;
+  border-radius: 12rpx;
+  text-align: center;
+  cursor: pointer;
+}
+.ai-apply-text {
+  font-size: 26rpx;
+  color: #8B5CF6;
+  font-weight: 500;
+}
+.ai-apply-tags:active {
+  opacity: 0.7;
+}
+
+.note-input {
+  width: 100%;
+  min-height: 140rpx;
+  background: #F9F9F9;
+  border-radius: 12rpx;
+  padding: 20rpx;
+  font-size: 26rpx;
+  color: #333;
+  box-sizing: border-box;
+}
+
+.submit-area {
+  margin: 40rpx 32rpx;
+}
+
+.submit-btn {
+  width: 100%;
+  height: 88rpx;
+  line-height: 88rpx;
+  background: linear-gradient(135deg, #FF6B9D, #FF8CB3);
+  color: #fff;
+  border-radius: 44rpx;
+  font-size: 30rpx;
+  font-weight: 600;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.submit-disabled {
+  opacity: 0.5;
+}
+
+.submit-icon {
+  margin-right: 8rpx;
+}
+
+.loading-mask {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.3);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 999;
+}
+
+.loading-box {
+  background: #fff;
+  padding: 40rpx 60rpx;
+  border-radius: 20rpx;
+}
+
+.loading-text {
+  font-size: 28rpx;
+  color: #666;
+}
+</style>

+ 422 - 0
cfc-frontend/pages/mind-detail/emotion-report.vue

@@ -0,0 +1,422 @@
+<template>
+  <view class="container">
+    <PageBanner theme="mind-wisdom"
+      tagline="EMI 心理报告"
+      quote="知己知彼,百战不殆" />
+
+    <!-- 报告数据 -->
+    <view class="report-card" v-if="report">
+      <view class="report-header">
+        <text class="report-title">EMI 心理测评报告</text>
+        <text class="report-date">测评日期: {{ report.assessmentDate || '-' }}</text>
+      </view>
+
+      <!-- 综合EQ分 -->
+      <view class="overall-wrap">
+        <text class="overall-label">综合EQ分</text>
+        <text class="overall-value">{{ report.overallScore || 0 }}</text>
+        <text :class="['overall-level', 'level-' + getScoreLevel(report.overallScore || 0)]">{{ getScoreLevelText(report.overallScore || 0) }}</text>
+      </view>
+
+      <!-- 四维条形图 -->
+      <view class="dimension-list">
+        <view class="dimension-item" v-for="dim in dimensions" :key="dim.key">
+          <view class="dimension-header">
+            <view class="dimension-label-row">
+              <text class="dimension-icon">{{ dim.icon }}</text>
+              <text class="dimension-name">{{ dim.label }}</text>
+            </view>
+            <view class="dimension-score-row">
+              <text class="dimension-score">{{ report[dim.key] || 0 }}</text>
+              <text :class="['dimension-level', 'level-' + getScoreLevel(report[dim.key] || 0)]">{{ getScoreLevelText(report[dim.key] || 0) }}</text>
+            </view>
+          </view>
+          <view class="dimension-bar">
+            <view class="dimension-bar-fill" :style="{ width: (report[dim.key] || 0) + '%', background: dim.color }"></view>
+          </view>
+          <text class="dimension-desc">{{ dim.desc }}</text>
+        </view>
+      </view>
+
+      <!-- 分析报告 -->
+      <view class="section-card" v-if="report.analysisReport">
+        <view class="section-header-row">
+          <text class="section-icon">&#x1F4DD;</text>
+          <text class="section-title">分析报告</text>
+        </view>
+        <text class="section-content">{{ report.analysisReport }}</text>
+      </view>
+
+      <!-- 成长建议 -->
+      <view class="section-card suggestion-card" v-if="report.growthSuggestions">
+        <view class="section-header-row">
+          <text class="section-icon">&#x1F331;</text>
+          <text class="section-title">成长建议</text>
+        </view>
+        <text class="section-content">{{ report.growthSuggestions }}</text>
+      </view>
+
+      <!-- 底部操作 -->
+      <view class="action-row">
+        <view class="action-btn secondary" @click="goAssessment">
+          <text class="action-btn-text">再次测评</text>
+        </view>
+        <view class="action-btn primary" @click="goBack">
+          <text class="action-btn-text">返回心智</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 加载中 -->
+    <view class="loading-state" v-if="loading">
+      <text class="loading-text">加载中...</text>
+    </view>
+
+    <!-- 无数据 -->
+    <view class="empty-state" v-if="!report && !loading">
+      <text class="empty-icon">&#x1F9E0;</text>
+      <text class="empty-title">暂无心理测评数据</text>
+      <text class="empty-desc">完成 EMI 心理测评后,可在此查看详细的心理分析报告</text>
+      <view class="empty-btn" @click="goAssessment">预约测评</view>
+    </view>
+  </view>
+</template>
+
+<script>
+import PageBanner from '../../components/PageBanner.vue'
+import { getEmiReport } from '../../utils/api.js'
+
+export default {
+  components: { PageBanner },
+  data: function() {
+    return {
+      childId: null,
+      report: null,
+      loading: true,
+      dimensions: [
+        { key: 'emotionScore', label: '情绪商数', icon: '\u2764\uFE0F', color: '#FF6B35', desc: '识别、理解和管理自身及他人情绪的能力' },
+        { key: 'resilienceScore', label: '心理韧性', icon: '\u{1F4AA}', color: '#F97316', desc: '面对压力和挫折时的适应与恢复能力' },
+        { key: 'stressCopingScore', label: '压力应对', icon: '\u{1F64F}', color: '#FB923C', desc: '有效应对生活压力和心理负担的能力' },
+        { key: 'selfAwarenessScore', label: '自我认知', icon: '\u{1F9EC}', color: '#FBBF24', desc: '对自身情绪、优势、弱点的深度理解' }
+      ]
+    }
+  },
+  onLoad: function(options) {
+    if (options && options.childId) {
+      this.childId = options.childId
+    } else {
+      this.childId = uni.getStorageSync('currentChildId') || null
+    }
+    this.loadReport()
+  },
+  methods: {
+    loadReport: function() {
+      var self = this
+      var childId = self.childId
+      if (!childId) {
+        self.loading = false
+        return
+      }
+      self.loading = true
+      getEmiReport(childId).then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.report = res.data
+        }
+        self.loading = false
+      }).catch(function() {
+        self.loading = false
+      })
+    },
+    getScoreLevel: function(score) {
+      if (score >= 85) return 'excellent'
+      if (score >= 70) return 'good'
+      if (score >= 55) return 'average'
+      return 'low'
+    },
+    getScoreLevelText: function(score) {
+      if (score >= 85) return '优秀'
+      if (score >= 70) return '良好'
+      if (score >= 55) return '一般'
+      return '需提升'
+    },
+    goAssessment: function() {
+      uni.navigateTo({ url: '/pages/assessment/apply' })
+    },
+    goBack: function() {
+      uni.navigateBack({ delta: 1 })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  min-height: 100vh;
+  background: #f5f7fa;
+  padding-bottom: 120rpx;
+}
+
+/* ===== 报告卡 ===== */
+.report-card {
+  margin: 20rpx 30rpx;
+  background: #fff;
+  border-radius: 24rpx;
+  padding: 40rpx;
+  box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06);
+}
+
+.report-header {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 30rpx;
+}
+
+.report-title {
+  font-size: 36rpx;
+  font-weight: bold;
+  color: #333;
+}
+
+.report-date {
+  font-size: 24rpx;
+  color: #999;
+}
+
+/* ===== 综合EQ ===== */
+.overall-wrap {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 40rpx 0;
+  background: linear-gradient(135deg, #FF6B35, #FFD700);
+  border-radius: 20rpx;
+  margin-bottom: 40rpx;
+  color: #fff;
+}
+
+.overall-label {
+  font-size: 28rpx;
+  opacity: 0.9;
+  margin-bottom: 8rpx;
+}
+
+.overall-value {
+  font-size: 80rpx;
+  font-weight: bold;
+  margin-bottom: 8rpx;
+}
+
+.overall-level {
+  font-size: 26rpx;
+  padding: 4rpx 24rpx;
+  border-radius: 20rpx;
+  background: rgba(255,255,255,0.3);
+}
+
+/* ===== 四维条形图 ===== */
+.dimension-list {
+  margin-bottom: 30rpx;
+}
+
+.dimension-item {
+  margin-bottom: 28rpx;
+}
+
+.dimension-header {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 10rpx;
+}
+
+.dimension-label-row {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+
+.dimension-icon {
+  font-size: 28rpx;
+  margin-right: 10rpx;
+}
+
+.dimension-name {
+  font-size: 28rpx;
+  color: #333;
+  font-weight: 500;
+}
+
+.dimension-score-row {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+
+.dimension-score {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #FF6B35;
+  margin-right: 12rpx;
+}
+
+.dimension-level {
+  font-size: 22rpx;
+  padding: 2rpx 14rpx;
+  border-radius: 12rpx;
+}
+
+.level-excellent { color: #10B981; background: #ECFDF5; }
+.level-good { color: #3B82F6; background: #EFF6FF; }
+.level-average { color: #F97316; background: #FFF7ED; }
+.level-low { color: #EF4444; background: #FEF2F2; }
+
+.dimension-bar {
+  height: 18rpx;
+  background: #f0f0f0;
+  border-radius: 9rpx;
+  overflow: hidden;
+  margin-bottom: 8rpx;
+}
+
+.dimension-bar-fill {
+  height: 100%;
+  border-radius: 9rpx;
+  transition: width 0.5s;
+}
+
+.dimension-desc {
+  font-size: 22rpx;
+  color: #999;
+  line-height: 1.4;
+}
+
+/* ===== 分析/建议卡 ===== */
+.section-card {
+  margin-top: 30rpx;
+  padding: 28rpx;
+  background: #f9f9f9;
+  border-radius: 16rpx;
+}
+
+.suggestion-card {
+  background: #FFFBEB;
+}
+
+.section-header-row {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  margin-bottom: 16rpx;
+}
+
+.section-icon {
+  font-size: 30rpx;
+  margin-right: 10rpx;
+}
+
+.section-title {
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #333;
+}
+
+.section-content {
+  font-size: 26rpx;
+  color: #666;
+  line-height: 1.7;
+  display: block;
+}
+
+/* ===== 底部操作 ===== */
+.action-row {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  margin-top: 40rpx;
+}
+
+.action-btn {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 20rpx 0;
+  border-radius: 40rpx;
+}
+
+.action-btn.primary {
+  background: linear-gradient(135deg, #FF6B35, #FFD700);
+  margin-left: 16rpx;
+  box-shadow: 0 4rpx 16rpx rgba(255,107,53,0.3);
+}
+
+.action-btn.secondary {
+  background: #fff;
+  border: 2rpx solid #ddd;
+  margin-right: 16rpx;
+}
+
+.action-btn-text {
+  font-size: 28rpx;
+  font-weight: 500;
+  color: #fff;
+}
+
+.action-btn.secondary .action-btn-text {
+  color: #666;
+}
+
+/* ===== 加载中 ===== */
+.loading-state {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 120rpx 0;
+}
+
+.loading-text {
+  font-size: 28rpx;
+  color: #999;
+}
+
+/* ===== 空状态 ===== */
+.empty-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 120rpx 40rpx 80rpx;
+}
+
+.empty-icon {
+  font-size: 100rpx;
+  margin-bottom: 20rpx;
+}
+
+.empty-title {
+  font-size: 32rpx;
+  color: #333;
+  font-weight: bold;
+  margin-bottom: 16rpx;
+}
+
+.empty-desc {
+  font-size: 26rpx;
+  color: #999;
+  text-align: center;
+  line-height: 1.6;
+  margin-bottom: 40rpx;
+}
+
+.empty-btn {
+  background: linear-gradient(135deg, #FF6B35, #FFD700);
+  color: #fff;
+  font-size: 28rpx;
+  font-weight: bold;
+  padding: 16rpx 64rpx;
+  border-radius: 40rpx;
+  box-shadow: 0 4rpx 16rpx rgba(255,107,53,0.3);
+}
+</style>

+ 441 - 0
cfc-frontend/pages/mind-detail/emotion-trend.vue

@@ -0,0 +1,441 @@
+<template>
+  <view class="trend-container">
+    <!-- 统计概览卡片 -->
+    <view class="stats-overview">
+      <view class="stat-item">
+        <text class="stat-value">{{ stats.totalCheckins || 0 }}</text>
+        <text class="stat-label">总打卡</text>
+      </view>
+      <view class="stat-item">
+        <text class="stat-value">{{ stats.currentStreak || 0 }}</text>
+        <text class="stat-label">连续天数</text>
+      </view>
+      <view class="stat-item">
+        <text class="stat-value">{{ stats.thisMonthCheckins || 0 }}</text>
+        <text class="stat-label">本月打卡</text>
+      </view>
+      <view class="stat-item">
+        <text class="stat-value">{{ stats.totalEnergy || 0 }}</text>
+        <text class="stat-label">心能量</text>
+      </view>
+    </view>
+
+    <!-- 周期切换 -->
+    <view class="period-tabs">
+      <view
+        :class="['period-tab', period === 'week' ? 'period-active' : '']"
+        @click="switchPeriod('week')">
+        <text>周</text>
+      </view>
+      <view
+        :class="['period-tab', period === 'month' ? 'period-active' : '']"
+        @click="switchPeriod('month')">
+        <text>月</text>
+      </view>
+    </view>
+
+    <!-- 情绪日历 -->
+    <view class="calendar-card">
+      <text class="card-title">情绪日历</text>
+      <view class="calendar-grid">
+        <view class="calendar-weekday" v-for="wd in weekdays" :key="wd">
+          <text>{{ wd }}</text>
+        </view>
+        <view
+          v-for="(day, idx) in calendarDays"
+          :key="idx"
+          :class="['calendar-day', 'weather-' + (day.weather || 'none'), day.isToday ? 'is-today' : '']">
+          <text class="day-num">{{ day.dayNum }}</text>
+          <text v-if="day.weather" class="day-emoji">{{ weatherEmoji(day.weather) }}</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 情绪分布 -->
+    <view class="distribution-card">
+      <text class="card-title">情绪分布</text>
+      <view class="distribution-list">
+        <view class="dist-item" v-for="dist in distributionData" :key="dist.weather">
+          <text class="dist-emoji">{{ dist.icon }}</text>
+          <text class="dist-label">{{ dist.label }}</text>
+          <view class="dist-bar-track">
+            <view class="dist-bar-fill" :style="{ width: dist.percent + '%', background: dist.color }"></view>
+          </view>
+          <text class="dist-count">{{ dist.count }}次</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 按天详情 -->
+    <view class="daily-list-card" v-if="dailyData.length > 0">
+      <text class="card-title">每日记录</text>
+      <view class="daily-item" v-for="(day, idx) in dailyData" :key="idx">
+        <text class="daily-date">{{ formatDate(day.date) }}</text>
+        <text class="daily-emoji">{{ weatherEmoji(day.weather) }}</text>
+        <text class="daily-weather">{{ weatherLabel(day.weather) }}</text>
+      </view>
+    </view>
+
+    <view class="empty-state" v-if="!stats || stats.totalCheckins === 0">
+      <text class="empty-icon">&#x1F4AD;</text>
+      <text class="empty-text">还没有情绪打卡记录</text>
+      <button class="empty-btn" @click="goCheckin">去打卡</button>
+    </view>
+  </view>
+</template>
+
+<script>
+import config from '../../config'
+
+export default {
+  data() {
+    return {
+      period: 'week',
+      stats: null,
+      trend: null,
+      weekdays: ['日', '一', '二', '三', '四', '五', '六'],
+      weatherConfig: {
+        sunny: { icon: '\u2600\uFE0F', label: '心情很好', color: '#FFB347' },
+        cloudy: { icon: '\u26C5', label: '一般般', color: '#A0A0A0' },
+        rainy: { icon: '\uD83C\uDF27\uFE0F', label: '有点难过', color: '#6B9BD2' },
+        stormy: { icon: '\u26A1', label: '很烦躁', color: '#8B5CF6' },
+        rainbow: { icon: '\uD83C\uDF08', label: '特别开心', color: '#FF6B9D' }
+      }
+    }
+  },
+
+  computed: {
+    calendarDays() {
+      const days = []
+      const now = new Date()
+      const year = now.getFullYear()
+      const month = now.getMonth()
+      const firstDay = new Date(year, month, 1).getDay()
+      const daysInMonth = new Date(year, month + 1, 0).getDate()
+      const today = now.getDate()
+
+      // Build weather map from daily data
+      const weatherMap = {}
+      if (this.trend && this.trend.dailyData) {
+        for (const d of this.trend.dailyData) {
+          if (d.date) {
+            const dateObj = new Date(d.date)
+            weatherMap[dateObj.getDate()] = d.weather
+          }
+        }
+      }
+
+      // Empty leading cells
+      for (let i = 0; i < firstDay; i++) {
+        days.push({ dayNum: '', weather: null, isToday: false })
+      }
+
+      for (let d = 1; d <= daysInMonth; d++) {
+        days.push({
+          dayNum: d,
+          weather: weatherMap[d] || null,
+          isToday: d === today
+        })
+      }
+
+      return days
+    },
+
+    distributionData() {
+      const dist = this.trend && this.trend.weatherDistribution
+      if (!dist) return []
+      const total = Object.values(dist).reduce((a, b) => a + b, 0)
+      if (total === 0) return []
+
+      return Object.keys(dist).map(w => {
+        const cfg = this.weatherConfig[w] || { icon: '?', label: w, color: '#ccc' }
+        return {
+          weather: w,
+          icon: cfg.icon,
+          label: cfg.label,
+          color: cfg.color,
+          count: dist[w],
+          percent: Math.round(dist[w] / total * 100)
+        }
+      })
+    },
+
+    dailyData() {
+      return this.trend && this.trend.dailyData || []
+    }
+  },
+
+  onLoad() {
+    this.loadData()
+  },
+
+  methods: {
+    loadData() {
+      const app = getApp()
+      const childId = app.globalData.currentChildId
+      if (!childId) return
+
+      uni.request({
+        url: config.API_BASE_URL + '/api/mind/checkin/stats',
+        method: 'POST',
+        data: { childId },
+        success: (res) => {
+          if (res.data && res.data.code === 200) {
+            this.stats = res.data.data
+          }
+        }
+      })
+
+      uni.request({
+        url: config.API_BASE_URL + '/api/mind/checkin/trend',
+        method: 'POST',
+        data: { childId, period: this.period },
+        success: (res) => {
+          if (res.data && res.data.code === 200) {
+            this.trend = res.data.data
+          }
+        }
+      })
+    },
+
+    switchPeriod(p) {
+      this.period = p
+      this.loadData()
+    },
+
+    weatherEmoji(weather) {
+      const cfg = this.weatherConfig[weather]
+      return cfg ? cfg.icon : ''
+    },
+
+    weatherLabel(weather) {
+      const cfg = this.weatherConfig[weather]
+      return cfg ? cfg.label : weather
+    },
+
+    formatDate(dateStr) {
+      if (!dateStr) return ''
+      const d = new Date(dateStr)
+      return (d.getMonth() + 1) + '/' + d.getDate()
+    },
+
+    goCheckin() {
+      uni.navigateTo({ url: '/pages/mind/emotion-checkin' })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.trend-container {
+  min-height: 100vh;
+  background: #F8F8F8;
+  padding-bottom: 40rpx;
+}
+
+.stats-overview {
+  display: flex;
+  margin: 24rpx 32rpx;
+  background: linear-gradient(135deg, #FF6B9D, #FF8CB3);
+  border-radius: 20rpx;
+  padding: 28rpx 16rpx;
+}
+
+.stat-item {
+  flex: 1;
+  text-align: center;
+}
+
+.stat-value {
+  font-size: 36rpx;
+  font-weight: 700;
+  color: #fff;
+  display: block;
+}
+
+.stat-label {
+  font-size: 22rpx;
+  color: rgba(255, 255, 255, 0.8);
+  margin-top: 6rpx;
+  display: block;
+}
+
+.period-tabs {
+  display: flex;
+  margin: 0 32rpx;
+  background: #fff;
+  border-radius: 12rpx;
+  overflow: hidden;
+}
+
+.period-tab {
+  flex: 1;
+  text-align: center;
+  padding: 16rpx 0;
+  font-size: 26rpx;
+  color: #666;
+}
+
+.period-active {
+  background: #FF6B9D;
+  color: #fff;
+  font-weight: 600;
+}
+
+.calendar-card,
+.distribution-card,
+.daily-list-card {
+  margin: 24rpx 32rpx;
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 28rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
+}
+
+.card-title {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #333;
+  display: block;
+  margin-bottom: 20rpx;
+}
+
+.calendar-grid {
+  display: flex;
+  flex-wrap: wrap;
+}
+
+.calendar-weekday {
+  width: 14.28%;
+  text-align: center;
+  font-size: 22rpx;
+  color: #999;
+  padding: 10rpx 0;
+}
+
+.calendar-day {
+  width: 14.28%;
+  text-align: center;
+  padding: 8rpx 0;
+  min-height: 80rpx;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+}
+
+.is-today {
+  background: #FFF0F5;
+  border-radius: 12rpx;
+}
+
+.day-num {
+  font-size: 26rpx;
+  color: #333;
+}
+
+.day-emoji {
+  font-size: 24rpx;
+  margin-top: 4rpx;
+}
+
+.weather-sunny { background: #FFF8E1; border-radius: 8rpx; }
+.weather-cloudy { background: #F5F5F5; border-radius: 8rpx; }
+.weather-rainy { background: #E3F2FD; border-radius: 8rpx; }
+.weather-stormy { background: #EDE7F6; border-radius: 8rpx; }
+.weather-rainbow { background: #FCE4EC; border-radius: 8rpx; }
+
+.distribution-list {
+  display: flex;
+  flex-direction: column;
+  gap: 16rpx;
+}
+
+.dist-item {
+  display: flex;
+  align-items: center;
+  gap: 16rpx;
+}
+
+.dist-emoji {
+  font-size: 32rpx;
+  width: 48rpx;
+  text-align: center;
+}
+
+.dist-label {
+  font-size: 24rpx;
+  color: #666;
+  width: 120rpx;
+}
+
+.dist-bar-track {
+  flex: 1;
+  height: 20rpx;
+  background: #F0F0F0;
+  border-radius: 10rpx;
+  overflow: hidden;
+}
+
+.dist-bar-fill {
+  height: 100%;
+  border-radius: 10rpx;
+  transition: width 0.3s;
+}
+
+.dist-count {
+  font-size: 24rpx;
+  color: #999;
+  width: 60rpx;
+  text-align: right;
+}
+
+.daily-item {
+  display: flex;
+  align-items: center;
+  padding: 16rpx 0;
+  border-bottom: 2rpx solid #F5F5F5;
+  gap: 16rpx;
+}
+
+.daily-date {
+  font-size: 24rpx;
+  color: #999;
+  width: 100rpx;
+}
+
+.daily-emoji {
+  font-size: 32rpx;
+}
+
+.daily-weather {
+  font-size: 26rpx;
+  color: #666;
+}
+
+.empty-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 120rpx 32rpx;
+}
+
+.empty-icon {
+  font-size: 80rpx;
+  margin-bottom: 20rpx;
+}
+
+.empty-text {
+  font-size: 28rpx;
+  color: #999;
+  margin-bottom: 40rpx;
+}
+
+.empty-btn {
+  background: #FF6B9D;
+  color: #fff;
+  padding: 16rpx 60rpx;
+  border-radius: 40rpx;
+  font-size: 28rpx;
+}
+</style>

+ 308 - 0
cfc-frontend/pages/mind-detail/member-mind-detail.vue

@@ -0,0 +1,308 @@
+<template>
+  <view class="detail-container">
+    <!-- 顶部导航 -->
+    <view class="nav-bar">
+      <view class="nav-back" @click="goBack">
+        <text class="nav-back-icon">&#x2190;</text>
+      </view>
+      <text class="nav-title">心 · 成员详情</text>
+    </view>
+
+    <!-- 五维能量条 -->
+    <FamilyEnergyBar
+      dimensionCode="mind"
+      :sandboxData="sandboxData"
+      :dualDimension="dualDimension" />
+
+    <!-- 成员信息卡 -->
+    <view class="member-card" v-if="memberInfo">
+      <view class="member-avatar mind-avatar">
+        <text class="avatar-text">{{ memberInfo.name && memberInfo.name.charAt(0) || '孩' }}</text>
+      </view>
+      <view class="member-info">
+        <text class="member-name">{{ memberInfo.name || '孩子' }}</text>
+        <text class="member-role">心智维度</text>
+      </view>
+      <view class="member-score" v-if="dualDimension">
+        <text class="score-value mind-score">{{ dualDimension.energy || 0 }}</text>
+        <text class="score-label">能量值</text>
+      </view>
+    </view>
+
+    <!-- 家庭成员关系图谱(只读) -->
+    <FamilyRelationGraph
+      dimensionCode="mind"
+      :selfId="selfId"
+      :members="graphMembers"
+      :energyMap="energyMapForGraph"
+      :intimacyMap="intimacyMapForGraph"
+      :interactive="false" />
+
+    <!-- 今日任务 -->
+    <DimensionTasks
+      dimensionCode="mind"
+      :tasks="dimensionTasks"
+      @taskClick="onTaskClick"
+      @moreTasks="goTasks" />
+
+    <!-- 活动 -->
+    <DimensionActivities
+      dimensionCode="mind"
+      :activities="dimensionActivities"
+      @activityClick="goActivityDetail"
+      @moreActivities="goMoreActivities" />
+
+    <!-- 商品 -->
+    <DimensionProducts
+      dimensionCode="mind"
+      :products="dimensionProducts"
+      @productClick="goProductDetail"
+      @moreProducts="goMoreProducts" />
+
+    <!-- 底部占位 -->
+    <view class="bottom-spacer"></view>
+  </view>
+</template>
+
+<script>
+import FamilyEnergyBar from '../../components/FamilyEnergyBar.vue'
+import FamilyRelationGraph from '../../components/FamilyRelationGraph.vue'
+import DimensionTasks from '../../components/DimensionTasks.vue'
+import DimensionActivities from '../../components/DimensionActivities.vue'
+import DimensionProducts from '../../components/DimensionProducts.vue'
+import { getEnergyOverview, getFamilyEnergySandbox, getTodayTasksByCategory, getActivityList, getProductsByDomain, getChildren } from '../../utils/api.js'
+
+export default {
+  components: { FamilyEnergyBar, FamilyRelationGraph, DimensionTasks, DimensionActivities, DimensionProducts },
+  data() {
+    return {
+      childId: null,
+      selfId: null,
+      memberInfo: null,
+      sandboxData: null,
+      dualDimension: null,
+      dimensionTasks: [],
+      dimensionActivities: [],
+      dimensionProducts: []
+    }
+  },
+  computed: {
+    graphMembers: function() {
+      if (!this.sandboxData || !this.sandboxData.members) return []
+      return this.sandboxData.members.map(function(m) {
+        return {
+          id: m.memberId || m.id,
+          nickname: m.name || m.nickname || '成员',
+          memberType: m.memberType || 'child'
+        }
+      })
+    },
+    energyMapForGraph: function() {
+      var map = {}
+      if (this.sandboxData && this.sandboxData.members) {
+        for (var i = 0; i < this.sandboxData.members.length; i++) {
+          var m = this.sandboxData.members[i]
+          map[m.memberId || m.id] = {
+            bodyScore: m.bodyScore || 0,
+            mindScore: m.mindScore || 0,
+            actionScore: m.actionScore || 0
+          }
+        }
+      }
+      return map
+    },
+    intimacyMapForGraph: function() {
+      return {}
+    }
+  },
+  onLoad: function(options) {
+    this.childId = options.childId || uni.getStorageSync('currentChildId') || null
+  },
+  onShow: function() {
+    this.selfId = uni.getStorageSync('userId') || null
+    this.loadChildren()
+    this.loadSandboxData()
+    if (this.childId) {
+      this.loadEnergyData()
+      this.loadDimensionTasks()
+      this.loadDimensionActivities()
+      this.loadDimensionProducts()
+    }
+  },
+  methods: {
+    loadChildren: function() {
+      var self = this
+      getChildren().then(function(res) {
+        if (res.code === 200 && res.data) {
+          for (var i = 0; i < res.data.length; i++) {
+            if (res.data[i].childId === self.childId) {
+              self.memberInfo = res.data[i]
+              break
+            }
+          }
+          if (!self.memberInfo && res.data.length > 0) {
+            self.memberInfo = res.data[0]
+            self.childId = res.data[0].childId
+          }
+        }
+      }).catch(function() {})
+    },
+    loadSandboxData: function() {
+      var self = this
+      getFamilyEnergySandbox().then(function(res) {
+        if (res && res.data) self.sandboxData = res.data
+      }).catch(function() {})
+    },
+    loadEnergyData: function() {
+      var self = this
+      getEnergyOverview(this.childId).then(function(res) {
+        if (res && res.data) {
+          var dims = res.data.dimensions
+          if (dims && dims.length > 0) {
+            for (var i = 0; i < dims.length; i++) {
+              if (dims[i].code === 'mind') {
+                self.dualDimension = dims[i]
+                break
+              }
+            }
+          }
+        }
+      }).catch(function() {})
+    },
+    loadDimensionTasks: function() {
+      var self = this
+      getTodayTasksByCategory(this.childId, 'mind').then(function(res) {
+        if (res && res.data) {
+          self.dimensionTasks = Array.isArray(res.data) ? res.data.slice(0, 5) : []
+        }
+      }).catch(function() { self.dimensionTasks = [] })
+    },
+    loadDimensionActivities: function() {
+      var self = this
+      getActivityList({ dimensionCode: 'mind', page: 1, size: 3 }).then(function(res) {
+        if (res && res.data) {
+          var list = Array.isArray(res.data) ? res.data : (res.data.records || [])
+          self.dimensionActivities = list.slice(0, 3)
+        }
+      }).catch(function() { self.dimensionActivities = [] })
+    },
+    loadDimensionProducts: function() {
+      var self = this
+      getProductsByDomain('mind', 1, 4).then(function(res) {
+        if (res && res.data) {
+          var list = Array.isArray(res.data) ? res.data : (res.data.records || [])
+          self.dimensionProducts = list.slice(0, 4)
+        }
+      }).catch(function() { self.dimensionProducts = [] })
+    },
+    onTaskClick: function(task) {
+      uni.navigateTo({ url: '/pages/tasks/tasks' })
+    },
+    goActivityDetail: function(act) {
+      if (act && act.id) uni.navigateTo({ url: '/pages/discover/activity-detail/activity-detail?id=' + act.id })
+    },
+    goMoreActivities: function() {
+      uni.navigateTo({ url: '/pages/discover/index?type=activity&dimension=mind' })
+    },
+    goProductDetail: function(prod) {
+      if (prod && prod.id) uni.navigateTo({ url: '/pages/discover/product-detail/product-detail?id=' + prod.id })
+    },
+    goMoreProducts: function() {
+      uni.navigateTo({ url: '/pages/discover/index?type=product&domain=mind' })
+    },
+    goTasks: function() { uni.switchTab({ url: '/pages/tasks/tasks' }) },
+    goBack: function() { uni.navigateBack() }
+  }
+}
+</script>
+
+<style scoped>
+.detail-container {
+  min-height: 100vh;
+  background: #f5f7fa;
+  padding-bottom: 40rpx;
+}
+.nav-bar {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  padding: 20rpx 30rpx;
+  background: #fff;
+}
+.nav-back {
+  padding: 10rpx;
+}
+.nav-back-icon {
+  font-size: 36rpx;
+  color: #333;
+}
+.nav-title {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #333;
+  margin-left: 16rpx;
+}
+.member-card {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  margin: 20rpx 30rpx;
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 24rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
+}
+.member-avatar {
+  width: 80rpx;
+  height: 80rpx;
+  border-radius: 40rpx;
+  background: linear-gradient(135deg, #FF6B35, #FBBF24);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 20rpx;
+}
+.mind-avatar {
+  background: linear-gradient(135deg, #8B5CF6, #A78BFA);
+}
+.avatar-text {
+  font-size: 32rpx;
+  color: #fff;
+  font-weight: bold;
+}
+.member-info {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+}
+.member-name {
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #333;
+}
+.member-role {
+  font-size: 24rpx;
+  color: #999;
+  margin-top: 4rpx;
+}
+.member-score {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+.score-value {
+  font-size: 40rpx;
+  font-weight: bold;
+  color: #FF6B35;
+}
+.mind-score {
+  color: #8B5CF6;
+}
+.score-label {
+  font-size: 22rpx;
+  color: #999;
+}
+.bottom-spacer {
+  height: 40rpx;
+}
+</style>

+ 396 - 0
cfc-frontend/pages/mind-detail/soothe-toolbox.vue

@@ -0,0 +1,396 @@
+<template>
+  <view class="toolbox-container">
+    <!-- ===== 4-7-8 呼吸练习 ===== -->
+    <view class="tool-section">
+      <view class="section-header">
+        <text class="section-emoji">&#x1F9D8;</text>
+        <text class="section-title">4-7-8 呼吸练习</text>
+      </view>
+      <text class="section-desc">用深呼吸让自己平静下来</text>
+
+      <view class="breathing-area" @click="toggleBreathing">
+        <view :class="['breath-circle', breathPhase]">
+          <text class="breath-text">{{ breathInstruction }}</text>
+        </view>
+      </view>
+      <text class="breath-hint" v-if="!breathing">点击开始呼吸练习</text>
+      <text class="breath-count" v-if="breathing">第 {{ breathRound }} 轮</text>
+    </view>
+
+    <!-- ===== 正念练习 ===== -->
+    <view class="tool-section">
+      <view class="section-header">
+        <text class="section-emoji">&#x1F4A1;</text>
+        <text class="section-title">正念练习</text>
+      </view>
+      <view class="mindfulness-cards">
+        <view
+          :class="['mind-card', activeMindCard === idx ? 'mind-card-active' : '']"
+          v-for="(item, idx) in mindfulnessItems"
+          :key="idx"
+          @click="activeMindCard = idx">
+          <text class="mind-card-title">{{ item.title }}</text>
+          <text class="mind-card-desc">{{ item.desc }}</text>
+        </view>
+      </view>
+      <view class="mindfulness-content" v-if="activeMindCard !== -1">
+        <text class="mindfulness-text">{{ mindfulnessItems[activeMindCard].content }}</text>
+      </view>
+    </view>
+
+    <!-- ===== 情绪涂鸦 ===== -->
+    <view class="tool-section">
+      <view class="section-header">
+        <text class="section-emoji">&#x1F3A8;</text>
+        <text class="section-title">情绪涂鸦</text>
+      </view>
+      <text class="section-desc">在纸上自由画下你现在的情绪</text>
+      <view class="doodle-suggestions">
+        <view class="doodle-item" v-for="(item, idx) in doodlePrompts" :key="idx">
+          <text class="doodle-icon">{{ item.icon }}</text>
+          <text class="doodle-text">{{ item.text }}</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- ===== 运动释压 ===== -->
+    <view class="tool-section">
+      <view class="section-header">
+        <text class="section-emoji">&#x1F3CB;</text>
+        <text class="section-title">运动释压</text>
+      </view>
+      <text class="section-desc">简单的伸展动作,释放身体紧张</text>
+      <view class="exercise-list">
+        <view class="exercise-item" v-for="(ex, idx) in exercises" :key="idx">
+          <text class="exercise-name">{{ ex.name }}</text>
+          <text class="exercise-desc">{{ ex.desc }}</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 底部提示 -->
+    <view class="disclaimer">
+      <text class="disclaimer-text">以上内容仅供参考,不能替代专业心理咨询或医疗建议。如有需要,请寻求专业帮助。</text>
+    </view>
+  </view>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      breathing: false,
+      breathPhase: 'inhal',
+      breathInstruction: '吸气',
+      breathRound: 0,
+      breathTimer: null,
+      activeMindCard: -1,
+      mindfulnessItems: [
+        {
+          title: '5-4-3-2-1 感官练习',
+          desc: '快速回归当下',
+          content: '找一个舒服的姿势坐下,深呼吸三次。然后:\n\n看:说出你看到的 5 样东西\n听:说出你听到的 4 种声音\n触:感受你身体的 3 处触觉\n闻:闻到的 2 种气味\n味:品尝到的 1 种味道\n\n这个练习能帮你快速从焦虑中回到当下。'
+        },
+        {
+          title: '身体扫描',
+          desc: '放松全身',
+          content: '闭上眼睛,把注意力带到脚趾。感受脚趾的温度和触感。\n\n慢慢向上移动注意力:脚掌 → 脚踝 → 小腿 → 膝盖 → 大腿 → 腹部 → 胸部 → 手指 → 手臂 → 肩膀 → 脖子 → 面部 → 头顶。\n\n在每个部位停留 3-5 秒,感受那里的感觉。'
+        },
+        {
+          title: '感恩三件事',
+          desc: '培养积极心态',
+          content: '想一想今天让你感到感恩的三件事。可以是很小的事:\n\n1. 今天吃到了一顿美味的饭\n2. 有人对你说了一句温暖的话\n3. 今天的天气很好\n4. 你学会了一样新东西\n5. 帮助了别人\n\n在心里默默感谢这些美好。'
+        }
+      ],
+      doodlePrompts: [
+        { icon: '\u{1F308}', text: '画出你今天的情绪彩虹' },
+        { icon: '\u{1F33F}', text: '画一棵代表你内心的小树' },
+        { icon: '\u{1F30A}', text: '画出心情波浪' },
+        { icon: '\u{1F31F}', text: '画出你心中的星星' }
+      ],
+      exercises: [
+        { name: '耸肩放松', desc: '双肩用力向上耸起(靠近耳朵),保持 5 秒,然后突然放松。重复 5 次。' },
+        { name: '颈部伸展', desc: '慢慢将头向右侧倾斜,右手轻压头部,保持 15 秒。换左侧重复。' },
+        { name: '猫牛式', desc: '双手双膝着地,吸气时塌腰抬头(牛式),呼气时弓背低头(猫式)。重复 10 次。' },
+        { name: '蝴蝶式', desc: '坐姿,双脚掌相对,双手握住脚踝,膝盖向两侧打开轻轻上下抖动,像蝴蝶扇动翅膀。' }
+      ]
+    }
+  },
+
+  methods: {
+    toggleBreathing() {
+      if (this.breathing) {
+        this.stopBreathing()
+      } else {
+        this.startBreathing()
+      }
+    },
+
+    startBreathing() {
+      this.breathing = true
+      this.breathRound = 1
+      this.doBreathPhase('inhal', 4)
+    },
+
+    doBreathPhase(phase, seconds) {
+      var self = this
+      if (!self.breathing) return
+
+      self.breathPhase = phase
+      var instructions = {
+        inhal: '吸气...',
+        hold: '屏息...',
+        exhale: '呼气...'
+      }
+      self.breathInstruction = instructions[phase] || ''
+
+      self.breathTimer = setTimeout(function() {
+        if (!self.breathing) return
+        if (phase === 'inhal') {
+          self.doBreathPhase('hold', 7)
+        } else if (phase === 'hold') {
+          self.doBreathPhase('exhale', 8)
+        } else {
+          // exhale done, start next round
+          var nextRound = self.breathRound + 1
+          self.breathRound = nextRound
+          if (nextRound <= 5) {
+            self.doBreathPhase('inhal', 4)
+          } else {
+            self.stopBreathing()
+            uni.showToast({ title: '完成一组呼吸', icon: 'none' })
+          }
+        }
+      }, seconds * 1000)
+    },
+
+    stopBreathing() {
+      this.breathing = false
+      if (this.breathTimer) {
+        clearTimeout(this.breathTimer)
+        this.breathTimer = null
+      }
+      this.breathPhase = 'inhal'
+      this.breathInstruction = '吸气'
+      this.breathRound = 0
+    }
+  },
+
+  onUnload() {
+    this.stopBreathing()
+  }
+}
+</script>
+
+<style scoped>
+.toolbox-container {
+  min-height: 100vh;
+  background: linear-gradient(180deg, #F0FDF4 0%, #FFFFFF 50%, #FFF7ED 100%);
+  padding-bottom: 40rpx;
+}
+
+.tool-section {
+  margin: 24rpx 32rpx;
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 28rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
+}
+
+.section-header {
+  display: flex;
+  align-items: center;
+  margin-bottom: 8rpx;
+}
+
+.section-emoji {
+  font-size: 36rpx;
+  margin-right: 12rpx;
+}
+
+.section-title {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #333;
+}
+
+.section-desc {
+  font-size: 24rpx;
+  color: #999;
+  display: block;
+  margin-bottom: 20rpx;
+}
+
+/* ===== 呼吸练习 ===== */
+.breathing-area {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 40rpx 0;
+}
+
+.breath-circle {
+  width: 280rpx;
+  height: 280rpx;
+  border-radius: 50%;
+  background: linear-gradient(135deg, #A7F3D0, #6EE7B7);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  transition: all 0.3s;
+  box-shadow: 0 8rpx 40rpx rgba(16, 185, 129, 0.2);
+}
+
+.breath-circle.inhal {
+  animation: breatheIn 4s ease-in-out;
+}
+
+.breath-circle.hold {
+  background: linear-gradient(135deg, #FDE68A, #F59E0B);
+  box-shadow: 0 8rpx 40rpx rgba(245, 158, 11, 0.3);
+}
+
+.breath-circle.exhale {
+  animation: breatheOut 8s ease-in-out;
+}
+
+.breath-text {
+  font-size: 36rpx;
+  font-weight: 600;
+  color: #fff;
+}
+
+.breath-hint {
+  display: block;
+  text-align: center;
+  font-size: 24rpx;
+  color: #999;
+}
+
+.breath-count {
+  display: block;
+  text-align: center;
+  font-size: 24rpx;
+  color: #10B981;
+  margin-top: 8rpx;
+}
+
+/* ===== 正念练习 ===== */
+.mindfulness-cards {
+  display: flex;
+  flex-direction: column;
+  gap: 16rpx;
+}
+
+.mind-card {
+  padding: 20rpx;
+  background: #F9F9F9;
+  border-radius: 12rpx;
+  border: 2rpx solid transparent;
+}
+
+.mind-card-active {
+  background: #F0FDF4;
+  border-color: #10B981;
+}
+
+.mind-card-title {
+  font-size: 26rpx;
+  font-weight: 600;
+  color: #333;
+  display: block;
+}
+
+.mind-card-desc {
+  font-size: 22rpx;
+  color: #999;
+  display: block;
+  margin-top: 4rpx;
+}
+
+.mindfulness-content {
+  margin-top: 16rpx;
+  padding: 20rpx;
+  background: #F0FDF4;
+  border-radius: 12rpx;
+  border-left: 6rpx solid #10B981;
+}
+
+.mindfulness-text {
+  font-size: 24rpx;
+  color: #555;
+  line-height: 1.8;
+  white-space: pre-wrap;
+}
+
+/* ===== 情绪涂鸦 ===== */
+.doodle-suggestions {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 16rpx;
+}
+
+.doodle-item {
+  width: calc(50% - 8rpx);
+  display: flex;
+  align-items: center;
+  gap: 12rpx;
+  padding: 20rpx;
+  background: #FFFBEB;
+  border-radius: 12rpx;
+  box-sizing: border-box;
+}
+
+.doodle-icon {
+  font-size: 36rpx;
+}
+
+.doodle-text {
+  font-size: 24rpx;
+  color: #666;
+  flex: 1;
+}
+
+/* ===== 运动释压 ===== */
+.exercise-list {
+  display: flex;
+  flex-direction: column;
+  gap: 16rpx;
+}
+
+.exercise-item {
+  padding: 20rpx;
+  background: #F0F9FF;
+  border-radius: 12rpx;
+  border-left: 6rpx solid #3B82F6;
+}
+
+.exercise-name {
+  font-size: 26rpx;
+  font-weight: 600;
+  color: #2563EB;
+  display: block;
+  margin-bottom: 6rpx;
+}
+
+.exercise-desc {
+  font-size: 24rpx;
+  color: #666;
+  line-height: 1.6;
+  display: block;
+}
+
+/* ===== 免责声明 ===== */
+.disclaimer {
+  margin: 32rpx;
+  padding: 20rpx;
+  background: #FEF3C7;
+  border-radius: 12rpx;
+}
+
+.disclaimer-text {
+  font-size: 22rpx;
+  color: #92400E;
+  line-height: 1.6;
+}
+</style>

+ 398 - 0
cfc-frontend/pages/mind-detail/traditional-mirror.vue

@@ -0,0 +1,398 @@
+<template>
+  <view class="mirror-container">
+    <!-- 加载状态 -->
+    <view class="loading-state" v-if="loading">
+      <text>加载中...</text>
+    </view>
+
+    <template v-if="!loading && dataLoaded">
+      <!-- 八字四柱 -->
+      <view class="mirror-section">
+        <view class="section-header">
+          <text class="section-icon">&#x2637;</text>
+          <text class="section-title">八字四柱</text>
+        </view>
+        <view class="bazi-grid" v-if="eightChars">
+          <view class="bazi-column" v-for="(pillar, key) in eightChars" :key="key">
+            <text class="bazi-pillar-label">{{ pillarLabel(key) }}</text>
+            <text class="bazi-pillar-value">{{ pillar }}</text>
+          </view>
+        </view>
+        <view class="empty-data" v-else>
+          <text>暂未设置出生信息,无法排八字</text>
+        </view>
+      </view>
+
+      <!-- 五行能量 -->
+      <view class="mirror-section">
+        <view class="section-header">
+          <text class="section-icon">&#x267E;</text>
+          <text class="section-title">五行能量</text>
+        </view>
+        <view class="wuxing-list" v-if="wuxingData">
+          <view class="wuxing-item" v-for="wx in wuxingData" :key="wx.key">
+            <text class="wuxing-label">{{ wx.label }}</text>
+            <view class="wuxing-bar-track">
+              <view class="wuxing-bar-fill" :style="{ width: wx.value + '%', background: wx.color }"></view>
+            </view>
+            <text class="wuxing-value">{{ wx.value }}%</text>
+          </view>
+        </view>
+
+        <!-- 五行解读 -->
+        <view class="reading-card" v-if="wuxingReading">
+          <text class="reading-label">五行解读</text>
+          <text class="reading-text" v-if="wuxingReading.personalTrait">{{ wuxingReading.personalTrait }}</text>
+          <text class="reading-text" v-if="wuxingReading.mindGrowthAdvice">{{ wuxingReading.mindGrowthAdvice }}</text>
+        </view>
+      </view>
+
+      <!-- 太阳星座 -->
+      <view class="mirror-section" v-if="zodiac">
+        <view class="section-header">
+          <text class="section-icon">&#x2600;</text>
+          <text class="section-title">太阳星座</text>
+        </view>
+        <view class="zodiac-display">
+          <text class="zodiac-name">{{ zodiac }}</text>
+        </view>
+      </view>
+
+      <!-- 生命灵数 -->
+      <view class="mirror-section" v-if="numSoulData">
+        <view class="section-header">
+          <text class="section-icon">&#x1F52E;</text>
+          <text class="section-title">生命灵数</text>
+        </view>
+        <view class="numsoul-display">
+          <view class="numsoul-number" :style="{ background: numSoulData.colorHex || '#FF6B9D' }">
+            <text class="numsoul-num">{{ lifeNumber }}</text>
+          </view>
+          <view class="numsoul-info">
+            <text class="numsoul-title">{{ numSoulData.title || '' }}</text>
+            <text class="numsoul-keywords" v-if="numSoulData.keywords">{{ numSoulData.keywords }}</text>
+            <text class="numsoul-advice" v-if="numSoulData.mindGrowthAdvice">{{ numSoulData.mindGrowthAdvice }}</text>
+          </view>
+        </view>
+      </view>
+    </template>
+
+    <!-- 无数据 -->
+    <view class="empty-state" v-if="!loading && !dataLoaded">
+      <text class="empty-icon">&#x1F4AD;</text>
+      <text class="empty-text">暂无传统文化数据</text>
+    </view>
+  </view>
+</template>
+
+<script>
+import config from '../../config'
+
+export default {
+  data() {
+    return {
+      loading: true,
+      dataLoaded: false,
+      eightChars: null,
+      wuxingElements: null,
+      zodiac: '',
+      lifeNumber: null,
+      wuxingReading: null,
+      numSoulData: null,
+      wuxingConfig: {
+        wood: { label: '木', color: '#10B981' },
+        fire: { label: '火', color: '#FF6B9D' },
+        earth: { label: '土', color: '#FF8C42' },
+        metal: { label: '金', color: '#6366F1' },
+        water: { label: '水', color: '#3B82F6' }
+      },
+      pillarLabels: { year: '年柱', month: '月柱', day: '日柱', hour: '时柱' }
+    }
+  },
+
+  computed: {
+    wuxingData() {
+      if (!this.wuxingElements) return null
+      var keys = ['wood', 'fire', 'earth', 'metal', 'water']
+      var result = []
+      for (var i = 0; i < keys.length; i++) {
+        var k = keys[i]
+        if (this.wuxingElements[k] !== undefined) {
+          result.push({
+            key: k,
+            label: this.wuxingConfig[k].label,
+            color: this.wuxingConfig[k].color,
+            value: this.wuxingElements[k]
+          })
+        }
+      }
+      return result.sort(function(a, b) { return b.value - a.value })
+    }
+  },
+
+  onLoad() {
+    this.loadData()
+  },
+
+  methods: {
+    loadData() {
+      var self = this
+      var app = getApp()
+      var memberId = this.$mp.query.memberId || app.globalData.currentChildId
+      if (!memberId) {
+        // Try to get from storage
+        memberId = uni.getStorageSync('currentChildId')
+      }
+      if (!memberId) {
+        self.loading = false
+        return
+      }
+
+      uni.request({
+        url: config.API_BASE_URL + '/api/mind/traditional/mirror',
+        method: 'POST',
+        data: { memberId: memberId, memberType: 'child' },
+        success: function(res) {
+          self.loading = false
+          if (res.data && res.data.code === 200 && res.data.data) {
+            var d = res.data.data
+            self.eightChars = d.eightCharacters || null
+            self.wuxingElements = d.wuxingElements || null
+            self.zodiac = d.zodiac || ''
+            self.lifeNumber = d.lifeNumber || null
+            self.wuxingReading = d.wuxingReading || null
+            self.numSoulData = d.numSoul || null
+            self.dataLoaded = !!(self.eightChars || self.wuxingElements || self.zodiac || self.numSoulData)
+          } else {
+            self.dataLoaded = false
+          }
+        },
+        fail: function() {
+          self.loading = false
+        }
+      })
+    },
+
+    pillarLabel(key) {
+      return this.pillarLabels[key] || key
+    }
+  }
+}
+</script>
+
+<style scoped>
+.mirror-container {
+  min-height: 100vh;
+  background: #F8F6F0;
+  padding-bottom: 40rpx;
+}
+
+.loading-state {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 120rpx 0;
+  color: #999;
+  font-size: 28rpx;
+}
+
+.mirror-section {
+  margin: 24rpx 32rpx;
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 28rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
+}
+
+.section-header {
+  display: flex;
+  align-items: center;
+  margin-bottom: 20rpx;
+}
+
+.section-icon {
+  font-size: 36rpx;
+  margin-right: 12rpx;
+}
+
+.section-title {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #333;
+}
+
+.bazi-grid {
+  display: flex;
+  justify-content: space-around;
+  padding: 16rpx 0;
+}
+
+.bazi-column {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.bazi-pillar-label {
+  font-size: 22rpx;
+  color: #999;
+  margin-bottom: 8rpx;
+}
+
+.bazi-pillar-value {
+  font-size: 36rpx;
+  font-weight: 700;
+  color: #8B5CF6;
+  background: #F5F0FF;
+  padding: 12rpx 16rpx;
+  border-radius: 12rpx;
+}
+
+.wuxing-list {
+  display: flex;
+  flex-direction: column;
+  gap: 16rpx;
+}
+
+.wuxing-item {
+  display: flex;
+  align-items: center;
+  gap: 16rpx;
+}
+
+.wuxing-label {
+  font-size: 28rpx;
+  font-weight: 600;
+  width: 48rpx;
+  text-align: center;
+}
+
+.wuxing-bar-track {
+  flex: 1;
+  height: 24rpx;
+  background: #F0F0F0;
+  border-radius: 12rpx;
+  overflow: hidden;
+}
+
+.wuxing-bar-fill {
+  height: 100%;
+  border-radius: 12rpx;
+}
+
+.wuxing-value {
+  font-size: 24rpx;
+  color: #666;
+  width: 60rpx;
+  text-align: right;
+}
+
+.reading-card {
+  margin-top: 20rpx;
+  padding: 16rpx;
+  background: #FFFBEB;
+  border-radius: 12rpx;
+  border-left: 6rpx solid #F59E0B;
+}
+
+.reading-label {
+  font-size: 24rpx;
+  font-weight: 600;
+  color: #D97706;
+  display: block;
+  margin-bottom: 8rpx;
+}
+
+.reading-text {
+  font-size: 24rpx;
+  color: #666;
+  line-height: 1.6;
+  display: block;
+  margin-top: 8rpx;
+}
+
+.zodiac-display {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 20rpx;
+}
+
+.zodiac-name {
+  font-size: 40rpx;
+  font-weight: 700;
+  color: #F59E0B;
+}
+
+.numsoul-display {
+  display: flex;
+  gap: 24rpx;
+  align-items: center;
+}
+
+.numsoul-number {
+  width: 100rpx;
+  height: 100rpx;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-shrink: 0;
+}
+
+.numsoul-num {
+  font-size: 44rpx;
+  font-weight: 700;
+  color: #fff;
+}
+
+.numsoul-info {
+  flex: 1;
+}
+
+.numsoul-title {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #333;
+  display: block;
+}
+
+.numsoul-keywords {
+  font-size: 24rpx;
+  color: #666;
+  display: block;
+  margin-top: 6rpx;
+}
+
+.numsoul-advice {
+  font-size: 24rpx;
+  color: #8B5CF6;
+  display: block;
+  margin-top: 8rpx;
+  line-height: 1.5;
+}
+
+.empty-data {
+  padding: 20rpx;
+  text-align: center;
+  color: #999;
+  font-size: 24rpx;
+}
+
+.empty-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 120rpx 32rpx;
+}
+
+.empty-icon {
+  font-size: 80rpx;
+  margin-bottom: 20rpx;
+}
+
+.empty-text {
+  font-size: 28rpx;
+  color: #999;
+}
+</style>

+ 514 - 0
cfc-frontend/pages/mind-detail/training.vue

@@ -0,0 +1,514 @@
+<template>
+  <view class="container">
+    <PageBanner theme="mind-wisdom"
+      tagline="能力训练"
+      :quote="currentQuote" />
+
+    <!-- 训练概览 -->
+    <view class="stats-card" v-if="stats">
+      <view class="stats-row">
+        <view class="stats-item">
+          <text class="stats-value">{{ stats.totalGames || 0 }}</text>
+          <text class="stats-label">总训练次数</text>
+        </view>
+        <view class="stats-divider"></view>
+        <view class="stats-item">
+          <text class="stats-value">{{ stats.bestScore || 0 }}</text>
+          <text class="stats-label">最高分</text>
+        </view>
+        <view class="stats-divider"></view>
+        <view class="stats-item">
+          <text class="stats-value">{{ stats.avgScore ? Math.round(stats.avgScore) : 0 }}</text>
+          <text class="stats-label">平均分</text>
+        </view>
+        <view class="stats-divider"></view>
+        <view class="stats-item">
+          <text class="stats-value">{{ stats.totalPoints || 0 }}</text>
+          <text class="stats-label">累计积分</text>
+        </view>
+      </view>
+    </view>
+    <view class="stats-card stats-loading" v-else-if="statsLoading">
+      <text class="stats-loading-text">加载训练数据...</text>
+    </view>
+
+    <!-- 训练维度描述 -->
+    <view class="dimension-card" v-if="currentDim">
+      <view class="dimension-icon-wrap" :style="{ background: currentDim.lightColor }">
+        <text class="dimension-icon">{{ currentDim.icon }}</text>
+      </view>
+      <text class="dimension-name">{{ currentDim.name }}</text>
+      <text class="dimension-desc">{{ currentDim.desc }}</text>
+    </view>
+
+    <!-- 推荐游戏 -->
+    <view class="section" v-if="currentDim && currentDim.games.length > 0">
+      <view class="section-header">
+        <text class="section-title">推荐训练</text>
+      </view>
+      <view class="game-list">
+        <view class="game-card" v-for="game in currentDim.games" :key="game.code" @click="goGame(game)">
+          <view class="game-icon-wrap" :style="{ background: currentDim.lightColor }">
+            <text class="game-card-icon">{{ game.icon }}</text>
+          </view>
+          <view class="game-info">
+            <text class="game-title">{{ game.name }}</text>
+            <text class="game-desc">{{ game.desc }}</text>
+          </view>
+          <text class="game-arrow">&#x2192;</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 成长建议 -->
+    <view class="section" v-if="currentDim">
+      <view class="section-header">
+        <text class="section-title">成长建议</text>
+      </view>
+      <view class="tip-card">
+        <view class="tip-left-bar" :style="{ background: currentDim.color }"></view>
+        <view class="tip-body">
+          <text class="tip-content">{{ currentDim.tip }}</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 其他维度训练 -->
+    <view class="section">
+      <view class="section-header">
+        <text class="section-title">其他维度</text>
+      </view>
+      <view class="other-grid">
+        <view class="other-item" v-for="dim in otherDimensions" :key="dim.key" @click="switchDimension(dim.key)">
+          <view class="other-icon-wrap" :style="{ background: dim.lightColor }">
+            <text class="other-icon">{{ dim.icon }}</text>
+          </view>
+          <text class="other-name">{{ dim.name }}</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 底部占位 -->
+    <view class="bottom-spacer"></view>
+  </view>
+</template>
+
+<script>
+import PageBanner from '../../components/PageBanner.vue'
+import { getGameStats } from '../../utils/api.js'
+
+var DIMENSION_MAP = {
+  perception: {
+    key: 'perception',
+    name: '感知训练',
+    icon: '\u{1F441}',
+    color: '#8B5CF6',
+    lightColor: '#EDE9FE',
+    desc: '通过图形匹配、颜色分辨等练习提升感知能力',
+    tip: '感知力是认知的起点。多观察身边事物的细节,锻炼视觉辨识能力,能有效提升感知水平。',
+    games: [
+      { code: 'schulte', name: '舒尔特方格', icon: '\u{1F3AF}', desc: '快速定位数字,训练视觉搜索' }
+    ]
+  },
+  focus: {
+    key: 'focus',
+    name: '专注训练',
+    icon: '\u{1F3AF}',
+    color: '#F97316',
+    lightColor: '#FFF7ED',
+    desc: '通过舒尔特方格、数字追踪等练习提升专注力',
+    tip: '专注力是一切学习的基础。每天坚持10分钟专注训练,持续21天会有显著提升。',
+    games: [
+      { code: 'schulte', name: '舒尔特方格', icon: '\u{1F3AF}', desc: '按序点击数字,训练持续专注' }
+    ]
+  },
+  memory: {
+    key: 'memory',
+    name: '记忆训练',
+    icon: '\u{1F9E0}',
+    color: '#10B981',
+    lightColor: '#ECFDF5',
+    desc: '通过数字记忆、序列回忆等练习提升记忆力',
+    tip: '记忆力可以通过科学训练显著提升。联想记忆法和间隔重复是最有效的两种方法。',
+    games: [
+      { code: '1a2b', name: '猜数字', icon: '\u{1F522}', desc: '记忆数字排列,训练工作记忆' }
+    ]
+  },
+  logic: {
+    key: 'logic',
+    name: '逻辑训练',
+    icon: '\u{1F9E9}',
+    color: '#3B82F6',
+    lightColor: '#EFF6FF',
+    desc: '通过推理题、模式识别等练习提升逻辑思维',
+    tip: '逻辑思维是解决复杂问题的关键。多做推理和归纳练习,能帮助建立清晰的思维框架。',
+    games: [
+      { code: 'sudoku', name: '数独', icon: '\u{1F9E9}', desc: '数字逻辑推理,训练演绎思维' },
+      { code: '1a2b', name: '猜数字', icon: '\u{1F522}', desc: '逻辑排除推理,训练分析能力' }
+    ]
+  },
+  spatial: {
+    key: 'spatial',
+    name: '空间训练',
+    icon: '\u{1F9CA}',
+    color: '#8D6E63',
+    lightColor: '#EFEBE9',
+    desc: '通过空间旋转、立体构建等练习提升空间思维能力',
+    tip: '空间思维对数学和科学学习至关重要。拼图和积木是提升空间能力的最佳方式。',
+    games: [
+      { code: 'sudoku', name: '数独', icon: '\u{1F9E9}', desc: '九宫格空间布局,训练空间推理' }
+    ]
+  },
+  processingSpeed: {
+    key: 'processingSpeed',
+    name: '加工速度训练',
+    icon: '\u26A1',
+    color: '#FFD700',
+    lightColor: '#FFFBEB',
+    desc: '通过快速反应、速度测试等练习提升加工速度',
+    tip: '加工速度影响学习和反应效率。速算、速读等限时练习是提升加工速度的有效方法。',
+    games: [
+      { code: 'schulte', name: '舒尔特方格', icon: '\u{1F3AF}', desc: '限时完成,训练反应速度' },
+      { code: '1a2b', name: '猜数字', icon: '\u{1F522}', desc: '快速推理,训练信息处理速度' }
+    ]
+  }
+}
+
+var GAME_PAGES = {
+  schulte: '/pages/games/schulte',
+  '1a2b': '/pages/games/1a2b',
+  sudoku: '/pages/games/sudoku'
+}
+
+export default {
+  components: { PageBanner },
+  data: function() {
+    return {
+      dimension: null,
+      childId: null,
+      stats: null,
+      statsLoading: false
+    }
+  },
+  computed: {
+    currentDim: function() {
+      var dim = DIMENSION_MAP[this.dimension]
+      return dim || null
+    },
+    currentQuote: function() {
+      var dim = this.currentDim
+      return dim ? dim.desc : '知己知彼,百战不殆'
+    },
+    otherDimensions: function() {
+      var self = this
+      var keys = Object.keys(DIMENSION_MAP)
+      var result = []
+      for (var i = 0; i < keys.length; i++) {
+        if (keys[i] !== self.dimension) {
+          result.push(DIMENSION_MAP[keys[i]])
+        }
+      }
+      return result
+    }
+  },
+  onLoad: function(options) {
+    if (options && options.dimension) {
+      this.dimension = options.dimension
+    }
+    if (options && options.childId) {
+      this.childId = options.childId
+    } else {
+      this.childId = uni.getStorageSync('currentChildId') || null
+    }
+    uni.setNavigationBarTitle({
+      title: this.currentDim ? this.currentDim.name : '能力训练'
+    })
+    this.loadStats()
+  },
+  methods: {
+    loadStats: function() {
+      var self = this
+      if (!self.childId) return
+      self.statsLoading = true
+      getGameStats({ childId: self.childId }).then(function(res) {
+        if (res && res.data) {
+          self.stats = res.data
+        }
+      }).catch(function() {
+        // silent — stats not critical
+      }).finally(function() {
+        self.statsLoading = false
+      })
+    },
+    goGame: function(game) {
+      var url = GAME_PAGES[game.code]
+      if (!url) {
+        uni.showToast({ title: '游戏暂未开放', icon: 'none' })
+        return
+      }
+      var childId = this.childId || ''
+      url = url + '?childId=' + childId + '&dimension=' + (this.dimension || '')
+      uni.navigateTo({ url: url })
+    },
+    switchDimension: function(key) {
+      this.dimension = key
+      uni.setNavigationBarTitle({
+        title: DIMENSION_MAP[key].name
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  min-height: 100vh;
+  background: #f5f7fa;
+  padding-bottom: 120rpx;
+}
+
+/* ===== 训练概览卡 ===== */
+.stats-card {
+  margin: 30rpx;
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 36rpx 24rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
+}
+
+.stats-row {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-around;
+}
+
+.stats-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  flex: 1;
+}
+
+.stats-value {
+  font-size: 44rpx;
+  font-weight: bold;
+  color: #F97316;
+  margin-bottom: 8rpx;
+}
+
+.stats-label {
+  font-size: 22rpx;
+  color: #999;
+}
+
+.stats-divider {
+  width: 1rpx;
+  height: 60rpx;
+  background: #eee;
+  flex-shrink: 0;
+}
+
+.stats-loading {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 24rpx;
+}
+
+.stats-loading-text {
+  font-size: 26rpx;
+  color: #bbb;
+}
+
+/* ===== 维度描述卡 ===== */
+.dimension-card {
+  margin: 30rpx;
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 40rpx 32rpx;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
+}
+
+.dimension-icon-wrap {
+  width: 120rpx;
+  height: 120rpx;
+  border-radius: 60rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-bottom: 20rpx;
+}
+
+.dimension-icon {
+  font-size: 56rpx;
+}
+
+.dimension-name {
+  font-size: 36rpx;
+  font-weight: bold;
+  color: #333;
+  margin-bottom: 16rpx;
+}
+
+.dimension-desc {
+  font-size: 26rpx;
+  color: #888;
+  text-align: center;
+  line-height: 1.6;
+}
+
+/* ===== 通用区段 ===== */
+.section {
+  margin: 20rpx 30rpx;
+}
+
+.section-header {
+  margin-bottom: 20rpx;
+}
+
+.section-title {
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #333;
+}
+
+/* ===== 推荐游戏列表 ===== */
+.game-list {
+  display: flex;
+  flex-direction: column;
+}
+
+.game-card {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 24rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
+  margin-bottom: 20rpx;
+}
+
+.game-card:active {
+  opacity: 0.7;
+}
+
+.game-icon-wrap {
+  width: 80rpx;
+  height: 80rpx;
+  border-radius: 40rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 20rpx;
+  flex-shrink: 0;
+}
+
+.game-card-icon {
+  font-size: 40rpx;
+}
+
+.game-info {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+}
+
+.game-title {
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #333;
+  margin-bottom: 6rpx;
+}
+
+.game-desc {
+  font-size: 22rpx;
+  color: #999;
+}
+
+.game-arrow {
+  font-size: 28rpx;
+  color: #ccc;
+  margin-left: 16rpx;
+  flex-shrink: 0;
+}
+
+/* ===== 成长建议卡 ===== */
+.tip-card {
+  display: flex;
+  flex-direction: row;
+  background: #fff;
+  border-radius: 20rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
+  overflow: hidden;
+}
+
+.tip-left-bar {
+  width: 6rpx;
+  flex-shrink: 0;
+}
+
+.tip-body {
+  flex: 1;
+  padding: 28rpx 24rpx;
+}
+
+.tip-content {
+  font-size: 26rpx;
+  color: #666;
+  line-height: 1.7;
+}
+
+/* ===== 其他维度网格 ===== */
+.other-grid {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+  margin: 0 -10rpx;
+}
+
+.other-item {
+  width: calc(33.33% - 20rpx);
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 24rpx 8rpx;
+  margin: 0 10rpx 20rpx;
+  box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.05);
+}
+
+.other-item:active {
+  opacity: 0.7;
+}
+
+.other-icon-wrap {
+  width: 64rpx;
+  height: 64rpx;
+  border-radius: 32rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-bottom: 10rpx;
+}
+
+.other-icon {
+  font-size: 32rpx;
+}
+
+.other-name {
+  font-size: 22rpx;
+  color: #666;
+  font-weight: 500;
+}
+
+/* ===== 底部 ===== */
+.bottom-spacer {
+  height: 120rpx;
+}
+</style>

+ 8 - 8
cfc-frontend/pages/mind/index.vue

@@ -326,10 +326,10 @@ export default {
         { icon: '\u{1F4D6}', label: '阅读', needLogin: false, page: '/pages/mind/articles' },
         { icon: '\u{1F4CA}', label: '测评报告', needLogin: true, page: '/pages/mind/emotion-report' },
         { icon: '\u{1F4DD}', label: '成长档案', needLogin: true, page: '/pages/growth/index' },
-        { icon: '\u{2728}', label: '情绪打卡', needLogin: true, page: '/pages/mind/emotion-checkin' },
-        { icon: '\u{1F4C8}', label: '情绪趋势', needLogin: true, page: '/pages/mind/emotion-trend' },
-        { icon: '\u{2637}', label: '传统文化', needLogin: true, page: '/pages/mind/traditional-mirror' },
-        { icon: '\u{1F9D8}', label: '疏导工具', needLogin: true, page: '/pages/mind/soothe-toolbox' }
+        { icon: '\u{2728}', label: '情绪打卡', needLogin: true, page: '/pages/mind-detail/emotion-checkin' },
+        { icon: '\u{1F4C8}', label: '情绪趋势', needLogin: true, page: '/pages/mind-detail/emotion-trend' },
+        { icon: '\u{2637}', label: '传统文化', needLogin: true, page: '/pages/mind-detail/traditional-mirror' },
+        { icon: '\u{1F9D8}', label: '疏导工具', needLogin: true, page: '/pages/mind-detail/soothe-toolbox' }
       ],
       articles: [],
 
@@ -599,10 +599,10 @@ export default {
     // ===== 导航 =====
     goEmotionReport: function() {
       var childId = this.currentChildId || ''
-      uni.navigateTo({ url: '/pages/mind/emotion-report?childId=' + childId })
+      uni.navigateTo({ url: '/pages/mind-detail/emotion-report?childId=' + childId })
     },
     goArticles: function() {
-      uni.navigateTo({ url: '/pages/mind/articles' })
+      uni.navigateTo({ url: '/pages/mind-detail/articles' })
     },
     onFuncClick: function(item) {
       if (item.needLogin && !this.isLoggedIn) {
@@ -616,7 +616,7 @@ export default {
       if (item.page) uni.navigateTo({ url: item.page })
     },
     goArticleDetail: function(id) {
-      var url = '/pages/mind/article-detail?id=' + id
+      var url = '/pages/mind-detail/article-detail?id=' + id
       uni.navigateTo({ url: url })
     },
     // ===== 孩子列表与维度数据 =====
@@ -746,7 +746,7 @@ export default {
     goMemberDetail: function(member) {
       if (!member || !member.memberId) return
       uni.navigateTo({
-        url: '/pages/mind/member-mind-detail?childId=' + member.memberId
+        url: '/pages/mind-detail/member-mind-detail?childId=' + member.memberId
       })
     },
 

+ 407 - 0
cfc-frontend/pages/wisdom-detail/cognitive-report.vue

@@ -0,0 +1,407 @@
+<template>
+  <view class="container">
+    <PageBanner theme="wisdom"
+      tagline="认知能力 · 全面评估"
+      quote="知人者智,自知者明" />
+
+    <view class="report-card" v-if="report">
+      <view class="report-header">
+        <text class="report-title">认知测评报告</text>
+        <text class="report-date">测评日期: {{ report.assessmentDate || '-' }}</text>
+      </view>
+
+      <!-- 综合认知评分 -->
+      <view class="overall-score-wrap">
+        <text class="overall-label">综合认知评分</text>
+        <view class="overall-row">
+          <text class="overall-value">{{ report.overallScore || 0 }}</text>
+          
+        </view>
+      </view>
+
+      <!-- 六维条形图 -->
+      <view class="dimension-list">
+        <view class="dimension-item" v-for="dim in dimensions" :key="dim.key">
+          <view class="dimension-header">
+            <view class="dimension-label-row">
+              <text class="dimension-icon">{{ dim.icon }}</text>
+              <text class="dimension-name">{{ dim.label }}</text>
+            </view>
+            <text class="dimension-score">{{ report[dim.key] || 0 }}</text>
+          </view>
+          <view class="dimension-bar">
+            <view class="dimension-bar-fill" :style="{ width: (report[dim.key] || 0) + '%' }"></view>
+          </view>
+          <text class="dimension-desc">{{ dim.desc }}</text>
+        </view>
+      </view>
+
+      <!-- 分析报告 -->
+      <view class="report-analysis" v-if="report.analysisReport">
+        <view class="analysis-header-row">
+          <text class="analysis-icon">&#x1F4DD;</text>
+          <text class="analysis-title">分析报告</text>
+        </view>
+        <text class="analysis-content">{{ report.analysisReport }}</text>
+      </view>
+
+      <!-- 成长建议 -->
+      <view class="report-suggestions" v-if="report.growthSuggestions">
+        <view class="analysis-header-row">
+          <text class="analysis-icon">&#x1F331;</text>
+          <text class="analysis-title">成长建议</text>
+        </view>
+        <text class="suggestion-content">{{ report.growthSuggestions }}</text>
+      </view>
+
+      <!-- 底部操作 -->
+      <view class="action-row">
+        <view class="action-btn secondary" @click="goAssessment">
+          <text class="action-btn-text">预约测评</text>
+        </view>
+        <view class="action-btn primary" @click="goTraining">
+          <text class="action-btn-text">认知训练</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 加载中 -->
+    <view class="loading-state" v-if="loading">
+      <text class="loading-text">加载中...</text>
+    </view>
+
+    <!-- 无数据 -->
+    <view class="empty-state" v-if="!report && !loading">
+      <text class="empty-icon">&#x1F9E0;</text>
+      <text class="empty-title">暂无认知测评数据</text>
+      <text class="empty-desc">完成 DAN 认知评估后,可在此查看详细的六维认知分析报告</text>
+      <view class="empty-btn" @click="goAssessment">预约测评</view>
+    </view>
+  </view>
+</template>
+
+<script>
+import PageBanner from '../../components/PageBanner.vue'
+import { getAssessmentLatestResult } from '../../utils/api.js'
+
+export default {
+  components: { PageBanner },
+  data: function() {
+    return {
+      childId: null,
+      report: null,
+      loading: true,
+      dimensions: [
+        { key: 'perceptionScore', label: '感知能力', icon: '\u{1F441}', desc: '感官信息处理能力' },
+        { key: 'focusScore', label: '专注力', icon: '\u{1F3AF}', desc: '持续集中和抗干扰能力' },
+        { key: 'memoryScore', label: '记忆力', icon: '\u{1F9E0}', desc: '信息编码和回忆能力' },
+        { key: 'logicScore', label: '逻辑思维', icon: '\u{1F9E9}', desc: '推理和问题解决能力' },
+        { key: 'spatialScore', label: '空间思维', icon: '\u{1F9CA}', desc: '空间想象和方位判断' },
+        { key: 'processingSpeedScore', label: '加工速度', icon: '\u26A1', desc: '信息处理和反应速度' }
+      ]
+    }
+  },
+  onLoad: function(options) {
+    if (options && options.childId) {
+      this.childId = options.childId
+    } else {
+      this.childId = uni.getStorageSync('currentChildId') || null
+    }
+    this.loadReport()
+  },
+  methods: {
+    loadReport: function() {
+      var self = this
+      var childId = self.childId
+      if (!childId) {
+        self.loading = false
+        return
+      }
+      self.loading = true
+      getAssessmentLatestResult(childId).then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.report = res.data
+        }
+        self.loading = false
+      }).catch(function() {
+        self.loading = false
+      })
+    },
+    goAssessment: function() {
+      uni.navigateTo({ url: '/pages/assessment/apply' })
+    },
+    goTraining: function() {
+      uni.navigateTo({ url: '/pages/mind/training?childId=' + (this.childId || '') })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  min-height: 100vh;
+  background: #f5f7fa;
+  padding-bottom: 120rpx;
+}
+
+/* ===== 报告卡 ===== */
+.report-card {
+  margin: 20rpx 30rpx;
+  background: #fff;
+  border-radius: 24rpx;
+  padding: 40rpx;
+  box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06);
+}
+
+.report-header {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 30rpx;
+}
+
+.report-title {
+  font-size: 36rpx;
+  font-weight: bold;
+  color: #333;
+}
+
+.report-date {
+  font-size: 24rpx;
+  color: #999;
+}
+
+/* ===== 综合评分 ===== */
+.overall-score-wrap {
+  text-align: center;
+  padding: 40rpx 0;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  border-radius: 20rpx;
+  margin-bottom: 40rpx;
+  color: #fff;
+}
+
+.overall-label {
+  font-size: 28rpx;
+  opacity: 0.9;
+  display: block;
+  margin-bottom: 16rpx;
+}
+
+.overall-row {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: center;
+}
+
+.overall-value {
+  font-size: 80rpx;
+  font-weight: bold;
+  margin-right: 20rpx;
+}
+
+.dan-badge {
+  background: rgba(255,255,255,0.25);
+  border-radius: 12rpx;
+  padding: 6rpx 20rpx;
+}
+
+.dan-level {
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #FFD700;
+}
+
+/* ===== 七维条形图 ===== */
+.dimension-list {
+  margin-bottom: 30rpx;
+}
+
+.dimension-item {
+  margin-bottom: 28rpx;
+}
+
+.dimension-header {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 10rpx;
+}
+
+.dimension-label-row {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+
+.dimension-icon {
+  font-size: 28rpx;
+  margin-right: 10rpx;
+}
+
+.dimension-name {
+  font-size: 28rpx;
+  color: #333;
+  font-weight: 500;
+}
+
+.dimension-score {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #667eea;
+}
+
+.dimension-bar {
+  height: 16rpx;
+  background: #f0f0f0;
+  border-radius: 8rpx;
+  overflow: hidden;
+  margin-bottom: 8rpx;
+}
+
+.dimension-bar-fill {
+  height: 100%;
+  background: linear-gradient(90deg, #667eea, #764ba2);
+  border-radius: 8rpx;
+  transition: width 0.5s;
+}
+
+.dimension-desc {
+  font-size: 22rpx;
+  color: #999;
+  line-height: 1.4;
+}
+
+/* ===== 分析/建议 ===== */
+.report-analysis, .report-suggestions {
+  margin-top: 30rpx;
+  padding: 28rpx;
+  border-radius: 16rpx;
+}
+
+.report-analysis {
+  background: #f9f9ff;
+}
+
+.report-suggestions {
+  background: #f0fdf4;
+}
+
+.analysis-header-row {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  margin-bottom: 16rpx;
+}
+
+.analysis-icon {
+  font-size: 30rpx;
+  margin-right: 10rpx;
+}
+
+.analysis-title {
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #333;
+}
+
+.analysis-content, .suggestion-content {
+  font-size: 26rpx;
+  color: #666;
+  line-height: 1.7;
+  display: block;
+}
+
+/* ===== 底部操作 ===== */
+.action-row {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  margin-top: 40rpx;
+}
+
+.action-btn {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 20rpx 0;
+  border-radius: 40rpx;
+}
+
+.action-btn.primary {
+  background: linear-gradient(135deg, #667eea, #764ba2);
+  margin-left: 16rpx;
+  box-shadow: 0 4rpx 16rpx rgba(102,126,234,0.3);
+}
+
+.action-btn.secondary {
+  background: #fff;
+  border: 2rpx solid #ddd;
+  margin-right: 16rpx;
+}
+
+.action-btn-text {
+  font-size: 28rpx;
+  font-weight: 500;
+  color: #fff;
+}
+
+.action-btn.secondary .action-btn-text {
+  color: #666;
+}
+
+/* ===== 加载中 ===== */
+.loading-state {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 120rpx 0;
+}
+
+.loading-text {
+  font-size: 28rpx;
+  color: #999;
+}
+
+/* ===== 空状态 ===== */
+.empty-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 120rpx 40rpx 80rpx;
+}
+
+.empty-icon {
+  font-size: 100rpx;
+  margin-bottom: 20rpx;
+}
+
+.empty-title {
+  font-size: 32rpx;
+  color: #333;
+  font-weight: bold;
+  margin-bottom: 16rpx;
+}
+
+.empty-desc {
+  font-size: 26rpx;
+  color: #999;
+  text-align: center;
+  line-height: 1.6;
+  margin-bottom: 40rpx;
+}
+
+.empty-btn {
+  background: linear-gradient(135deg, #667eea, #764ba2);
+  color: #fff;
+  font-size: 28rpx;
+  font-weight: bold;
+  padding: 16rpx 64rpx;
+  border-radius: 40rpx;
+  box-shadow: 0 4rpx 16rpx rgba(102,126,234,0.3);
+}
+</style>

+ 267 - 0
cfc-frontend/pages/wisdom-detail/member-wisdom-detail.vue

@@ -0,0 +1,267 @@
+<template>
+  <view class="detail-container">
+    <!-- 顶部导航 -->
+    <view class="nav-bar">
+      <view class="nav-back" @click="goBack">
+        <text class="nav-back-icon">&#x2190;</text>
+      </view>
+      <text class="nav-title">智 · 成员详情</text>
+    </view>
+
+    <!-- 五维能量条 -->
+    <FamilyEnergyBar
+      dimensionCode="wisdom"
+      :sandboxData="sandboxData"
+      :dualDimension="dualDimension" />
+
+    <!-- 成员信息卡 -->
+    <view class="member-card" v-if="memberInfo">
+      <view class="member-avatar wisdom-avatar">
+        <text class="avatar-text">{{ memberInfo.name && memberInfo.name.charAt(0) || '孩' }}</text>
+      </view>
+      <view class="member-info">
+        <text class="member-name">{{ memberInfo.name || '孩子' }}</text>
+        <text class="member-role">智慧维度</text>
+      </view>
+      <view class="member-score" v-if="dualDimension">
+        <text class="score-value wisdom-score">{{ dualDimension.energy || 0 }}</text>
+        <text class="score-label">能量值</text>
+      </view>
+    </view>
+
+    <!-- 家庭成员关系图谱(只读) -->
+    <FamilyRelationGraph
+      dimensionCode="wisdom"
+      :selfId="selfId"
+      :members="graphMembers"
+      :energyMap="energyMapForGraph"
+      :intimacyMap="intimacyMapForGraph"
+      :interactive="false" />
+
+    <!-- 今日任务 -->
+    <DimensionTasks
+      dimensionCode="wisdom"
+      :tasks="dimensionTasks"
+      @taskClick="onTaskClick"
+      @moreTasks="goTasks" />
+
+    <!-- 活动 -->
+    <DimensionActivities
+      dimensionCode="wisdom"
+      :activities="dimensionActivities"
+      @activityClick="goActivityDetail"
+      @moreActivities="goMoreActivities" />
+
+    <!-- 商品 -->
+    <DimensionProducts
+      dimensionCode="wisdom"
+      :products="dimensionProducts"
+      @productClick="goProductDetail"
+      @moreProducts="goMoreProducts" />
+
+    <!-- 底部占位 -->
+    <view class="bottom-spacer"></view>
+  </view>
+</template>
+
+<script>
+import FamilyEnergyBar from '../../components/FamilyEnergyBar.vue'
+import FamilyRelationGraph from '../../components/FamilyRelationGraph.vue'
+import DimensionTasks from '../../components/DimensionTasks.vue'
+import DimensionActivities from '../../components/DimensionActivities.vue'
+import DimensionProducts from '../../components/DimensionProducts.vue'
+import { getEnergyOverview, getFamilyEnergySandbox, getTodayTasksByCategory, getActivityList, getProductsByDomain, getChildren } from '../../utils/api.js'
+
+export default {
+  components: { FamilyEnergyBar, FamilyRelationGraph, DimensionTasks, DimensionActivities, DimensionProducts },
+  data() {
+    return {
+      childId: null,
+      selfId: null,
+      memberInfo: null,
+      graphMembers: [],
+      energyMapForGraph: {},
+      intimacyMapForGraph: {},
+      dimensionTasks: [],
+      dimensionActivities: [],
+      dimensionProducts: [],
+      dualDimension: null,
+      sandboxData: null
+    }
+  },
+  onLoad: function(options) {
+    if (options && options.memberId) {
+      this.selfId = options.memberId
+      this.childId = options.memberId
+      this.loadMemberData()
+    }
+  },
+  methods: {
+    goBack: function() {
+      uni.navigateBack()
+    },
+    loadMemberData: function() {
+      var self = this
+      var childId = this.childId
+      if (!childId) return
+
+      getEnergyOverview(childId).then(function(res) {
+        if (res && res.data) {
+          self.dualDimension = res.data
+        }
+      }).catch(function() {})
+
+      getFamilyEnergySandbox(childId).then(function(res) {
+        if (res && res.data) {
+          self.sandboxData = res.data.sandboxData || null
+          self.graphMembers = res.data.members || []
+          self.energyMapForGraph = res.data.energyMap || {}
+          self.intimacyMapForGraph = res.data.intimacyMap || {}
+        }
+      }).catch(function() {})
+
+      getTodayTasksByCategory({ childId: childId, dimension: 'wisdom' }).then(function(res) {
+        if (res && res.data) {
+          self.dimensionTasks = res.data.slice(0, 5)
+        }
+      }).catch(function() {})
+
+      getActivityList({ dimension: 'wisdom', page: 1, size: 5 }).then(function(res) {
+        if (res && res.data) {
+          self.dimensionActivities = res.data.records || res.data
+        }
+      }).catch(function() {})
+
+      getProductsByDomain({ domain: 'wisdom', page: 1, size: 4 }).then(function(res) {
+        if (res && res.data) {
+          self.dimensionProducts = res.data.records || res.data
+        }
+      }).catch(function() {})
+    },
+    onTaskClick: function(task) {},
+    goTasks: function() { uni.switchTab({ url: '/pages/tasks/tasks' }) },
+    goActivityDetail: function(id) {},
+    goMoreActivities: function() {},
+    goProductDetail: function(id) {
+      var token = uni.getStorageSync('token')
+      if (token) {
+        uni.navigateTo({ url: '/pages/discover/product-detail/product-detail?id=' + id })
+      } else {
+        uni.navigateTo({ url: '/pages/login/login' })
+      }
+    },
+    goMoreProducts: function() {}
+  }
+}
+</script>
+
+<style scoped>
+.detail-container {
+  min-height: 100vh;
+  background: #f5f7fa;
+  padding-bottom: 120rpx;
+}
+
+.nav-bar {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  padding: 80rpx 30rpx 20rpx;
+  background: linear-gradient(135deg, #FFD700, #FFA500);
+}
+
+.nav-back {
+  width: 60rpx;
+  height: 60rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.nav-back-icon {
+  font-size: 36rpx;
+  color: #fff;
+}
+
+.nav-title {
+  flex: 1;
+  text-align: center;
+  font-size: 34rpx;
+  font-weight: bold;
+  color: #fff;
+  margin-right: 60rpx;
+}
+
+.member-card {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  background: #fff;
+  margin: 20rpx 30rpx;
+  padding: 30rpx;
+  border-radius: 20rpx;
+  box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06);
+}
+
+.member-avatar {
+  width: 100rpx;
+  height: 100rpx;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 24rpx;
+}
+
+.wisdom-avatar {
+  background: linear-gradient(135deg, #FFD700, #FFA500);
+}
+
+.avatar-text {
+  font-size: 40rpx;
+  font-weight: bold;
+  color: #fff;
+}
+
+.member-info {
+  flex: 1;
+}
+
+.member-name {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #333;
+  display: block;
+}
+
+.member-role {
+  font-size: 24rpx;
+  color: #999;
+  margin-top: 6rpx;
+  display: block;
+}
+
+.member-score {
+  text-align: center;
+}
+
+.score-value {
+  font-size: 40rpx;
+  font-weight: bold;
+  display: block;
+}
+
+.wisdom-score {
+  color: #B8860B;
+}
+
+.score-label {
+  font-size: 22rpx;
+  color: #999;
+  display: block;
+}
+
+.bottom-spacer {
+  height: 40rpx;
+}
+</style>

+ 921 - 0
cfc-frontend/pages/wisdom-detail/self-quiz.vue

@@ -0,0 +1,921 @@
+<template>
+  <view class="container">
+    <view class="resume-prompt" v-if="showResume">
+      <text class="resume-text">检测到上次未完成的测评</text>
+      <view class="resume-btns">
+        <view class="btn btn--outline" @tap="resumeSession">继续答题</view>
+        <view class="btn btn--ghost" @tap="clearSession">重新开始</view>
+      </view>
+    </view>
+
+    <view class="step-wrap" v-if="!showResume">
+      <view class="step-row">
+        <view class="step-item" v-for="i in 4" :key="i" @tap="goStep(i)">
+          <view class="step-dot" :class="{
+            'step-dot--active': step === i,
+            'step-dot--done': step > i,
+            'step-dot--pending': step < i
+          }">
+            <text v-if="step > i" class="step-check">&#10003;</text>
+            <text v-else class="step-num">{{ i }}</text>
+          </view>
+          <text class="step-label" :class="{
+            'step-label--active': step >= i,
+            'step-label--muted': step < i
+          }">{{ stepLabels[i - 1] }}</text>
+        </view>
+      </view>
+    </view>
+
+    <view class="step-content" v-if="step === 1 && !showResume">
+      <view class="content-header">
+        <text class="content-title">选择测评目标</text>
+        <text class="content-sub">选择你想提升的能力维度</text>
+      </view>
+      <view class="goal-grid">
+        <view class="goal-card" v-for="g in goals" :key="g.id" :class="{ 'goal-card--selected': selectedGoal && selectedGoal.id === g.id }" @tap="selectGoal(g)">
+          <view class="goal-icon-wrap">
+            <text class="goal-icon">{{ g.icon }}</text>
+          </view>
+          <text class="goal-title">{{ g.title }}</text>
+          <text class="goal-desc">{{ g.desc }}</text>
+        </view>
+      </view>
+      <view class="action-bar">
+        <view class="btn btn--primary" :class="{ 'btn--disabled': !selectedGoal }" @tap="goStep2">下一步</view>
+      </view>
+    </view>
+
+    <view class="step-content" v-if="step === 2 && !showResume">
+      <view class="content-header">
+        <text class="content-title">选择题目类型</text>
+        <text class="content-sub">共5道题,选择适合的答题方式</text>
+      </view>
+      <view class="type-list">
+        <view class="type-card" v-for="t in questionTypes" :key="t.id" :class="{ 'type-card--selected': questionType === t.id }" @tap="selectType(t.id)">
+          <view class="type-icon-wrap">
+            <text class="type-icon">{{ t.icon }}</text>
+          </view>
+          <view class="type-info">
+            <text class="type-name">{{ t.name }}</text>
+            <text class="type-desc">{{ t.desc }}</text>
+          </view>
+          <view class="type-radio" :class="{ 'type-radio--checked': questionType === t.id }">
+            <view v-if="questionType === t.id" class="type-radio-dot"></view>
+          </view>
+        </view>
+      </view>
+      <view class="action-bar action-bar--double">
+        <view class="btn btn--outline" @tap="goStep1">上一步</view>
+        <view class="btn btn--primary" :class="{ 'btn--disabled': !questionType }" @tap="startQuiz">开始答题</view>
+      </view>
+    </view>
+
+    <view class="step-content" v-if="step === 3 && !showResume">
+      <view class="progress-header">
+        <text class="progress-label">第 {{ currentQ + 1 }}/{{ questions.length }} 题</text>
+        <view class="progress-bar">
+          <view class="progress-fill" :style="'width:' + progressPct + '%'"></view>
+        </view>
+      </view>
+      <view class="question-card" v-if="currentQuestion">
+        <view class="q-tag">{{ tagLabel }}</view>
+        <text class="q-text">{{ currentQuestion.question }}</text>
+        <view class="q-options" v-if="currentQuestion.isTF">
+          <view class="q-option q-option--tf" :class="{ 'q-option--selected': currentAnswer === '正确' }" @tap="currentAnswer = '正确'">
+            <text class="q-option-tf-text">正确</text>
+          </view>
+          <view class="q-option q-option--tf" :class="{ 'q-option--selected': currentAnswer === '错误' }" @tap="currentAnswer = '错误'">
+            <text class="q-option-tf-text">错误</text>
+          </view>
+        </view>
+        <view class="q-options" v-else>
+          <view class="q-option" v-for="(opt, oi) in currentQuestion.options" :key="oi" :class="{ 'q-option--selected': currentAnswer === opt }" @tap="currentAnswer = opt">
+            <view class="q-radio" :class="{ 'q-radio--checked': currentAnswer === opt }">
+              <view v-if="currentAnswer === opt" class="q-radio-dot"></view>
+            </view>
+            <text class="q-option-text">{{ opt }}</text>
+          </view>
+        </view>
+      </view>
+      <view class="action-bar action-bar--double">
+        <view class="btn btn--outline" :class="{ 'btn--disabled': currentQ === 0 }" @tap="prevQuestion">上一题</view>
+        <view class="btn btn--primary" :class="{ 'btn--disabled': !currentAnswer }" @tap="nextQuestion">{{ currentQ < questions.length - 1 ? '下一题' : '查看结果' }}</view>
+      </view>
+    </view>
+
+    <view class="step-content" v-if="step === 4 && !showResume">
+      <view class="result-header">
+        <view class="score-circle">
+          <text class="score-value">{{ score }}/5</text>
+        </view>
+        <text class="score-label">{{ resultMessage }}</text>
+      </view>
+      <view class="section-title">维度分析</view>
+      <view class="result-dims">
+        <view class="dim-bar-item" v-for="(dim, di) in dimBreakdown" :key="di">
+          <view class="dim-bar-header">
+            <text class="dim-bar-name">{{ dim.name }}</text>
+            <text class="dim-bar-count">{{ dim.correct }}/{{ dim.total }}</text>
+          </view>
+          <view class="dim-bar-track">
+            <view class="dim-bar-fill" :style="'width:' + dim.pct + '%'"></view>
+          </view>
+        </view>
+      </view>
+      <view class="action-bar">
+        <view class="btn btn--primary" @tap="resetQuiz">再测一次</view>
+        <view class="btn btn--outline" @tap="goHome">返回首页</view>
+      </view>
+    </view>
+
+    <view class="bottom-spacer"></view>
+  </view>
+</template>
+
+<script>
+var QUESTION_BANK = [
+  { dimension: 'focus', question: '以下哪个因素最容易影响注意力集中?', options: ['噪音', '温度', '光照', '以上都是'], answer: '以上都是' },
+  { dimension: 'focus', question: '持续专注工作建议多久休息一次?', options: ['15分钟', '30分钟', '45分钟', '60分钟'], answer: '45分钟' },
+  { dimension: 'focus', question: '舒尔特方格的训练目标是提升什么?', options: ['记忆力', '专注力', '逻辑思维', '创造力'], answer: '专注力' },
+  { dimension: 'memory', question: '短期记忆通常能记住几个信息组块?', options: ['3-4个', '5-9个', '10-15个', '20个以上'], answer: '5-9个' },
+  { dimension: 'memory', question: '以下哪个方法有助于增强记忆?', options: ['死记硬背', '联想记忆', '反复抄写', '一次性学习'], answer: '联想记忆' },
+  { dimension: 'memory', question: '睡眠对记忆的影响是什么?', options: ['削弱记忆', '巩固记忆', '无影响', '干扰记忆'], answer: '巩固记忆' },
+  { dimension: 'logic', question: '如果A>B且B>C,那么?', options: ['A=C', 'A>C', 'A<C', '不确定'], answer: 'A>C' },
+  { dimension: 'logic', question: '1, 1, 2, 3, 5, 8, ? 下一个数是多少?', options: ['10', '11', '12', '13'], answer: '13' },
+  { dimension: 'logic', question: '以下哪个不是有效推理方式?', options: ['归纳推理', '演绎推理', '循环论证', '类比推理'], answer: '循环论证' },
+  { dimension: 'perception', question: '人眼能分辨约多少种颜色?', options: ['几百种', '几千种', '几万种', '数百万种'], answer: '数百万种' },
+  { dimension: 'perception', question: '以下哪种错觉与视觉感知有关?', options: ['缪勒-莱尔错觉', '德布罗意错觉', '薛定谔错觉', '迈克尔逊错觉'], answer: '缪勒-莱尔错觉' },
+  { dimension: 'perception', question: '感觉适应是指什么?', options: ['敏感度降低', '敏感度升高', '感知消失', '感知增强'], answer: '敏感度降低' },
+  { dimension: 'spatial', question: '正方体有多少个面?', options: ['4', '6', '8', '12'], answer: '6' },
+  { dimension: 'spatial', question: '以下哪个是立体几何的基本要素?', options: ['点线面', '正反合', '红黄蓝', '上下左右'], answer: '点线面' },
+  { dimension: 'spatial', question: '一个立方体展开图有几个正方形?', options: ['4', '6', '8', '12'], answer: '6' },
+  { dimension: 'speed', question: '反应时实验测量的是什么?', options: ['思考速度', '反应速度', '运动速度', '阅读速度'], answer: '反应速度' },
+  { dimension: 'speed', question: '以下哪个因素会影响加工速度?', options: ['年龄', '情绪', '疲劳', '以上都是'], answer: '以上都是' },
+  { dimension: 'speed', question: '信息加工速度最快的年龄段是?', options: ['儿童期', '青春期', '成年早期', '中老年期'], answer: '成年早期' }
+]
+
+export default {
+  data() {
+    return {
+      step: 1,
+      selectedGoal: null,
+      questionType: null,
+      questions: [],
+      currentQ: 0,
+      answers: [],
+      showResume: false,
+      savedSession: null,
+      goals: [
+        { id: 'focus', title: '专注力', desc: '提升注意力集中能力', icon: '集' },
+        { id: 'memory', title: '记忆力', desc: '增强信息记忆与回忆能力', icon: '忆' },
+        { id: 'logic', title: '逻辑思维', desc: '锻炼逻辑推理与判断能力', icon: '逻' },
+        { id: 'perception', title: '感知觉', desc: '提高感官信息处理能力', icon: '感' },
+        { id: 'spatial', title: '空间思维', desc: '培养空间想象与构建能力', icon: '间' },
+        { id: 'speed', title: '加工速度', desc: '加快信息处理与反应速度', icon: '速' }
+      ],
+      questionTypes: [
+        { id: 'choice', name: '选择题', desc: '四选一标准选择题', icon: '选' },
+        { id: 'truefalse', name: '判断题', desc: '判断陈述正误', icon: '判' },
+        { id: 'mix', name: '混合题型', desc: '选择题与判断题混合', icon: '混' }
+      ],
+      stepLabels: ['目标', '题型', '答题', '结果']
+    }
+  },
+  computed: {
+    currentQuestion: function() {
+      if (this.questions.length === 0) {
+        return null
+      }
+      return this.questions[this.currentQ]
+    },
+    currentAnswer: {
+      get: function() {
+        return this.answers[this.currentQ] || null
+      },
+      set: function(val) {
+        this.$set(this.answers, this.currentQ, val)
+      }
+    },
+    progressPct: function() {
+      if (this.questions.length === 0) {
+        return 0
+      }
+      return ((this.currentQ + 1) / this.questions.length) * 100
+    },
+    score: function() {
+      var correct = 0
+      for (var i = 0; i < this.questions.length; i++) {
+        var q = this.questions[i]
+        var a = this.answers[i]
+        if (q && a && q.answer === a) {
+          correct++
+        }
+      }
+      return correct
+    },
+    resultMessage: function() {
+      var s = this.score
+      if (s >= 5) return '太棒了!满分通过!'
+      if (s >= 4) return '非常优秀!继续保持!'
+      if (s >= 3) return '表现不错,还有提升空间!'
+      return '继续加油,多练习会更好!'
+    },
+    tagLabel: function() {
+      if (!this.currentQuestion) return ''
+      var dimMap = {
+        focus: '专注力',
+        memory: '记忆力',
+        logic: '逻辑思维',
+        perception: '感知觉',
+        spatial: '空间思维',
+        speed: '加工速度'
+      }
+      return dimMap[this.currentQuestion.dimension] || ''
+    },
+    dimBreakdown: function() {
+      var dimNames = {
+        focus: '专注力',
+        memory: '记忆力',
+        logic: '逻辑思维',
+        perception: '感知觉',
+        spatial: '空间思维',
+        speed: '加工速度'
+      }
+      var dims = {}
+      for (var i = 0; i < this.questions.length; i++) {
+        var q = this.questions[i]
+        if (!q) continue
+        if (!dims[q.dimension]) {
+          dims[q.dimension] = { total: 0, correct: 0 }
+        }
+        dims[q.dimension].total++
+        if (this.answers[i] && q.answer === this.answers[i]) {
+          dims[q.dimension].correct++
+        }
+      }
+      var result = []
+      var keys = Object.keys(dims)
+      for (var k = 0; k < keys.length; k++) {
+        var key = keys[k]
+        var item = dims[key]
+        result.push({
+          name: dimNames[key] || key,
+          total: item.total,
+          correct: item.correct,
+          pct: item.total > 0 ? Math.round((item.correct / item.total) * 100) : 0
+        })
+      }
+      return result
+    }
+  },
+  onLoad: function() {
+    this.checkSession()
+  },
+  onUnload: function() {
+    this.saveSession()
+  },
+  methods: {
+    checkSession: function() {
+      var saved = uni.getStorageSync('selfQuizSession')
+      if (saved) {
+        try {
+          var session = JSON.parse(saved)
+          if (session && session.step && session.step >= 2 && session.step <= 3) {
+            this.showResume = true
+            this.savedSession = session
+          } else {
+            uni.removeStorageSync('selfQuizSession')
+          }
+        } catch (e) {
+          uni.removeStorageSync('selfQuizSession')
+        }
+      }
+    },
+    saveSession: function() {
+      var session = {
+        step: this.step,
+        selectedGoal: this.selectedGoal,
+        questionType: this.questionType,
+        questions: this.questions,
+        currentQ: this.currentQ,
+        answers: this.answers
+      }
+      uni.setStorageSync('selfQuizSession', JSON.stringify(session))
+    },
+    resumeSession: function() {
+      if (!this.savedSession) return
+      var s = this.savedSession
+      this.step = s.step
+      this.selectedGoal = s.selectedGoal
+      this.questionType = s.questionType
+      this.questions = s.questions || []
+      this.currentQ = s.currentQ || 0
+      this.answers = s.answers || []
+      this.showResume = false
+      this.savedSession = null
+    },
+    clearSession: function() {
+      uni.removeStorageSync('selfQuizSession')
+      this.showResume = false
+      this.savedSession = null
+    },
+    selectGoal: function(g) {
+      this.selectedGoal = g
+    },
+    goStep2: function() {
+      if (this.selectedGoal) {
+        this.step = 2
+        this.saveSession()
+      }
+    },
+    goStep1: function() {
+      this.step = 1
+      this.saveSession()
+    },
+    selectType: function(id) {
+      this.questionType = id
+    },
+    startQuiz: function() {
+      if (!this.questionType) return
+      this.generateQuestions()
+      this.currentQ = 0
+      this.answers = []
+      this.step = 3
+      this.saveSession()
+    },
+    shuffle: function(arr) {
+      var a = arr.slice()
+      for (var i = a.length - 1; i > 0; i--) {
+        var j = Math.floor(Math.random() * (i + 1))
+        var tmp = a[i]
+        a[i] = a[j]
+        a[j] = tmp
+      }
+      return a
+    },
+    convertToTF: function(q) {
+      var opts = q.options
+      var idx = opts.indexOf(q.answer)
+      var wrongOpts = []
+      for (var w = 0; w < opts.length; w++) {
+        if (opts[w] !== q.answer) {
+          wrongOpts.push(opts[w])
+        }
+      }
+      var isTrue = Math.random() > 0.4
+      var claimIdx
+      if (isTrue) {
+        claimIdx = idx
+      } else {
+        var randomWrong = wrongOpts[Math.floor(Math.random() * wrongOpts.length)]
+        claimIdx = opts.indexOf(randomWrong)
+      }
+      return {
+        dimension: q.dimension,
+        question: q.question + ' 正确答案是"' + opts[claimIdx] + '"',
+        options: ['正确', '错误'],
+        answer: isTrue ? '正确' : '错误',
+        isTF: true
+      }
+    },
+    generateQuestions: function() {
+      var goalId = this.selectedGoal && this.selectedGoal.id
+      if (!goalId) return
+      var pool = []
+      for (var p = 0; p < QUESTION_BANK.length; p++) {
+        if (QUESTION_BANK[p].dimension === goalId) {
+          pool.push(QUESTION_BANK[p])
+        }
+      }
+      var shuffled = this.shuffle(pool)
+      var type = this.questionType
+      if (type === 'truefalse') {
+        var tfQuestions = []
+        var count = Math.min(5, shuffled.length)
+        for (var ti = 0; ti < count; ti++) {
+          tfQuestions.push(this.convertToTF(shuffled[ti]))
+        }
+        this.questions = tfQuestions
+      } else if (type === 'mix') {
+        var allDims = ['focus', 'memory', 'logic', 'perception', 'spatial', 'speed']
+        var otherDims = []
+        for (var od = 0; od < allDims.length; od++) {
+          if (allDims[od] !== goalId) {
+            otherDims.push(allDims[od])
+          }
+        }
+        otherDims = this.shuffle(otherDims)
+        var selectedDims = [goalId].concat(otherDims.slice(0, 4))
+        var mixedQuestions = []
+        for (var di = 0; di < selectedDims.length; di++) {
+          var dimPool = []
+          for (var dq = 0; dq < QUESTION_BANK.length; dq++) {
+            if (QUESTION_BANK[dq].dimension === selectedDims[di]) {
+              dimPool.push(QUESTION_BANK[dq])
+            }
+          }
+          if (dimPool.length > 0) {
+            var pick = this.shuffle(dimPool)
+            mixedQuestions.push(pick[0])
+          }
+        }
+        this.questions = this.shuffle(mixedQuestions).slice(0, 5)
+      } else {
+        this.questions = shuffled.slice(0, 5)
+      }
+    },
+    nextQuestion: function() {
+      if (!this.currentAnswer) return
+      if (this.currentQ < this.questions.length - 1) {
+        this.currentQ++
+        this.saveSession()
+      } else {
+        this.step = 4
+        this.saveSession()
+      }
+    },
+    prevQuestion: function() {
+      if (this.currentQ > 0) {
+        this.currentQ--
+      }
+    },
+    resetQuiz: function() {
+      uni.removeStorageSync('selfQuizSession')
+      this.step = 1
+      this.selectedGoal = null
+      this.questionType = null
+      this.questions = []
+      this.currentQ = 0
+      this.answers = []
+    },
+    goHome: function() {
+      uni.switchTab({ url: '/pages/wisdom/index' })
+    },
+    goStep: function(n) {
+      if (n === this.step) return
+      if (n < this.step) {
+        this.step = n
+        this.saveSession()
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  min-height: 100vh;
+  background: #f5f7fa;
+  padding: 30rpx;
+  padding-bottom: 120rpx;
+  box-sizing: border-box;
+}
+
+.resume-prompt {
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 50rpx 30rpx;
+  text-align: center;
+  margin-top: 160rpx;
+  box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06);
+}
+.resume-text {
+  font-size: 28rpx;
+  color: #333;
+  display: block;
+  margin-bottom: 30rpx;
+}
+.resume-btns {
+  display: flex;
+  justify-content: center;
+  gap: 20rpx;
+}
+
+.step-wrap {
+  margin-bottom: 10rpx;
+}
+.step-row {
+  display: flex;
+  justify-content: center;
+  align-items: flex-start;
+  padding: 20rpx 0;
+}
+.step-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  margin: 0 24rpx;
+}
+.step-dot {
+  width: 48rpx;
+  height: 48rpx;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  border: 3rpx solid #ddd;
+  background: #fff;
+  transition: all 0.3s;
+}
+.step-dot--active {
+  background: #FFD700;
+  border-color: #FFD700;
+}
+.step-dot--done {
+  border-color: #FFD700;
+  background: #FFD700;
+}
+.step-dot--pending {
+  border-color: #ddd;
+  background: #fff;
+}
+.step-check {
+  font-size: 22rpx;
+  color: #fff;
+  font-weight: bold;
+}
+.step-num {
+  font-size: 22rpx;
+  color: #999;
+}
+.step-dot--active .step-num {
+  color: #fff;
+}
+.step-label {
+  font-size: 20rpx;
+  margin-top: 8rpx;
+  color: #999;
+}
+.step-label--active {
+  color: #FFD700;
+  font-weight: bold;
+}
+.step-label--muted {
+  color: #ccc;
+}
+
+.step-content {
+  animation: fadeIn 0.3s ease;
+}
+@keyframes fadeIn {
+  from { opacity: 0; transform: translateY(20rpx); }
+  to { opacity: 1; transform: translateY(0); }
+}
+
+.content-header {
+  text-align: center;
+  margin-bottom: 30rpx;
+}
+.content-title {
+  font-size: 36rpx;
+  font-weight: bold;
+  color: #333;
+  display: block;
+}
+.content-sub {
+  font-size: 24rpx;
+  color: #999;
+  margin-top: 10rpx;
+  display: block;
+}
+.section-title {
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #333;
+  margin-bottom: 20rpx;
+}
+
+.goal-grid {
+  display: flex;
+  flex-wrap: wrap;
+}
+.goal-card {
+  width: calc(50% - 12rpx);
+  margin: 6rpx;
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 30rpx 16rpx;
+  box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06);
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  border: 3rpx solid transparent;
+  transition: all 0.3s;
+  box-sizing: border-box;
+}
+.goal-card--selected {
+  border-color: #FFD700;
+  background: #FFF8E1;
+}
+.goal-icon-wrap {
+  width: 80rpx;
+  height: 80rpx;
+  border-radius: 50%;
+  background: linear-gradient(135deg, #FFD700, #FFA500);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-bottom: 16rpx;
+}
+.goal-icon {
+  font-size: 32rpx;
+  color: #fff;
+  font-weight: bold;
+}
+.goal-title {
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #333;
+  margin-bottom: 6rpx;
+}
+.goal-desc {
+  font-size: 22rpx;
+  color: #999;
+  text-align: center;
+  line-height: 1.4;
+}
+
+.type-list {
+  margin-top: 10rpx;
+}
+.type-card {
+  display: flex;
+  align-items: center;
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 28rpx 24rpx;
+  margin-bottom: 20rpx;
+  box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06);
+  border: 3rpx solid transparent;
+  transition: all 0.3s;
+}
+.type-card--selected {
+  border-color: #FFD700;
+  background: #FFF8E1;
+}
+.type-icon-wrap {
+  width: 72rpx;
+  height: 72rpx;
+  border-radius: 50%;
+  background: linear-gradient(135deg, #FFD700, #FFA500);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 20rpx;
+  flex-shrink: 0;
+}
+.type-icon {
+  font-size: 28rpx;
+  color: #fff;
+  font-weight: bold;
+}
+.type-info {
+  flex: 1;
+}
+.type-name {
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #333;
+  display: block;
+}
+.type-desc {
+  font-size: 22rpx;
+  color: #999;
+  margin-top: 4rpx;
+  display: block;
+}
+.type-radio {
+  width: 36rpx;
+  height: 36rpx;
+  border-radius: 50%;
+  border: 3rpx solid #ddd;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-shrink: 0;
+  margin-left: 16rpx;
+}
+.type-radio--checked {
+  border-color: #FFD700;
+}
+.type-radio-dot {
+  width: 20rpx;
+  height: 20rpx;
+  border-radius: 50%;
+  background: #FFD700;
+}
+
+.progress-header {
+  margin-bottom: 30rpx;
+}
+.progress-label {
+  font-size: 26rpx;
+  color: #666;
+  display: block;
+  margin-bottom: 12rpx;
+}
+.progress-bar {
+  height: 8rpx;
+  background: #eee;
+  border-radius: 4rpx;
+  overflow: hidden;
+}
+.progress-fill {
+  height: 100%;
+  background: linear-gradient(135deg, #FFD700, #FFA500);
+  border-radius: 4rpx;
+  transition: width 0.3s ease;
+}
+
+.question-card {
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 36rpx 28rpx;
+  box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06);
+  margin-bottom: 30rpx;
+}
+.q-tag {
+  display: inline-block;
+  padding: 6rpx 20rpx;
+  background: #FFF8E1;
+  color: #B8860B;
+  font-size: 20rpx;
+  border-radius: 20rpx;
+  margin-bottom: 20rpx;
+}
+.q-text {
+  font-size: 30rpx;
+  color: #333;
+  line-height: 1.6;
+  display: block;
+  margin-bottom: 30rpx;
+}
+.q-options {
+  display: flex;
+  flex-direction: column;
+  gap: 16rpx;
+}
+.q-option {
+  display: flex;
+  align-items: center;
+  padding: 24rpx 20rpx;
+  border-radius: 16rpx;
+  border: 2rpx solid #eee;
+  transition: all 0.3s;
+}
+.q-option--selected {
+  border-color: #FFD700;
+  background: #FFF8E1;
+}
+.q-option--tf {
+  justify-content: center;
+  padding: 28rpx;
+}
+.q-option-tf-text {
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #666;
+}
+.q-option--selected .q-option-tf-text {
+  color: #B8860B;
+}
+.q-radio {
+  width: 32rpx;
+  height: 32rpx;
+  border-radius: 50%;
+  border: 3rpx solid #ddd;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-shrink: 0;
+  margin-right: 16rpx;
+}
+.q-radio--checked {
+  border-color: #FFD700;
+}
+.q-radio-dot {
+  width: 18rpx;
+  height: 18rpx;
+  border-radius: 50%;
+  background: #FFD700;
+}
+.q-option-text {
+  font-size: 26rpx;
+  color: #333;
+  flex: 1;
+}
+
+.action-bar {
+  margin-top: 40rpx;
+  display: flex;
+  flex-direction: column;
+  gap: 16rpx;
+}
+.action-bar--double {
+  flex-direction: row;
+  gap: 20rpx;
+}
+.action-bar--double .btn {
+  flex: 1;
+}
+
+.btn {
+  padding: 24rpx 0;
+  border-radius: 16rpx;
+  text-align: center;
+  font-size: 28rpx;
+  font-weight: bold;
+  transition: all 0.3s;
+  box-sizing: border-box;
+}
+.btn--primary {
+  background: linear-gradient(135deg, #FFD700, #FFA500);
+  color: #fff;
+}
+.btn--primary:active {
+  opacity: 0.9;
+  transform: scale(0.98);
+}
+.btn--disabled {
+  opacity: 0.4;
+  pointer-events: none;
+}
+.btn--outline {
+  border: 3rpx solid #FFD700;
+  color: #B8860B;
+  background: #fff;
+}
+.btn--outline:active {
+  background: #FFF8E1;
+}
+.btn--ghost {
+  color: #999;
+  background: transparent;
+}
+
+.result-header {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  margin: 40rpx 0;
+}
+.score-circle {
+  width: 160rpx;
+  height: 160rpx;
+  border-radius: 50%;
+  background: linear-gradient(135deg, #FFD700, #FFA500);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  box-shadow: 0 8rpx 30rpx rgba(255,215,0,0.3);
+  margin-bottom: 20rpx;
+}
+.score-value {
+  font-size: 40rpx;
+  font-weight: bold;
+  color: #fff;
+}
+.score-label {
+  font-size: 28rpx;
+  color: #666;
+  display: block;
+}
+
+.result-dims {
+  margin: 20rpx 0 30rpx;
+}
+.dim-bar-item {
+  margin-bottom: 24rpx;
+}
+.dim-bar-header {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 8rpx;
+}
+.dim-bar-name {
+  font-size: 24rpx;
+  color: #333;
+  font-weight: bold;
+}
+.dim-bar-count {
+  font-size: 22rpx;
+  color: #999;
+}
+.dim-bar-track {
+  height: 8rpx;
+  background: #eee;
+  border-radius: 4rpx;
+  overflow: hidden;
+}
+.dim-bar-fill {
+  height: 100%;
+  background: linear-gradient(135deg, #FFD700, #FFA500);
+  border-radius: 4rpx;
+  transition: width 0.6s ease;
+}
+
+.bottom-spacer {
+  height: 120rpx;
+}
+</style>

+ 178 - 0
cfc-frontend/pages/wisdom-detail/training-hub.vue

@@ -0,0 +1,178 @@
+<template>
+  <view class="container">
+    <view class="header-bar">
+      <text class="back-btn" @click="goBack">← 返回</text>
+      <text class="header-title">认知训练中心</text>
+      <text class="header-placeholder"></text>
+    </view>
+
+    <view class="desc-section">
+      <text class="desc-text">选择训练游戏,提升你的认知能力</text>
+    </view>
+
+    <view class="game-card" @click="goGame('schulte')">
+      <view class="game-card-left">
+        <text class="game-icon">🎯</text>
+      </view>
+      <view class="game-card-center">
+        <text class="game-name">舒尔特方格</text>
+        <text class="game-desc">提升专注力和反应速度</text>
+      </view>
+      <text class="game-arrow">›</text>
+    </view>
+
+    <view class="game-card" @click="goGame('1a2b')">
+      <view class="game-card-left">
+        <text class="game-icon">🔢</text>
+      </view>
+      <view class="game-card-center">
+        <text class="game-name">猜数字</text>
+        <text class="game-desc">锻炼逻辑思维和推理能力</text>
+      </view>
+      <text class="game-arrow">›</text>
+    </view>
+
+    <view class="note-section">
+      <text class="note-text">更多训练游戏正在开发中,敬请期待</text>
+    </view>
+  </view>
+</template>
+
+<script>
+export default {
+  data: function() {
+    return {
+      childId: null
+    }
+  },
+  onLoad: function(options) {
+    if (options && options.childId) {
+      this.childId = options.childId
+    }
+  },
+  methods: {
+    goBack: function() {
+      uni.navigateBack()
+    },
+    goGame: function(game) {
+      var url = ''
+      if (game === 'schulte') {
+        url = '/pages/games/schulte'
+      } else if (game === '1a2b') {
+        url = '/pages/games/1a2b'
+      }
+      if (url) {
+        uni.navigateTo({ url: url })
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  min-height: 100vh;
+  background: linear-gradient(180deg, #FFF8E1 0%, #f5f7fa 100%);
+  padding-bottom: 60rpx;
+}
+
+.header-bar {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  padding: 80rpx 30rpx 30rpx;
+}
+
+.back-btn {
+  font-size: 30rpx;
+  color: #B8860B;
+  padding: 10rpx;
+}
+
+.header-title {
+  font-size: 36rpx;
+  font-weight: bold;
+  color: #B8860B;
+}
+
+.header-placeholder {
+  width: 60rpx;
+}
+
+.desc-section {
+  padding: 0 30rpx 30rpx;
+}
+
+.desc-text {
+  font-size: 26rpx;
+  color: #999;
+  text-align: center;
+  display: block;
+}
+
+.game-card {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  background: #fff;
+  margin: 0 30rpx 20rpx;
+  padding: 30rpx;
+  border-radius: 20rpx;
+  box-shadow: 0 4rpx 20rpx rgba(255, 215, 0, 0.12);
+  border-left: 8rpx solid #FFD700;
+}
+
+.game-card:active {
+  opacity: 0.8;
+}
+
+.game-card-left {
+  width: 80rpx;
+  height: 80rpx;
+  border-radius: 20rpx;
+  background: #FFF8E1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 20rpx;
+}
+
+.game-icon {
+  font-size: 40rpx;
+}
+
+.game-card-center {
+  flex: 1;
+}
+
+.game-name {
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #333;
+  display: block;
+  margin-bottom: 6rpx;
+}
+
+.game-desc {
+  font-size: 24rpx;
+  color: #999;
+  display: block;
+}
+
+.game-arrow {
+  font-size: 40rpx;
+  color: #ccc;
+  margin-left: 10rpx;
+}
+
+.note-section {
+  padding: 40rpx 30rpx;
+  text-align: center;
+}
+
+.note-text {
+  font-size: 24rpx;
+  color: #ccc;
+}
+</style>

+ 8 - 8
cfc-frontend/pages/wisdom/index.vue

@@ -178,9 +178,9 @@ export default {
       articles: [],
       wisdomTips: [],
       funcList: [
-        { icon: '\u{1F4CA}', label: '认知报告', needLogin: true, page: '/pages/wisdom/cognitive-report' },
-        { icon: '\u{270F}\uFE0F', label: '自己出题', needLogin: true, page: '/pages/wisdom/self-quiz' },
-        { icon: '\u{1F9E0}', label: '训练中心', needLogin: true, page: '/pages/wisdom/training-hub' },
+        { icon: '\u{1F4CA}', label: '认知报告', needLogin: true, page: '/pages/wisdom-detail/cognitive-report' },
+        { icon: '\u{270F}\uFE0F', label: '自己出题', needLogin: true, page: '/pages/wisdom-detail/self-quiz' },
+        { icon: '\u{1F9E0}', label: '训练中心', needLogin: true, page: '/pages/wisdom-detail/training-hub' },
         { icon: '\u{1F3AF}', label: '测评预约', needLogin: false, page: '/pages/assessment/apply' },
         { icon: '\u{1F4D6}', label: '阅读天地', needLogin: false, page: '/pages/mind/articles' }
       ]
@@ -390,16 +390,16 @@ export default {
     },
     scrollToSection: function(section) {},
     goMemberDetail: function(memberId) {
-      uni.navigateTo({ url: '/pages/wisdom/member-wisdom-detail?memberId=' + memberId })
+      uni.navigateTo({ url: '/pages/wisdom-detail/member-wisdom-detail?memberId=' + memberId })
     },
     goCognitiveReport: function() {
-      uni.navigateTo({ url: '/pages/wisdom/cognitive-report' })
+      uni.navigateTo({ url: '/pages/wisdom-detail/cognitive-report' })
     },
     goSelfQuiz: function() {
-      uni.navigateTo({ url: '/pages/wisdom/self-quiz' })
+      uni.navigateTo({ url: '/pages/wisdom-detail/self-quiz' })
     },
     goTrainingHub: function() {
-      uni.navigateTo({ url: '/pages/wisdom/training-hub' })
+      uni.navigateTo({ url: '/pages/wisdom-detail/training-hub' })
     },
     goAssessment: function() {
       uni.navigateTo({ url: '/pages/assessment/apply' })
@@ -452,7 +452,7 @@ export default {
       uni.navigateTo({ url: '/pages/mind/articles' })
     },
     goArticleDetail: function(article) {
-      uni.navigateTo({ url: '/pages/wisdom/article-detail?id=' + (article.id || article) })
+      uni.navigateTo({ url: '/pages/mind-detail/article-detail?id=' + (article.id || article) })
     }
   }
 }