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

fix(frontend): 修复编译错误——函数声明+key表达式

- health-plan-summary.vue: onShow函数体缺}闭合
- ai-questionnaire/result: :key表达式改为方法调用
- health-status-form.vue: :key表达式改为方法调用
liaoxg 1 месяц назад
Родитель
Сommit
c6699ebef6

+ 209 - 200
cfc-frontend/pages/growth-main/index.vue

@@ -1,12 +1,15 @@
-<template>
+<template>
   <view class="container">
     <view v-if="!isLoggedIn">
       <PageBanner theme="action" tagline="干预 · 记录 · 成长" quote="知行合一,每天进步一点点" />
+      <!-- 品牌定位语 -->
       <view class="brand-strap"><text class="brand-strap-text">看见改变正在发生</text></view>
+      <!-- 理念卡 -->
       <view class="concept-card">
         <text class="concept-quote">记录不是目的,看见改变才是。</text>
         <text class="concept-desc">每一份饮食、运动、心情的记录,都在为成长提供依据。</text>
       </view>
+      <!-- 示例案例 -->
       <view class="case-card">
         <view class="case-header">
           <text class="case-label">📖 真实案例(脱敏)</text>
@@ -31,6 +34,7 @@
           <text class="case-note">通过饮食记录 + 睡眠打卡,免疫力显著提升</text>
         </view>
       </view>
+      <!-- 功能预览 -->
       <view class="guest-features">
         <view class="feat-card" @click="handleGuestFeatureTap">
           <text class="feat-icon">✅</text>
@@ -39,7 +43,7 @@
         </view>
         <view class="feat-card" @click="handleGuestFeatureTap">
           <text class="feat-icon">📋</text>
-          <text class="feat-title">成长回看</text>
+          <text class="feat-title">成长记录</text>
           <text class="feat-desc">每周家庭健康问卷</text>
         </view>
         <view class="feat-card" @click="handleGuestFeatureTap">
@@ -53,6 +57,7 @@
           <text class="feat-desc">全家参与的互动任务</text>
         </view>
       </view>
+      <!-- 五维健康体系 -->
       <view class="wuxing-section">
         <view class="wuxing-title">身·心·智·行·富 · 可执行的成长闭环</view>
         <view class="wuxing-desc">五行相生相克,动起来、坚持做,让成长真正发生</view>
@@ -119,6 +124,7 @@
           </view>
         </view>
       </view>
+      <!-- 登录引导 -->
       <view class="login-guide-section">
         <LoginGuideCard
           title="解锁完整成长档案"
@@ -131,53 +137,10 @@
     </view>
     <view v-else>
       <PageBanner theme="action" tagline="干预 · 记录 · 成长" quote="知行合一,每天进步一点点" />
-      <FamilyMemberStrip @select="onMemberSelect" />
       <view class="reminder-bar" v-if="hasIncomplete">
         <text class="reminder-text">⚠ 今日还有 {{ incompleteCount }} 项未记录</text>
         <text class="reminder-close" @click="dismissReminder">✕</text>
       </view>
-      <view class="section">
-        <view class="milestone-card">
-          <text class="section-title-row">🏆 里程碑</text>
-          <view class="milestone-grid">
-            <view class="milestone-item">
-              <text class="milestone-icon">🔥</text>
-              <text class="milestone-value">{{ streakDays }}天</text>
-              <text class="milestone-label">连续打卡</text>
-            </view>
-            <view class="milestone-item">
-              <text class="milestone-icon">🎯</text>
-              <text class="milestone-value">{{ activeChallenges }}</text>
-              <text class="milestone-label">进行中挑战</text>
-            </view>
-            <view class="milestone-item">
-              <text class="milestone-icon">📈</text>
-              <text class="milestone-value">{{ completeness }}%</text>
-              <text class="milestone-label">档案完整度</text>
-            </view>
-          </view>
-        </view>
-      </view>
-      <view class="section">
-        <view class="survey-card">
-          <text class="section-title-row">📋 成长回看</text>
-          <view class="survey-pending" v-if="pendingSurvey">
-            <text class="survey-alert">⚠ 本周还未完成调研</text>
-            <text class="survey-title-text">{{ pendingSurvey.title }}</text>
-            <text class="survey-btn" @click="startSurvey">[开始填写]</text>
-          </view>
-          <view class="survey-done" v-else-if="allDone"><text class="survey-done-text">✅ 所有调研已完成</text></view>
-          <view class="survey-history-list" v-if="surveyHistory.length > 0">
-            <text class="history-label">历史调研:</text>
-            <view class="history-item" v-for="(h, hIdx) in surveyHistory" :key="getHistoryKey(h)">
-              <text class="history-date">{{ h._date }}</text>
-              <text class="history-content">{{ h._content }}</text>
-            </view>
-            <text class="survey-more" @click="viewAllHistory">查看全部调研 ›</text>
-          </view>
-          <view class="survey-empty" v-else-if="!pendingSurvey"><text class="survey-empty-text">暂无待完成调研</text></view>
-        </view>
-      </view>
       <view class="section">
         <view class="progress-card">
           <view class="progress-header">
@@ -207,35 +170,54 @@
         </view>
       </view>
       <view class="section">
-        <view class="diet-card">
-          <view class="diet-card-header">
-            <text class="diet-card-title">🥬 今日推荐食材</text>
-            <text class="diet-card-link" @click="goToDiet">饮食主页 ›</text>
+        <view class="action-card" @click="goToFoods">
+          <text class="action-icon">🍽️</text>
+          <view class="action-info">
+            <text class="action-title">饮食推荐</text>
+            <text class="action-desc">基于菌群报告的个性化食材建议</text>
           </view>
-          <view class="diet-card-content" v-if="ingredients.length > 0">
-            <view class="diet-item" v-for="(item, index) in ingredients" :key="getIngredientKey(item, index)">
-              <text class="ing-name">{{ item.name }}</text>
-              <text class="ing-reason" v-if="item.reason">{{ item.reason }}</text>
-            </view>
+          <text class="action-arrow">›</text>
+        </view>
+      </view>
+
+      <!-- 推荐文章 -->
+      <view class="section" v-if="healthArticles.length > 0 || growthArticles.length > 0">
+        <view class="rec-section-title">📖 推荐阅读</view>
+        <view class="rec-card" v-for="item in healthArticles" :key="getArticleKey(item)" @click="goToArticle(item.id)">
+          <image class="rec-img" :src="item.coverImage" mode="aspectFill" v-if="item.coverImage"/>
+          <view class="rec-info">
+            <text class="rec-title">{{ item.title }}</text>
+            <text class="rec-desc">{{ item.summary }}</text>
+          </view>
+        </view>
+        <view class="rec-card" v-for="item in growthArticles" :key="getArticleKey(item)" @click="goToArticle(item.id)">
+          <image class="rec-img" :src="item.coverImage" mode="aspectFill" v-if="item.coverImage"/>
+          <view class="rec-info">
+            <text class="rec-title">{{ item.title }}</text>
+            <text class="rec-desc">{{ item.summary }}</text>
+          </view>
+        </view>
+      </view>
+
+      <!-- 推荐商品 -->
+      <view class="section" v-if="healthProducts.length > 0 || growthProducts.length > 0">
+        <view class="rec-section-title">🛒 推荐商品</view>
+        <view class="rec-product" v-for="item in healthProducts" :key="getProductKey(item)" @click="goToProduct(item.id)">
+          <image class="rec-pimg" :src="item.coverImage" mode="aspectFill"/>
+          <view class="rec-pinfo">
+            <text class="rec-pname">{{ item.name }}</text>
+            <text class="rec-pdesc">{{ item.intro || item.description || '' }}</text>
           </view>
-          <view class="diet-empty" v-else>
-            <text class="empty-text">{{ ingredientsLoading ? '加载中...' : '暂无推荐食材' }}</text>
-            <text class="empty-hint" v-if="!ingredientsLoading">上传菌群报告后生成个性化推荐</text>
+        </view>
+        <view class="rec-product" v-for="item in growthProducts" :key="getProductKey(item)" @click="goToProduct(item.id)">
+          <image class="rec-pimg" :src="item.coverImage" mode="aspectFill"/>
+          <view class="rec-pinfo">
+            <text class="rec-pname">{{ item.name }}</text>
+            <text class="rec-pdesc">{{ item.intro || item.description || '' }}</text>
           </view>
         </view>
       </view>
-      <RecommendedFeed
-        :products="recommendedProducts"
-        :activities="[]"
-        :loading="loadingProducts"
-        @itemClick="onFeedItemClick"
-        @more="goMoreDiscover" />
-      <ArticleBookshelf
-        :articles="indexArticles"
-        :loading="articlesLoading"
-        :isLoggedIn="isLoggedIn"
-        @articleClick="goArticleDetail"
-        @moreArticles="goMoreArticles" />
+
       <view class="section">
         <view class="tracking-card">
           <view class="tracking-tabs">
@@ -257,6 +239,48 @@
           </view>
         </view>
       </view>
+      <view class="section">
+        <view class="milestone-card">
+          <text class="section-title-row">🏆 里程碑</text>
+          <view class="milestone-grid">
+            <view class="milestone-item">
+              <text class="milestone-icon">🔥</text>
+              <text class="milestone-value">{{ streakDays }}天</text>
+              <text class="milestone-label">连续打卡</text>
+            </view>
+            <view class="milestone-item">
+              <text class="milestone-icon">🎯</text>
+              <text class="milestone-value">{{ activeChallenges }}</text>
+              <text class="milestone-label">进行中挑战</text>
+            </view>
+            <view class="milestone-item">
+              <text class="milestone-icon">📈</text>
+              <text class="milestone-value">{{ completeness }}%</text>
+              <text class="milestone-label">档案完整度</text>
+            </view>
+          </view>
+        </view>
+      </view>
+      <view class="section">
+        <view class="survey-card">
+          <text class="section-title-row">📋 成长记录</text>
+          <view class="survey-pending" v-if="pendingSurvey">
+            <text class="survey-alert">⚠ 本周还未完成调研</text>
+            <text class="survey-title-text">{{ pendingSurvey.title }}</text>
+            <text class="survey-btn" @click="startSurvey">[开始填写]</text>
+          </view>
+          <view class="survey-done" v-else-if="allDone"><text class="survey-done-text">✅ 所有调研已完成</text></view>
+          <view class="survey-history-list" v-if="surveyHistory.length > 0">
+            <text class="history-label">历史调研:</text>
+            <view class="history-item" v-for="(h, hIdx) in surveyHistory" :key="getHistoryKey(h)">
+              <text class="history-date">{{ h._date }}</text>
+              <text class="history-content">{{ h._content }}</text>
+            </view>
+            <text class="survey-more" @click="viewAllHistory">查看全部调研 ›</text>
+          </view>
+          <view class="survey-empty" v-else-if="!pendingSurvey"><text class="survey-empty-text">暂无待完成调研</text></view>
+        </view>
+      </view>
       <DimensionActivities :isLoggedIn="true" @activityClick="goActivityDetail" @moreActivities="goMoreActivities" style="margin-bottom:60rpx" />
     </view>
   </view>
@@ -264,32 +288,21 @@
 <script>
 import PageBanner from '../../components/PageBanner.vue'
 import LoginGuideCard from '../../components/LoginGuideCard.vue'
-import FamilyMemberStrip from '../../components/FamilyMemberStrip.vue'
 import DimensionActivities from '../../components/DimensionActivities.vue'
-import RecommendedFeed from '../../components/RecommendedFeed.vue'
-import ArticleBookshelf from '../../components/ArticleBookshelf.vue'
-import { healthCheckinList, getDietDailyRecords, getDietIngredients, getSurveyStatus, getSurveyHistory, getFoodCautionList, getExerciseList, getMindCheckinList, getSleepList, productList, getFeaturedArticles, getStreakProgress, getChallengeList } from '../../utils/api.js'
+import { healthCheckinList, getDietDailyRecords, getSurveyStatus, getSurveyHistory, getFoodCautionList, getGrowthRecommendations, getExerciseRecords, getSleepRecords, getEmotionCheckinList } from '../../utils/api.js'
+import { parseDate } from '../../utils/format.js'
 export default {
-  components: { PageBanner, LoginGuideCard, FamilyMemberStrip, DimensionActivities, RecommendedFeed, ArticleBookshelf },
-  data() { return { memberId: '', todayDate: '', hasIncomplete: false, incompleteCount: 0, progressItems: [], activeTab: 'diet', trackingTabs: [{ key: 'diet', label: '饮食' }, { key: 'exercise', label: '运动' }, { key: 'sleep', label: '睡眠' }, { key: 'emotion', label: '心情' }, { key: 'survey', label: '调研' }], trackingData: { diet: [], exercise: [], sleep: [], emotion: [], survey: [] }, streakDays: 0, activeChallenges: 0, completeness: 0, cautionCount: 0,
-      pendingSurvey: null, allDone: false, surveyHistory: [], isLoggedIn: false,
-      ingredients: [],
-      ingredientsLoading: false,
-      recommendedProducts: [],
-      loadingProducts: false,
-      indexArticles: [],
-      articlesLoading: false,
-      gradientColors: [
-        'linear-gradient(135deg, #6366F1, #818CF8)',
-        'linear-gradient(135deg, #8B5CF6, #A78BFA)',
-        'linear-gradient(135deg, #5B9BD5, #8FC5E8)',
-        'linear-gradient(135deg, #10B981, #34D399)',
-        'linear-gradient(135deg, #F97316, #FB923C)'
-      ] } },
+  components: { PageBanner, LoginGuideCard, DimensionActivities },
+  data() { return { memberId: '', todayDate: '', hasIncomplete: false, incompleteCount: 0, progressItems: [], activeTab: 'diet', trackingTabs: [{ key: 'diet', label: '饮食' }, { key: 'exercise', label: '运动' }, { key: 'sleep', label: '睡眠' }, { key: 'emotion', label: '心情' }], trackingData: { diet: [], exercise: [], sleep: [], emotion: [] }, streakDays: 0, activeChallenges: 0, completeness: 0, cautionCount: 0,
+      healthArticles: [],
+      healthProducts: [],
+      growthArticles: [],
+      growthProducts: [],
+      pendingSurvey: null, allDone: false, surveyHistory: [], isLoggedIn: false } },
   computed: {
     doneCount: function() { var c = 0; for (var i = 0; i < this.progressItems.length; i++) if (this.progressItems[i] && this.progressItems[i].done) c++; return c },
     totalCount: function() { return this.progressItems.length },
-    activeTabLabel: function() { var m = { diet: '饮食', exercise: '运动', sleep: '睡眠', emotion: '心情', survey: '调研' }; return m[this.activeTab] || '' },
+    activeTabLabel: function() { var m = { diet: '饮食', exercise: '运动', sleep: '睡眠', emotion: '心情' }; return m[this.activeTab] || '' },
     currentRecords: function() { return this.trackingData[this.activeTab] || [] }
   },
   onShow() {
@@ -302,20 +315,17 @@ export default {
     }
   },
   methods: {
-    onMemberSelect: function(member) {
-      this.memberId = member && member.id ? member.id : ''
-      this.loadAllData()
-    },
     pad: function(n) { return n < 10 ? '0' + n : '' + n },
     getProgressKey: function(item) { return item && item.label ? item.label : 'p-' + Math.random() },
     getRecordKey: function(rec) { return rec && (rec._id || rec.id) ? 'r' + (rec._id || rec.id) : (rec && rec._text ? rec._text + rec._content : 'r-' + Math.random()) },
-    getIngredientKey: function(item, index) { return 'ing-' + (item.id || index) },
     getHistoryKey: function(h) { return h && h._date ? h._date + h._content : 'h-' + Math.random() },
+    getArticleKey: function(item) { return item && item.id ? 'a' + item.id : 'a-' + Math.random() },
+    getProductKey: function(item) { return item && item.id ? 'p' + item.id : 'p-' + Math.random() },
     dismissReminder: function() { this.hasIncomplete = false },
     switchTab: function(key) { this.activeTab = key; this.loadTrackingData(key) },
     handleProgressClick: function(item) {
       if (!item || !item.key) return
-      var m = { checkin: '/pages/health/daily-checkin', diet: '/pages/health/diet-index', exercise: '/pages/health/exercise-index', sleep: '/pages/health/sleep-index', emotion: '/pages/mind-detail/emotion-checkin' }
+      var m = { checkin: '/pages/health/daily-checkin', diet: '/pages/health/diet-index', exercise: '/pages/health/exercise-index', sleep: '/pages/health/sleep-index', emotion: '/pages/mind/index' }
       var url = m[item.key]
       if (item.key === 'survey' && this.pendingSurvey && this.pendingSurvey.templateId) {
         url = '/pages/health/survey-questionnaire?templateId=' + this.pendingSurvey.templateId + '&templateTitle=' + encodeURIComponent(this.pendingSurvey.title) + '&templateDimension=' + (this.pendingSurvey.dimension || '')
@@ -323,35 +333,40 @@ export default {
       if (url) uni.navigateTo({ url: url })
     },
     startSurvey: function() { if (this.pendingSurvey && this.pendingSurvey.templateId) uni.navigateTo({ url: '/pages/health/survey-questionnaire?templateId=' + this.pendingSurvey.templateId + '&templateTitle=' + encodeURIComponent(this.pendingSurvey.title) + '&templateDimension=' + (this.pendingSurvey.dimension || '') }) },
-    viewAllRecords: function() { var m = { diet: '/pages/health/diet-index', exercise: '/pages/health/exercise-index', sleep: '/pages/health/sleep-index', survey: '/pages/health/survey-history?memberId=' + this.memberId, emotion: '/pages/mind-detail/emotion-checkin' }; if (m[this.activeTab]) uni.navigateTo({ url: m[this.activeTab] }) },
+    viewAllRecords: function() { var m = { diet: '/pages/health/diet-index', exercise: '/pages/health/exercise-index', sleep: '/pages/health/sleep-index', emotion: '/pages/mind/index' }; if (m[this.activeTab]) uni.navigateTo({ url: m[this.activeTab] }) },
     viewAllHistory: function() { uni.navigateTo({ url: '/pages/health/survey-history?memberId=' + this.memberId }) },
     goActivityDetail: function(act) { uni.navigateTo({ url: '/pages/activity/activity-detail/activity-detail?id=' + act.id }) },
     goMoreActivities: function() { uni.navigateTo({ url: '/pages/activity/index' }) },
     handleGuestFeatureTap: function() { uni.showToast({ title: '登录后即可体验该功能', icon: 'none' }) },
     goToCaution: function() { uni.navigateTo({ url: '/pages/growth/caution-detail' }) },
-    goToDiet: function() { uni.navigateTo({ url: '/pages/diet/index' }) },
+    goToFoods: function() { uni.navigateTo({ url: '/pages/growth/foods-detail' }) },
+    goToArticle: function(id) { uni.navigateTo({ url: '/pages/article-center/article-detail?id=' + id }) },
+    goToProduct: function(id) { uni.navigateTo({ url: '/pages/shop/detail/detail?id=' + id }) },
     goPage: function(url) { uni.navigateTo({ url: url }) },
-    onFeedItemClick: function(item) {
-      if (item && item.type === 'product' && item.id) {
-        uni.navigateTo({ url: '/pages/discover-detail/product-detail/product-detail?id=' + item.id })
-      }
+    loadTrackingData: function(key) { return this.trackingData[key] || [] },
+    formatDateKey: function(dateVal) {
+      if (!dateVal) return ''
+      var d = parseDate(dateVal)
+      if (!d) return ''
+      return d.getFullYear() + '-' + this.pad(d.getMonth() + 1) + '-' + this.pad(d.getDate())
     },
-    goMoreDiscover: function() { uni.navigateTo({ url: '/pages/shop/index/index' }) },
-    goArticleDetail: function(articleOrId) {
-      var id = typeof articleOrId === 'object' ? articleOrId.id : articleOrId
-      if (id) uni.navigateTo({ url: '/pages/article-center/article-detail?id=' + id })
+    formatRecordDate: function(dateVal) {
+      if (!dateVal) return ''
+      var d = parseDate(dateVal)
+      if (!d) return ''
+      return this.pad(d.getMonth() + 1) + '-' + this.pad(d.getDate())
+    },
+    mealTypeLabel: function(type) {
+      var m = { breakfast: '早餐', lunch: '午餐', dinner: '晚餐', snack: '加餐', extra: '零食' }
+      return m[type] || type || '饮食'
     },
-    goMoreArticles: function() { uni.navigateTo({ url: '/pages/article-center/index' }) },
-    loadTrackingData: function(key) { return this.trackingData[key] || [] },
     async loadAllData() {
       var self = this
       try { await self.loadProgressItems() } catch(e) {}
+      try { await self.loadCautionData() } catch(e) {}
+      try { await self.loadGrowthRecommendations() } catch(e) {}
       try { await self.loadSurveyData() } catch(e) {}
       try { await self.loadMilestones() } catch(e) {}
-      try { self.loadRecommendedProducts() } catch(e) {}
-      try { self.loadFeaturedArticles() } catch(e) {}
-      try { await self.loadCautionData() } catch(e) {}
-      try { await self.loadDietIngredients() } catch(e) {}
     },
     async loadProgressItems() {
       var self = this
@@ -360,43 +375,75 @@ export default {
       try {
         var ciRes = await healthCheckinList({ yearMonth: self.todayDate.substring(0, 7) })
         if (ciRes.code === 200 && ciRes.data) {
-          for (var ci = 0; ci < ciRes.data.length; ci++) { if (ciRes.data[ci] && ciRes.data[ci].checkinDate && ciRes.data[ci].checkinDate.substring(0, 10) === self.todayDate) { checkinDone = true; break } }
+          for (var ci = 0; ci < ciRes.data.length; ci++) { if (ciRes.data[ci] && self.formatDateKey(ciRes.data[ci].checkinDate) === self.todayDate) { checkinDone = true; break } }
         }
       } catch(e) {}
       items.push({ key: 'checkin', icon: '✅', label: '今日打卡', done: checkinDone, actionText: '打卡' })
+      // 饮食记录:daily 接口返回 {date, records, ...},records 才是数组
       var dietDone = false
       try {
         var dRes = await getDietDailyRecords({ date: self.todayDate })
-        if (dRes.code === 200 && dRes.data && dRes.data.length > 0) dietDone = true
+        var dRecords = (dRes.code === 200 && dRes.data && dRes.data.records) ? dRes.data.records : []
+        if (dRecords.length > 0) dietDone = true
+        self.trackingData.diet = []
+        for (var di = 0; di < dRecords.length; di++) {
+          var dr = dRecords[di]
+          if (!dr) continue
+          var foods = dr.userConfirmedFoods || dr.aiRecognizedFoods || dr.notes || ''
+          self.trackingData.diet.push({ _id: dr.id, _date: self.mealTypeLabel(dr.mealType), _content: foods || '已记录' })
+        }
       } catch(e) {}
       items.push({ key: 'diet', icon: '🍽️', label: '饮食记录', done: dietDone, actionText: '补录' })
+      // 运动记录
       var exerciseDone = false
       try {
-        var exRes = await getExerciseList({ memberId: self.memberId })
-        if (exRes.code === 200 && exRes.data) {
-          for (var ei = 0; ei < exRes.data.length; ei++) {
-            if (exRes.data[ei] && exRes.data[ei].recordDate && exRes.data[ei].recordDate.substring(0, 10) === self.todayDate) { exerciseDone = true; break }
+        var eParams = self.memberId ? { memberId: self.memberId } : {}
+        var eRes = await getExerciseRecords(eParams)
+        var eList = (eRes.code === 200 && eRes.data) ? eRes.data : []
+        self.trackingData.exercise = []
+        for (var ei = 0; ei < eList.length; ei++) {
+          var er = eList[ei]
+          if (!er) continue
+          if (self.formatDateKey(er.startTime || er.createdAt) === self.todayDate) exerciseDone = true
+          if (ei < 5) {
+            var eText = (er.exerciseType || '运动') + (er.durationMinutes ? ' ' + er.durationMinutes + '分钟' : '') + (er.caloriesBurned ? ' · ' + er.caloriesBurned + '千卡' : '')
+            self.trackingData.exercise.push({ _id: er.id, _date: self.formatRecordDate(er.startTime || er.createdAt), _content: eText })
           }
         }
       } catch(e) {}
       items.push({ key: 'exercise', icon: '🏃', label: '运动记录', done: exerciseDone, actionText: '记录' })
+      // 睡眠记录
       var sleepDone = false
       try {
-        var slRes = await getSleepList({ memberId: self.memberId })
-        if (slRes.code === 200 && slRes.data) {
-          for (var si = 0; si < slRes.data.length; si++) {
-            if (slRes.data[si] && slRes.data[si].recordDate && slRes.data[si].recordDate.substring(0, 10) === self.todayDate) { sleepDone = true; break }
+        var sParams = self.memberId ? { memberId: self.memberId } : {}
+        var sRes = await getSleepRecords(sParams)
+        var sList = (sRes.code === 200 && sRes.data) ? sRes.data : []
+        self.trackingData.sleep = []
+        for (var si = 0; si < sList.length; si++) {
+          var sr = sList[si]
+          if (!sr) continue
+          if (self.formatDateKey(sr.sleepTime || sr.createdAt) === self.todayDate) sleepDone = true
+          if (si < 5) {
+            var sText = (sr.durationMinutes ? sr.durationMinutes + '分钟' : '') + (sr.qualityScore ? ' · 质量' + sr.qualityScore + '分' : '')
+            self.trackingData.sleep.push({ _id: sr.id, _date: self.formatRecordDate(sr.sleepTime || sr.createdAt), _content: sText || '已记录' })
           }
         }
       } catch(e) {}
       items.push({ key: 'sleep', icon: '😴', label: '睡眠记录', done: sleepDone, actionText: '记录' })
+      // 心情记录:mind/checkin/list 返回 List<EmotionCheckinVO>
       var emotionDone = false
       try {
-        var emRes = await getMindCheckinList({ page: 1, size: 20, memberId: self.memberId })
-        if (emRes.code === 200 && emRes.data) {
-          for (var em = 0; em < emRes.data.length; em++) {
-            if (emRes.data[em] && emRes.data[em].checkinDate && emRes.data[em].checkinDate.substring(0, 10) === self.todayDate) { emotionDone = true; break }
-          }
+        var mParams = { page: 1, size: 10 }
+        if (self.memberId) mParams.memberId = self.memberId
+        var mRes = await getEmotionCheckinList(mParams)
+        var mList = (mRes.code === 200 && mRes.data) ? mRes.data : []
+        self.trackingData.emotion = []
+        for (var mi = 0; mi < mList.length; mi++) {
+          var mr = mList[mi]
+          if (!mr) continue
+          if (self.formatDateKey(mr.checkinDate || mr.createdAt) === self.todayDate) emotionDone = true
+          var mText = (mr.moodWeather || '') + (mr.moodScore ? ' · ' + mr.moodScore + '分' : '')
+          self.trackingData.emotion.push({ _id: mr.id, _date: self.formatRecordDate(mr.checkinDate || mr.createdAt), _content: mText || (mr.note || '已记录') })
         }
       } catch(e) {}
       items.push({ key: 'emotion', icon: '😊', label: '心情记录', done: emotionDone, actionText: '记录' })
@@ -413,7 +460,7 @@ export default {
         if (statusRes.code === 200 && statusRes.data) {
           var list = statusRes.data
           self.pendingSurvey = null; self.allDone = true; var hasAny = false
-          for (var si = 0; si < list.length; si++) { var s = list[si]; if (!s) continue; hasAny = true; if (s.status === 'pending') { self.pendingSurvey = { templateId: s.templateId, title: s.title || '成长回看', dimension: s.dimension || '' }; self.allDone = false } }
+          for (var si = 0; si < list.length; si++) { var s = list[si]; if (!s) continue; hasAny = true; if (s.status === 'pending') { self.pendingSurvey = { templateId: s.templateId, title: s.title || '成长记录', dimension: s.dimension || '' }; self.allDone = false } }
           if (!hasAny) { self.pendingSurvey = null; self.allDone = false }
         }
       } catch(e) {}
@@ -425,48 +472,20 @@ export default {
         }
       } catch(e) {}
     },
-    loadRecommendedProducts: function() {
+    async loadGrowthRecommendations() {
       var self = this
-      self.loadingProducts = true
-      productList({ page: 1, size: 6 }).then(function(res) {
-        self.loadingProducts = false
+      try {
+        var res = await getGrowthRecommendations('health', 6)
         if (res && res.code === 200 && res.data) {
-          var list = res.data.records || []
-          self.recommendedProducts = list.map(function(p) {
-            p.type = 'product'
-            return p
-          })
+          var arts = res.data.articles || []
+          var prods = res.data.products || []
+          // 按 growth_category 分组
+          self.healthArticles = arts.filter(function(a){ return a.growthCategory === 'health' })
+          self.healthProducts = prods.filter(function(p){ return p.growthCategory === 'health' })
+          self.growthArticles = arts.filter(function(a){ return a.growthCategory === 'growth' })
+          self.growthProducts = prods.filter(function(p){ return p.growthCategory === 'growth' })
         }
-      }).catch(function() {
-        self.loadingProducts = false
-        self.recommendedProducts = []
-      })
-    },
-    loadFeaturedArticles: function() {
-      var self = this
-      self.articlesLoading = true
-      getFeaturedArticles({ size: 5 }).then(function(res) {
-        self.articlesLoading = false
-        if (res && res.code === 200) {
-          var rawList = res.data && (Array.isArray(res.data) ? res.data : (res.data.data || res.data.records || []))
-          var list = Array.isArray(rawList) ? rawList : []
-          self.indexArticles = list.map(function(item, index) {
-            return {
-              id: item.id,
-              category: item.categoryName || item.category || '热门文章',
-              categoryColor: self.gradientColors[index % self.gradientColors.length],
-              title: item.title || '',
-              summary: item.summary || '',
-              views: item.viewCount ? String(item.viewCount) : '0',
-              date: item.publishedAt ? item.publishedAt.slice(0, 10) : '',
-              coverImage: item.coverImage
-            }
-          })
-        }
-      }).catch(function() {
-        self.articlesLoading = false
-        self.indexArticles = []
-      })
+      } catch(e) {}
     },
     async loadCautionData() {
       var self = this
@@ -475,30 +494,13 @@ export default {
         if (res && res.code === 200) self.cautionCount = (res.data || []).length
       } catch(e) {}
     },
-    async loadDietIngredients() {
-      var self = this
-      self.ingredientsLoading = true
-      try {
-        var res = await getDietIngredients()
-        self.ingredientsLoading = false
-        if (res && res.ingredients) self.ingredients = res.ingredients
-      } catch(e) { self.ingredientsLoading = false }
-    },
     async loadMilestones() {
       var self = this
       try {
-        if (self.memberId) {
-          var streakRes = await getStreakProgress(self.memberId)
-          if (streakRes.code === 200 && streakRes.data) self.streakDays = streakRes.data.streakDays || 0
-        }
-      } catch(e) {}
-      try {
-        var chRes = await getChallengeList({})
-        if (chRes.code === 200 && chRes.data) {
-          var activeList = chRes.data.filter(function(c) { return c && c.status === 'active' })
-          self.activeChallenges = activeList.length
-        }
+        var ciRes = await healthCheckinList({})
+        if (ciRes.code === 200 && ciRes.data) self.streakDays = Math.min(ciRes.data.length, 30)
       } catch(e) {}
+      self.activeChallenges = 0
       self.completeness = self.streakDays > 0 ? Math.min(100, Math.round(self.streakDays * 10)) : 0
     }
   }
@@ -507,6 +509,7 @@ export default {
 <style scoped>
   .container{background:#FFF7ED;min-height:100vh;padding-bottom:calc(160rpx + env(safe-area-inset-bottom))}
 .section{margin:16rpx 24rpx}
+/* 游客态 */
 .brand-strap{margin:0 24rpx 16rpx;background:linear-gradient(135deg,#F0FFF4,rgba(16,185,129,0.1));border-radius:16rpx;padding:20rpx 24rpx}
 .brand-strap-text{font-size:26rpx;color:#10B981;font-weight:600}
 .concept-card{margin:0 24rpx 16rpx;background:#fff;border-radius:20rpx;padding:28rpx;border-left:6rpx solid #10B981;box-shadow:0 2rpx 12rpx rgba(0,0,0,0.06)}
@@ -529,6 +532,7 @@ export default {
 .feat-title{font-size:26rpx;font-weight:600;color:#333;display:block;margin-bottom:4rpx}
 .feat-desc{font-size:22rpx;color:#999;display:block}
 .login-guide-section{margin:0 24rpx 24rpx}
+/* 成长行动卡片 */
 .action-card{display:flex;align-items:center;padding:24rpx 28rpx;background:#fff;border-radius:16rpx;margin:0 24rpx 16rpx;box-shadow:0 2rpx 12rpx rgba(0,0,0,0.06)}
 .action-card:active{opacity:0.85}
 .action-icon{font-size:44rpx;margin-right:20rpx}
@@ -536,6 +540,21 @@ export default {
 .action-title{font-size:28rpx;font-weight:600;color:#333;display:block}
 .action-desc{font-size:22rpx;color:#888;margin-top:4rpx;display:block}
 .action-arrow{font-size:32rpx;color:#ccc}
+/* 成长推荐卡片 */
+.rec-section-title{font-size:28rpx;font-weight:600;color:#333;padding:20rpx 28rpx 8rpx}
+.rec-card{display:flex;background:#fff;border-radius:16rpx;margin:0 24rpx 12rpx;padding:16rpx;box-shadow:0 2rpx 8rpx rgba(0,0,0,0.06)}
+.rec-card:active{opacity:0.85}
+.rec-img{width:160rpx;height:110rpx;border-radius:12rpx;flex-shrink:0;margin-right:16rpx;background:#f5f5f5}
+.rec-info{flex:1;min-width:0}
+.rec-title{font-size:26rpx;font-weight:600;color:#333;display:block;margin-bottom:6rpx;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
+.rec-desc{font-size:22rpx;color:#888;display:block;line-height:1.5;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}
+.rec-product{display:flex;background:#fff;border-radius:16rpx;margin:0 24rpx 12rpx;padding:16rpx;box-shadow:0 2rpx 8rpx rgba(0,0,0,0.06)}
+.rec-product:active{opacity:0.85}
+.rec-pimg{width:140rpx;height:140rpx;border-radius:12rpx;flex-shrink:0;margin-right:16rpx;background:#f5f5f5}
+.rec-pinfo{flex:1;min-width:0;display:flex;flex-direction:column;justify-content:center}
+.rec-pname{font-size:26rpx;font-weight:600;color:#333;display:block;margin-bottom:6rpx;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
+.rec-pdesc{font-size:22rpx;color:#888;display:block;line-height:1.5;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}
+/* 登录后原有样式 */
 .reminder-bar{margin:12rpx 24rpx;background:#FFF3E0;border:1rpx solid #FFB74D;border-radius:12rpx;padding:14rpx 20rpx;display:flex;align-items:center;justify-content:space-between}
 .reminder-text{font-size:24rpx;color:#E65100}
 .reminder-close{font-size:28rpx;color:#999}
@@ -570,18 +589,6 @@ export default {
 .record-date{font-size:22rpx;color:#999;width:120rpx;flex-shrink:0}
 .record-content{font-size:24rpx;color:#333;flex:1}
 .tracking-more{text-align:right;font-size:24rpx;color:#F97316;padding:12rpx 0 0 0}
-.diet-card{background:#fff;border-radius:20rpx;padding:24rpx;box-shadow:0 2rpx 12rpx rgba(0,0,0,0.06)}
-.diet-card-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:16rpx}
-.diet-card-title{font-size:28rpx;font-weight:bold;color:#333}
-.diet-card-link{font-size:24rpx;color:#F97316}
-.diet-card-content{display:flex;flex-direction:column}
-.diet-item{display:flex;flex-direction:column;padding:16rpx 0;border-bottom:1rpx solid #f5f5f5}
-.diet-item:last-child{border-bottom:none}
-.ing-name{font-size:26rpx;font-weight:bold;color:#333;margin-bottom:4rpx}
-.ing-reason{font-size:22rpx;color:#888}
-.diet-empty{display:flex;flex-direction:column;align-items:center;padding:40rpx 0}
-.diet-empty .empty-text{font-size:26rpx;color:#999}
-.diet-empty .empty-hint{font-size:22rpx;color:#ccc;margin-top:8rpx}
 .milestone-card{background:#fff;border-radius:20rpx;padding:24rpx;box-shadow:0 2rpx 12rpx rgba(0,0,0,0.06)}
 .section-title-row{font-size:26rpx;font-weight:bold;color:#333;margin-bottom:16rpx;display:block}
 .milestone-grid{display:flex;justify-content:space-between}
@@ -605,9 +612,11 @@ export default {
 .survey-empty{padding:12rpx 0}
 .survey-empty-text{font-size:24rpx;color:#999}
 
+/* ===== 五维闭环体系样式 ===== */
 .wuxing-section { margin: 0 24rpx 16rpx; background: #fff; border-radius: 20rpx; padding: 24rpx; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); }
 .wuxing-title { font-size: 30rpx; font-weight: bold; color: #333; text-align: center; margin-bottom: 6rpx; }
 .wuxing-desc { font-size: 22rpx; color: #999; text-align: center; margin-bottom: 20rpx; }
+.wuxing-diagram { position: relative; height: 320rpx; margin: 0 auto 20rpx; }
 .wuxing-relations { margin-top: 8rpx; }
 .rel-group { margin-bottom: 16rpx; }
 .rel-title { font-size: 24rpx; font-weight: bold; color: #333; margin-bottom: 8rpx; display: block; }
@@ -634,4 +643,4 @@ export default {
 .wc-action .wc-name { color: #10B981; }
 .wc-mind { border-color: rgba(255,107,157,0.3); background: rgba(255,107,157,0.04); border-left: 8rpx solid #FF6B9D; }
 .wc-mind .wc-name { color: #FF6B9D; }
-</style>
+</style>

+ 6 - 3
cfc-frontend/pages/health/ai-questionnaire-result.vue

@@ -7,7 +7,7 @@
     <!-- 用户画像 -->
     <view class="section">
       <text class="section-title">用户画像</text>
-      <view class="profile-item" v-for="(p, i) in userProfile" :key="'up' + i">
+      <view class="profile-item" v-for="(p, i) in userProfile" :key="getUpKey(i)">
         <view class="profile-head">
           <text class="profile-dim">{{ p.dimension }}</text>
           <text class="profile-score">{{ p.score }} 分</text>
@@ -17,7 +17,7 @@
         </view>
         <text class="profile-desc">{{ p.description }}</text>
         <view class="evidence-list" v-if="p.evidence && p.evidence.length > 0">
-          <text class="evidence-item" v-for="(ev, ei) in p.evidence" :key="'ev' + ei">· {{ ev }}</text>
+          <text class="evidence-item" v-for="(ev, ei) in p.evidence" :key="getEvKey(ei)">· {{ ev }}</text>
         </view>
       </view>
     </view>
@@ -25,7 +25,7 @@
     <!-- 需求画像 -->
     <view class="section" v-if="needProfile.length > 0">
       <text class="section-title">需求画像</text>
-      <view class="profile-item" v-for="(n, i) in needProfile" :key="'np' + i">
+      <view class="profile-item" v-for="(n, i) in needProfile" :key="getNpKey(i)">
         <text class="profile-dim">{{ n.dimension }}</text>
         <text class="profile-desc">{{ n.description }}</text>
         <text class="profile-suggest" v-if="n.suggestion">建议:{{ n.suggestion }}</text>
@@ -62,6 +62,9 @@ export default {
     }
   },
   methods: {
+    getUpKey: function(i) { return 'up' + i },
+    getEvKey: function(i) { return 'ev' + i },
+    getNpKey: function(i) { return 'np' + i },
     clamp: function(score) {
       var s = Number(score) || 0
       if (s < 0) return 0

+ 6 - 3
cfc-frontend/pages/health/ai-questionnaire.vue

@@ -3,7 +3,7 @@
     <!-- 场景选择 -->
     <view v-if="!sceneId" class="pick-wrap">
       <view class="pick-title">选择问卷场景</view>
-      <view class="scene-item" v-for="(s, i) in scenes" :key="'scene' + i" @click="chooseScene(s)">
+      <view class="scene-item" v-for="(s, i) in scenes" :key="getSceneKey(i)" @click="chooseScene(s)">
         <text class="scene-name">{{ s.sceneName }}</text>
         <text class="scene-desc">{{ s.description }}</text>
       </view>
@@ -12,7 +12,7 @@
     <!-- 成员选择 -->
     <view v-else-if="!memberId" class="pick-wrap">
       <view class="pick-title">选择填写人</view>
-      <view class="scene-item" v-for="(m, i) in members" :key="'mem' + i" @click="chooseMember(m)">
+      <view class="scene-item" v-for="(m, i) in members" :key="getMemberKey(i)" @click="chooseMember(m)">
         <text class="scene-name">{{ m.nickname || m.name || '成员' }}</text>
       </view>
     </view>
@@ -33,7 +33,7 @@
 
         <!-- 单选/多选 -->
         <view v-if="currentQuestion.type === 'single' || currentQuestion.type === 'multi'" class="qa-options">
-          <view class="qa-option" v-for="(opt, oi) in options" :key="'opt' + oi"
+          <view class="qa-option" v-for="(opt, oi) in options" :key="getOptKey(oi)"
                 :class="{ selected: isSelected(opt.id) }" @click="toggleOption(opt)">
             <text class="qa-option-label">{{ opt.label }}</text>
           </view>
@@ -100,6 +100,9 @@ export default {
     this.loadMembers()
   },
   methods: {
+    getSceneKey: function(i) { return 'scene' + i },
+    getMemberKey: function(i) { return 'mem' + i },
+    getOptKey: function(i) { return 'opt' + i },
     async loadScenes() {
       try {
         var res = await aiQSceneList({ enabledOnly: true })

+ 1 - 0
cfc-frontend/pages/health/health-plan-summary.vue

@@ -199,6 +199,7 @@ export default {
   },
   onShow: function() {
     this.loadHealthContext()
+  },
   methods: {
     formatDate: function(ts) {
       if (!ts) return '--'

+ 2 - 1
cfc-frontend/pages/health/health-status-form.vue

@@ -53,7 +53,7 @@
         >
           <text>{{ tag }}</text>
         </view>
-        <view v-for="(custom, idx) in customDiseases" :key="'c' + idx" class="tag-item tag-active">
+        <view v-for="(custom, idx) in customDiseases" :key="getCustomKey(idx)" class="tag-item tag-active">
           <text>{{ custom }}</text>
           <text class="tag-del" @click="removeCustomDisease(idx)">×</text>
         </view>
@@ -116,6 +116,7 @@ export default {
     this.load()
   },
   methods: {
+    getCustomKey: function(i) { return 'c' + i },
     load: function() {
       var self = this
       getHealthStatus().then(function(res) {