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

fix: 多页面综合修复 + 后端关系质量/能量同步 + 前端组件完善
- 后端: DatabaseInitializer/RelationshipQualityController/DimensionEnergySyncService
- 前端组件: ContactImport/DimensionActivities 完善
- 页面: action/body/mind/wisdom/discover/health 各维度更新
- splash.vue 删除
- 新增 HealthEnergyConfig.vue 管理端页面
- router/Layout 同步更新

User 4 месяцев назад
Родитель
Сommit
1288688022
27 измененных файлов с 1016 добавлено и 616 удалено
  1. 8 0
      cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
  2. 5 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/family/RelationshipQualityController.java
  3. 92 8
      cfc-backend/src/main/java/com/etotem/cfc/service/DimensionEnergySyncService.java
  4. 2 0
      cfc-frontend/App.vue
  5. 23 7
      cfc-frontend/components/ContactImport.vue
  6. 1 2
      cfc-frontend/components/DimensionActivities.vue
  7. 1 10
      cfc-frontend/pages.json
  8. 16 3
      cfc-frontend/pages/action-detail/contact-detail.vue
  9. 16 3
      cfc-frontend/pages/action/contact-detail.vue
  10. 1 0
      cfc-frontend/pages/action/index.vue
  11. 5 0
      cfc-frontend/pages/body-detail/checkin.vue
  12. 5 0
      cfc-frontend/pages/body-detail/health-report.vue
  13. 47 8
      cfc-frontend/pages/body/exercise-record.vue
  14. 25 62
      cfc-frontend/pages/body/index.vue
  15. 48 7
      cfc-frontend/pages/body/meal-record.vue
  16. 45 7
      cfc-frontend/pages/body/sleep-record.vue
  17. 8 1
      cfc-frontend/pages/discover-detail/activity-detail/activity-detail.vue
  18. 230 88
      cfc-frontend/pages/health/meditation-index.vue
  19. 1 1
      cfc-frontend/pages/health/nutrition-profile.vue
  20. 28 8
      cfc-frontend/pages/health/tongue-index.vue
  21. 142 79
      cfc-frontend/pages/index/index.vue
  22. 42 65
      cfc-frontend/pages/mind/index.vue
  23. 0 255
      cfc-frontend/pages/splash/splash.vue
  24. 2 1
      cfc-frontend/pages/wisdom/index.vue
  25. 6 0
      cfc-web/src/router/index.js
  26. 1 0
      cfc-web/src/views/Layout.vue
  27. 216 0
      cfc-web/src/views/admin/HealthEnergyConfig.vue

+ 8 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -1042,6 +1042,8 @@ log.info("已添加template_id列到tasks表");
             insertSysConfigSeed("trial_enabled", "true", "是否启用会员试用");
             insertSysConfigSeed("trial_enabled", "true", "是否启用会员试用");
             insertSysConfigSeed("trial_days", "7", "试用天数");
             insertSysConfigSeed("trial_days", "7", "试用天数");
             insertSysConfigSeed("trial_features", "[\"free_activities\",\"free_courses\",\"teacher_consultation\"]", "试用期间可用功能");
             insertSysConfigSeed("trial_features", "[\"free_activities\",\"free_courses\",\"teacher_consultation\"]", "试用期间可用功能");
+            insertSysConfigSeed("health_dimension_weights", "{\"growth\":0.20,\"sleep\":0.15,\"vision\":0.10,\"immunity\":0.15,\"nutrition\":0.15,\"gut\":0.10,\"exercise\":0.15}", "7维健康→身能量权重配置");
+            insertSysConfigSeed("health_dimension_sync_factor", "0.5", "7维健康同步至身能量系数");
             log.info("SysConfig 种子数据已加载");
             log.info("SysConfig 种子数据已加载");
         } catch (Exception e) {
         } catch (Exception e) {
             log.warn("SysConfig 种子数据初始化失败: {}", e.getMessage());
             log.warn("SysConfig 种子数据初始化失败: {}", e.getMessage());
@@ -2091,6 +2093,12 @@ log.info("已添加template_id列到tasks表");
 
 
         // 家庭成员表添加 role_override 列(替换旧 capability_role)
         // 家庭成员表添加 role_override 列(替换旧 capability_role)
         ensureColumn("family_members", "role_override", "VARCHAR(20) DEFAULT 'auto' COMMENT '角色覆盖: auto/parent/child/elderly'");
         ensureColumn("family_members", "role_override", "VARCHAR(20) DEFAULT 'auto' COMMENT '角色覆盖: auto/parent/child/elderly'");
+        try {
+            jdbcTemplate.execute("ALTER TABLE family_members DROP COLUMN capability_role");
+            log.info("已删除 family_members.capability_role 旧列,已由 role_override 替代");
+        } catch (Exception e) {
+            // 列不存在或已删除,忽略错误
+        }
 
 
         // activities 表添加报名/签到相关字段
         // activities 表添加报名/签到相关字段
         ensureColumn("activities", "require_registration", "TINYINT DEFAULT 1 COMMENT '是否需要报名: 1=需要, 0=直接参与'");
         ensureColumn("activities", "require_registration", "TINYINT DEFAULT 1 COMMENT '是否需要报名: 1=需要, 0=直接参与'");

+ 5 - 1
cfc-backend/src/main/java/com/etotem/cfc/controller/family/RelationshipQualityController.java

@@ -43,7 +43,11 @@ public class RelationshipQualityController {
     private Long getFamilyIdFromRequest(HttpServletRequest request, Map<String, Object> body) {
     private Long getFamilyIdFromRequest(HttpServletRequest request, Map<String, Object> body) {
         // 优先从请求体获取
         // 优先从请求体获取
         if (body != null && body.get("familyId") != null) {
         if (body != null && body.get("familyId") != null) {
-            return Long.valueOf(body.get("familyId").toString());
+            try {
+                return Long.valueOf(body.get("familyId").toString());
+            } catch (NumberFormatException e) {
+                // familyId不是有效数字(如前端传了{}),fallthrough到JWT用户推导
+            }
         }
         }
         // 从JWT用户推导
         // 从JWT用户推导
         Long userId = getUserId(request);
         Long userId = getUserId(request);

+ 92 - 8
cfc-backend/src/main/java/com/etotem/cfc/service/DimensionEnergySyncService.java

@@ -1,38 +1,122 @@
 package com.etotem.cfc.service;
 package com.etotem.cfc.service;
 
 
 import com.etotem.cfc.entity.HealthDimensionScore;
 import com.etotem.cfc.entity.HealthDimensionScore;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import org.springframework.stereotype.Service;
 import org.springframework.stereotype.Service;
+
 import javax.annotation.Resource;
 import javax.annotation.Resource;
-import java.util.List;
+import java.util.*;
 
 
 @Service
 @Service
 public class DimensionEnergySyncService {
 public class DimensionEnergySyncService {
 
 
+    private static final String CONFIG_WEIGHTS = "health_dimension_weights";
+    private static final String CONFIG_FACTOR = "health_dimension_sync_factor";
+
+    // 默认权重(各维度对身能量的贡献比例,总和=1.0)
+    private static final Map<String, Double> DEFAULT_WEIGHTS = new LinkedHashMap<>();
+
+    static {
+        DEFAULT_WEIGHTS.put("growth", 0.20);
+        DEFAULT_WEIGHTS.put("sleep", 0.15);
+        DEFAULT_WEIGHTS.put("vision", 0.10);
+        DEFAULT_WEIGHTS.put("immunity", 0.15);
+        DEFAULT_WEIGHTS.put("nutrition", 0.15);
+        DEFAULT_WEIGHTS.put("gut", 0.10);
+        DEFAULT_WEIGHTS.put("exercise", 0.15);
+    }
+
+    private static final double DEFAULT_FACTOR = 0.5;
+    private static final String DESCRIPTION = "7维评分同步至身能量";
+
     @Resource
     @Resource
     private HealthDimensionScoreService scoreService;
     private HealthDimensionScoreService scoreService;
 
 
     @Resource
     @Resource
     private EnergyService energyService;
     private EnergyService energyService;
 
 
+    @Resource
+    private SysConfigService configService;
+
+    private final ObjectMapper objectMapper = new ObjectMapper();
+
     public void syncBodyEnergy(Long memberId) {
     public void syncBodyEnergy(Long memberId) {
         List<HealthDimensionScore> latest = scoreService.getLatestScores(memberId);
         List<HealthDimensionScore> latest = scoreService.getLatestScores(memberId);
         if (latest == null || latest.isEmpty()) return;
         if (latest == null || latest.isEmpty()) return;
 
 
-        double total = 0;
-        int count = 0;
+        // 读取权重和系数配置
+        Map<String, Double> weights = loadWeights();
+        double factor = loadFactor();
+
+        // 构建维度→评分映射
+        Map<String, Integer> scoreMap = new HashMap<>();
         for (HealthDimensionScore s : latest) {
         for (HealthDimensionScore s : latest) {
-            total += s.getScore();
-            count++;
+            if (s.getScore() != null) {
+                scoreMap.put(s.getDimension(), s.getScore());
+            }
+        }
+
+        // 加权计算(仅统计已有评分的维度)
+        double weightedSum = 0;
+        double totalWeight = 0;
+        for (Map.Entry<String, Double> entry : weights.entrySet()) {
+            String dim = entry.getKey();
+            Double weight = entry.getValue();
+            Integer score = scoreMap.get(dim);
+            if (score != null && weight != null && weight > 0) {
+                weightedSum += score.doubleValue() * weight;
+                totalWeight += weight;
+            }
         }
         }
-        int average = count > 0 ? (int) (total / count) : 0;
-        int energyValue = (int) (average * 0.5);
+
+        if (totalWeight <= 0) return;
+
+        double normalizedScore = weightedSum / totalWeight;
+        int energyValue = (int) (normalizedScore * factor);
         if (energyValue <= 0) return;
         if (energyValue <= 0) return;
 
 
         try {
         try {
             energyService.awardEnergy(memberId, "dimension", memberId,
             energyService.awardEnergy(memberId, "dimension", memberId,
-                energyValue, "7维评分同步至身能量", 30);
+                    energyValue, DESCRIPTION, 30);
         } catch (Exception e) {
         } catch (Exception e) {
             // Do not block score submission
             // Do not block score submission
         }
         }
     }
     }
+
+    /**
+     * 从 sys_config 读取维度权重配置(JSON格式)
+     * 示例: {"growth":0.20,"sleep":0.15,"vision":0.10,"immunity":0.15,"nutrition":0.15,"gut":0.10,"exercise":0.15}
+     * 无配置时返回默认值
+     */
+    private Map<String, Double> loadWeights() {
+        try {
+            String json = configService.getValue(CONFIG_WEIGHTS);
+            if (json != null && !json.isEmpty()) {
+                Map<String, Double> parsed = objectMapper.readValue(json,
+                        new TypeReference<Map<String, Double>>() {});
+                if (parsed != null && !parsed.isEmpty()) {
+                    return parsed;
+                }
+            }
+        } catch (Exception e) {
+            // 配置缺失或解析失败,使用默认值
+        }
+        return new LinkedHashMap<>(DEFAULT_WEIGHTS);
+    }
+
+    /**
+     * 读取同步系数(默认0.5),可从 sys_config 调整
+     */
+    private double loadFactor() {
+        try {
+            String val = configService.getValue(CONFIG_FACTOR);
+            if (val != null && !val.isEmpty()) {
+                return Double.parseDouble(val);
+            }
+        } catch (Exception e) {
+            // 使用默认值
+        }
+        return DEFAULT_FACTOR;
+    }
 }
 }

+ 2 - 0
cfc-frontend/App.vue

@@ -6,6 +6,8 @@ Vue.prototype.$config = config
 export default {
 export default {
   onLaunch: function(options) {
   onLaunch: function(options) {
     console.log('App Launch', options)
     console.log('App Launch', options)
+    // 冷启动标记,首页根据此标记决定显示 splash overlay
+    uni.setStorageSync('_showSplashOverlay', '1')
     // 处理小程序码场景值(从 wxacode 扫码进入)
     // 处理小程序码场景值(从 wxacode 扫码进入)
     this.handleScene(options)
     this.handleScene(options)
     
     

+ 23 - 7
cfc-frontend/components/ContactImport.vue

@@ -31,7 +31,7 @@
         </view>
         </view>
         <view class="form-row">
         <view class="form-row">
           <text class="form-label">个人介绍</text>
           <text class="form-label">个人介绍</text>
-          <input class="form-input" v-model="form.bio" placeholder="选填" />
+          <textarea class="form-textarea" v-model="form.bio" placeholder="选填" />
         </view>
         </view>
       </view>
       </view>
       <view class="modal-actions">
       <view class="modal-actions">
@@ -124,15 +124,17 @@ export default {
   background: rgba(0,0,0,0.5);
   background: rgba(0,0,0,0.5);
   z-index: 999;
   z-index: 999;
   display: flex;
   display: flex;
-  align-items: flex-end;
+  align-items: center;
+  justify-content: center;
 }
 }
 .modal {
 .modal {
   background: #fff;
   background: #fff;
-  border-radius: 24rpx 24rpx 0 0;
+  border-radius: 24rpx;
   padding: 40rpx 30rpx;
   padding: 40rpx 30rpx;
-  width: 100%;
+  width: 85%;
+  max-width: 600rpx;
   box-sizing: border-box;
   box-sizing: border-box;
-  max-height: 80vh;
+  max-height: 70vh;
   overflow-y: auto;
   overflow-y: auto;
 }
 }
 .modal-title {
 .modal-title {
@@ -155,18 +157,32 @@ export default {
 .form-input {
 .form-input {
   border: 1rpx solid #ddd;
   border: 1rpx solid #ddd;
   border-radius: 12rpx;
   border-radius: 12rpx;
-  padding: 16rpx 20rpx;
+  padding: 24rpx 20rpx;
   font-size: 28rpx;
   font-size: 28rpx;
   width: 100%;
   width: 100%;
   box-sizing: border-box;
   box-sizing: border-box;
   color: #333;
   color: #333;
+  min-height: 80rpx;
+}
+.form-textarea {
+  border: 1rpx solid #ddd;
+  border-radius: 12rpx;
+  padding: 24rpx 20rpx;
+  font-size: 28rpx;
+  width: 100%;
+  box-sizing: border-box;
+  color: #333;
+  min-height: 160rpx;
 }
 }
 .form-picker {
 .form-picker {
   border: 1rpx solid #ddd;
   border: 1rpx solid #ddd;
   border-radius: 12rpx;
   border-radius: 12rpx;
-  padding: 16rpx 20rpx;
+  padding: 24rpx 20rpx;
   font-size: 28rpx;
   font-size: 28rpx;
   color: #333;
   color: #333;
+  min-height: 80rpx;
+  display: flex;
+  align-items: center;
 }
 }
 .modal-actions {
 .modal-actions {
   display: flex;
   display: flex;

+ 1 - 2
cfc-frontend/components/DimensionActivities.vue

@@ -17,8 +17,7 @@
           </view>
           </view>
           <view class="activity-bottom">
           <view class="activity-bottom">
             <text class="activity-status" :class="'status-' + (act.status || 'upcoming')">{{ statusText(act.status) }}</text>
             <text class="activity-status" :class="'status-' + (act.status || 'upcoming')">{{ statusText(act.status) }}</text>
-            <text class="activity-fee" v-if="!isLoggedIn">登录查看</text>
-            <text class="activity-fee" v-else-if="act.priceLabel">{{ act.priceLabel }}</text>
+            <text class="activity-fee" v-if="act.priceLabel">{{ act.priceLabel }}</text>
             <text class="activity-fee" v-else-if="act.price && act.price > 0">{{ formatPriceWithSymbol(act.price) }}</text>
             <text class="activity-fee" v-else-if="act.price && act.price > 0">{{ formatPriceWithSymbol(act.price) }}</text>
             <text class="activity-fee fee-free" v-else>免费</text>
             <text class="activity-fee fee-free" v-else>免费</text>
           </view>
           </view>

+ 1 - 10
cfc-frontend/pages.json

@@ -1,11 +1,5 @@
 {
 {
   "pages": [
   "pages": [
-    {
-      "path": "pages/splash/splash",
-      "style": {
-        "navigationStyle": "custom"
-      }
-    },
     {
     {
       "path": "pages/index/index",
       "path": "pages/index/index",
       "style": {
       "style": {
@@ -625,10 +619,6 @@
           "path": "sleep-index",
           "path": "sleep-index",
           "style": { "navigationBarTitleText": "作息" }
           "style": { "navigationBarTitleText": "作息" }
         },
         },
-        {
-          "path": "gut-index",
-          "style": { "navigationBarTitleText": "菌群" }
-        },
         {
         {
           "path": "meditation-index",
           "path": "meditation-index",
           "style": { "navigationBarTitleText": "冥想" }
           "style": { "navigationBarTitleText": "冥想" }
@@ -849,6 +839,7 @@
     "navigationBarBackgroundColor": "#FFFFFF",
     "navigationBarBackgroundColor": "#FFFFFF",
     "backgroundColor": "#F8F8F8"
     "backgroundColor": "#F8F8F8"
   },
   },
+  "lazyCodeLoading": "requiredComponents",
   "tabBar": {
   "tabBar": {
     "color": "#7A7E83",
     "color": "#7A7E83",
     "selectedColor": "#F97316",
     "selectedColor": "#F97316",

+ 16 - 3
cfc-frontend/pages/action-detail/contact-detail.vue

@@ -76,7 +76,7 @@
         </view>
         </view>
         <view class="edit-row">
         <view class="edit-row">
           <text class="edit-label">个人介绍</text>
           <text class="edit-label">个人介绍</text>
-          <input class="edit-input" v-model="editForm.bio" placeholder="选填" />
+          <textarea class="edit-textarea" v-model="editForm.bio" placeholder="选填" />
         </view>
         </view>
         <view class="edit-row">
         <view class="edit-row">
           <text class="edit-label">备注</text>
           <text class="edit-label">备注</text>
@@ -330,16 +330,29 @@ export default {
 .edit-input {
 .edit-input {
   border: 1rpx solid #ddd;
   border: 1rpx solid #ddd;
   border-radius: 12rpx;
   border-radius: 12rpx;
-  padding: 16rpx 20rpx;
+  padding: 24rpx 20rpx;
   font-size: 28rpx;
   font-size: 28rpx;
   width: 100%;
   width: 100%;
   box-sizing: border-box;
   box-sizing: border-box;
+  min-height: 80rpx;
+}
+.edit-textarea {
+  border: 1rpx solid #ddd;
+  border-radius: 12rpx;
+  padding: 24rpx 20rpx;
+  font-size: 28rpx;
+  width: 100%;
+  box-sizing: border-box;
+  min-height: 160rpx;
 }
 }
 .edit-picker {
 .edit-picker {
   border: 1rpx solid #ddd;
   border: 1rpx solid #ddd;
   border-radius: 12rpx;
   border-radius: 12rpx;
-  padding: 16rpx 20rpx;
+  padding: 24rpx 20rpx;
   font-size: 28rpx;
   font-size: 28rpx;
+  min-height: 80rpx;
+  display: flex;
+  align-items: center;
 }
 }
 .btn-save {
 .btn-save {
   width: 100%;
   width: 100%;

+ 16 - 3
cfc-frontend/pages/action/contact-detail.vue

@@ -76,7 +76,7 @@
         </view>
         </view>
         <view class="edit-row">
         <view class="edit-row">
           <text class="edit-label">个人介绍</text>
           <text class="edit-label">个人介绍</text>
-          <input class="edit-input" v-model="editForm.bio" placeholder="选填" />
+          <textarea class="edit-textarea" v-model="editForm.bio" placeholder="选填" />
         </view>
         </view>
         <view class="edit-row">
         <view class="edit-row">
           <text class="edit-label">备注</text>
           <text class="edit-label">备注</text>
@@ -330,16 +330,29 @@ export default {
 .edit-input {
 .edit-input {
   border: 1rpx solid #ddd;
   border: 1rpx solid #ddd;
   border-radius: 12rpx;
   border-radius: 12rpx;
-  padding: 16rpx 20rpx;
+  padding: 24rpx 20rpx;
   font-size: 28rpx;
   font-size: 28rpx;
   width: 100%;
   width: 100%;
   box-sizing: border-box;
   box-sizing: border-box;
+  min-height: 80rpx;
+}
+.edit-textarea {
+  border: 1rpx solid #ddd;
+  border-radius: 12rpx;
+  padding: 24rpx 20rpx;
+  font-size: 28rpx;
+  width: 100%;
+  box-sizing: border-box;
+  min-height: 160rpx;
 }
 }
 .edit-picker {
 .edit-picker {
   border: 1rpx solid #ddd;
   border: 1rpx solid #ddd;
   border-radius: 12rpx;
   border-radius: 12rpx;
-  padding: 16rpx 20rpx;
+  padding: 24rpx 20rpx;
   font-size: 28rpx;
   font-size: 28rpx;
+  min-height: 80rpx;
+  display: flex;
+  align-items: center;
 }
 }
 .btn-save {
 .btn-save {
   width: 100%;
   width: 100%;

+ 1 - 0
cfc-frontend/pages/action/index.vue

@@ -385,6 +385,7 @@ export default {
       uni.navigateTo({ url: '/pages/tasks/tasks' })
       uni.navigateTo({ url: '/pages/tasks/tasks' })
     },
     },
     goActivityDetail: function(act) {
     goActivityDetail: function(act) {
+      if (!act || !act.id) return
       uni.navigateTo({ url: '/pages/discover-detail/activity-detail/activity-detail?id=' + act.id })
       uni.navigateTo({ url: '/pages/discover-detail/activity-detail/activity-detail?id=' + act.id })
     },
     },
     goMoreActivities: function() {
     goMoreActivities: function() {

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

@@ -161,6 +161,11 @@ export default {
   },
   },
   onLoad: function(options) {
   onLoad: function(options) {
     this.childId = options.childId ? parseInt(options.childId) : null
     this.childId = options.childId ? parseInt(options.childId) : null
+    // 如果没有传 childId,尝试从 storage 读取
+    if (!this.childId) {
+      var savedChildId = uni.getStorageSync('currentChildId')
+      if (savedChildId) this.childId = parseInt(savedChildId)
+    }
     this.checkinDate = this.formatDate(new Date())
     this.checkinDate = this.formatDate(new Date())
     this.loadHistory()
     this.loadHistory()
   },
   },

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

@@ -336,6 +336,11 @@ export default {
     var reportId = options.reportId
     var reportId = options.reportId
     this.childId = childId ? parseInt(childId) : null
     this.childId = childId ? parseInt(childId) : null
     this.reportId = reportId ? parseInt(reportId) : null
     this.reportId = reportId ? parseInt(reportId) : null
+    // 如果没有传 childId,尝试从 storage 读取(可能是家长自己的)
+    if (!this.childId) {
+      var savedChildId = uni.getStorageSync('currentChildId')
+      if (savedChildId) this.childId = parseInt(savedChildId)
+    }
     this.loadData()
     this.loadData()
   },
   },
   onReady: function() {
   onReady: function() {

+ 47 - 8
cfc-frontend/pages/body/exercise-record.vue

@@ -6,6 +6,14 @@
       <view class="nav-placeholder"></view>
       <view class="nav-placeholder"></view>
     </view>
     </view>
 
 
+    <!-- 成员选择 -->
+    <view class="member-selector-card">
+      <text class="member-label">记录人</text>
+      <picker mode="selector" :range="memberOptions" :value="selectedMemberIndex" @change="onMemberChange">
+        <view class="picker-display">{{ memberOptions[selectedMemberIndex] }}</view>
+      </picker>
+    </view>
+
     <view class="section-card">
     <view class="section-card">
       <view class="section-title">记录运动</view>
       <view class="section-title">记录运动</view>
       <view class="exercise-types">
       <view class="exercise-types">
@@ -65,11 +73,15 @@
 
 
 <script>
 <script>
 import config from '@/config.js'
 import config from '@/config.js'
+import { getChildren } from '@/utils/api.js'
 
 
 export default {
 export default {
   data() {
   data() {
     return {
     return {
       childId: null,
       childId: null,
+      selectedMemberIndex: 0,
+      memberOptions: ['我自己'],
+      memberIds: [],
       selectedType: 'running',
       selectedType: 'running',
       duration: '',
       duration: '',
       intensity: '中',
       intensity: '中',
@@ -91,23 +103,45 @@ export default {
   },
   },
   onLoad: function(options) {
   onLoad: function(options) {
     this.childId = options.childId ? parseInt(options.childId) : null
     this.childId = options.childId ? parseInt(options.childId) : null
-    if (this.childId) this.loadHistory()
+    this.loadChildren()
+    this.loadHistory()
   },
   },
   methods: {
   methods: {
+    loadChildren: function() {
+      var self = this
+      getChildren().then(function(res) {
+        if (res.code === 200 && res.data && res.data.length > 0) {
+          self.memberIds = res.data.map(function(c) { return c.childId })
+          self.memberOptions = ['我自己'].concat(res.data.map(function(c) { return c.name || c.nickname || '孩子' }))
+        }
+      }).catch(function() {})
+    },
+    onMemberChange: function(e) {
+      this.selectedMemberIndex = parseInt(e.detail.value)
+      if (this.selectedMemberIndex === 0) {
+        this.childId = null
+      } else {
+        this.childId = this.memberIds[this.selectedMemberIndex - 1]
+      }
+      this.loadHistory()
+    },
     submitExercise: function() {
     submitExercise: function() {
       if (!this.duration || parseInt(this.duration) <= 0) {
       if (!this.duration || parseInt(this.duration) <= 0) {
         uni.showToast({ title: '请输入有效时长', icon: 'none' })
         uni.showToast({ title: '请输入有效时长', icon: 'none' })
         return
         return
       }
       }
-      uni.showLoading({ title: '保存中...' })
-      healthRequest('/api/health/exercise/create', {
-        childId: this.childId,
-        memberId: this.childId,
+      var data = {
         exerciseType: this.selectedType,
         exerciseType: this.selectedType,
         durationMinutes: parseInt(this.duration),
         durationMinutes: parseInt(this.duration),
         intensity: this.intensity,
         intensity: this.intensity,
         remark: this.remark
         remark: this.remark
-      }).then(function() {
+      }
+      if (this.childId) {
+        data.childId = this.childId
+        data.memberId = this.childId
+      }
+      uni.showLoading({ title: '保存中...' })
+      healthRequest('/api/health/exercise/create', data).then(function() {
         uni.hideLoading()
         uni.hideLoading()
         uni.showToast({ title: '保存成功', icon: 'success' })
         uni.showToast({ title: '保存成功', icon: 'success' })
         this.duration = ''
         this.duration = ''
@@ -120,9 +154,11 @@ export default {
     },
     },
     loadHistory: function() {
     loadHistory: function() {
       var self = this
       var self = this
-      healthRequest('/api/health/exercise/list', { childId: this.childId }).then(function(res) {
+      var params = {}
+      if (this.childId) params.childId = this.childId
+      healthRequest('/api/health/exercise/list', params).then(function(res) {
         if (res.data) self.exerciseHistory = res.data
         if (res.data) self.exerciseHistory = res.data
-      }).catch(function() {})
+      }).catch(function() { self.exerciseHistory = [] })
     },
     },
     getTypeIcon: function(type) {
     getTypeIcon: function(type) {
       var map = { running: '🏃', cycling: '🚴', swimming: '🏊', jumping_rope: '🤸', ball_games: '⚽', stretching: '🧘' }
       var map = { running: '🏃', cycling: '🚴', swimming: '🏊', jumping_rope: '🤸', ball_games: '⚽', stretching: '🧘' }
@@ -153,6 +189,9 @@ function healthRequest(url, data) {
 .nav-back { font-size: 32rpx; color: #5B9BD5; }
 .nav-back { font-size: 32rpx; color: #5B9BD5; }
 .nav-title { font-size: 32rpx; font-weight: bold; color: #333; }
 .nav-title { font-size: 32rpx; font-weight: bold; color: #333; }
 .nav-placeholder { width: 80rpx; }
 .nav-placeholder { width: 80rpx; }
+.member-selector-card { margin: 20rpx 30rpx; background: #fff; border-radius: 20rpx; padding: 20rpx 24rpx; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); display: flex; flex-direction: row; align-items: center; justify-content: space-between; }
+.member-label { font-size: 28rpx; color: #666; }
+.picker-display { font-size: 28rpx; color: #333; font-weight: 500; }
 .section-card { margin: 20rpx 30rpx; background: #fff; border-radius: 20rpx; padding: 24rpx; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); }
 .section-card { margin: 20rpx 30rpx; background: #fff; border-radius: 20rpx; padding: 24rpx; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); }
 .section-title { font-size: 30rpx; font-weight: bold; color: #333; margin-bottom: 20rpx; }
 .section-title { font-size: 30rpx; font-weight: bold; color: #333; margin-bottom: 20rpx; }
 .exercise-types { display: flex; flex-direction: row; flex-wrap: wrap; margin-bottom: 20rpx; }
 .exercise-types { display: flex; flex-direction: row; flex-wrap: wrap; margin-bottom: 20rpx; }

+ 25 - 62
cfc-frontend/pages/body/index.vue

@@ -68,6 +68,12 @@
         @dimensionClick="goDimensionDetail" />
         @dimensionClick="goDimensionDetail" />
     </view>
     </view>
 
 
+    <!-- 七维健康全景(嵌入卡片) -->
+    <HealthDimensionsSection
+      v-if="isLoggedIn && dimensionData && dimensionData.dimensions"
+      :healthData="dimensionData"
+      :activeChildId="activeChildId" />
+
     <!-- 功能入口 -->
     <!-- 功能入口 -->
     <view class="func-section" v-if="sectionVisible('func_entries')">
     <view class="func-section" v-if="sectionVisible('func_entries')">
       <view class="func-grid">
       <view class="func-grid">
@@ -172,19 +178,6 @@
       @articleClick="goArticleDetail"
       @articleClick="goArticleDetail"
       @moreArticles="goMoreArticles" />
       @moreArticles="goMoreArticles" />
 
 
-    <!-- 健康小贴士 -->
-    <view class="section" v-if="sectionVisible('health_tips')">
-      <view class="section-header">
-        <text class="section-title">健康小贴士</text>
-      </view>
-      <view class="tips-list">
-        <view class="tip-card" v-for="item in healthTips" :key="item.id">
-          <text class="tip-title">{{ item.title }}</text>
-          <text class="tip-summary">{{ item.summary }}</text>
-        </view>
-      </view>
-    </view>
-
     <!-- 快捷操作按钮 -->
     <!-- 快捷操作按钮 -->
     <view class="section quick-actions" v-if="isLoggedIn">
     <view class="section quick-actions" v-if="isLoggedIn">
       <view class="action-btn" @click="goToHealthReport">
       <view class="action-btn" @click="goToHealthReport">
@@ -211,7 +204,6 @@
 import TabTransition from '../../components/tab-transition.vue'
 import TabTransition from '../../components/tab-transition.vue'
 import PageBanner from '../../components/PageBanner.vue'
 import PageBanner from '../../components/PageBanner.vue'
 import LoginGuideCard from '../../components/LoginGuideCard.vue'
 import LoginGuideCard from '../../components/LoginGuideCard.vue'
-import HealthTips from '../../components/HealthTips.vue'
 import FamilyEnergyBar from '../../components/FamilyEnergyBar.vue'
 import FamilyEnergyBar from '../../components/FamilyEnergyBar.vue'
 import UserQuickEntry from '../../components/UserQuickEntry.vue'
 import UserQuickEntry from '../../components/UserQuickEntry.vue'
 import DimensionTasks from '../../components/DimensionTasks.vue'
 import DimensionTasks from '../../components/DimensionTasks.vue'
@@ -220,6 +212,7 @@ import DimensionProducts from '../../components/DimensionProducts.vue'
 import DimensionArticles from '../../components/DimensionArticles.vue'
 import DimensionArticles from '../../components/DimensionArticles.vue'
 import FamilyRelationGraph from '../../components/FamilyRelationGraph.vue'
 import FamilyRelationGraph from '../../components/FamilyRelationGraph.vue'
 import RadarChart from '../../components/RadarChart.vue'
 import RadarChart from '../../components/RadarChart.vue'
+import HealthDimensionsSection from '../../components/HealthDimensionsSection.vue'
 import AIFloatingAvatar from '../../components/AIFloatingAvatar.vue'
 import AIFloatingAvatar from '../../components/AIFloatingAvatar.vue'
 import FamilyRelationshipSection from '../../components/FamilyRelationshipSection.vue'
 import FamilyRelationshipSection from '../../components/FamilyRelationshipSection.vue'
 import { getVisibleSections, getEnergyOverview, getChildren, getTodayTasksByCategory, getActivityList, getProductsByDomain, getFamilyEnergySandbox, getVisibleFamilyMembers, getDimensionOverview, getFeaturedArticles, getContactList, getHealthAlerts, getMilestones } from '../../utils/api.js'
 import { getVisibleSections, getEnergyOverview, getChildren, getTodayTasksByCategory, getActivityList, getProductsByDomain, getFamilyEnergySandbox, getVisibleFamilyMembers, getDimensionOverview, getFeaturedArticles, getContactList, getHealthAlerts, getMilestones } from '../../utils/api.js'
@@ -253,7 +246,7 @@ const healthRequest = function(url, data) {
 }
 }
 
 
 export default {
 export default {
-  components: { TabTransition, PageBanner, LoginGuideCard, HealthTips, FamilyEnergyBar, UserQuickEntry, DimensionTasks, DimensionActivities, DimensionProducts, DimensionArticles, FamilyRelationGraph, RadarChart, AIFloatingAvatar, FamilyRelationshipSection },
+  components: { TabTransition, PageBanner, LoginGuideCard, FamilyEnergyBar, UserQuickEntry, DimensionTasks, DimensionActivities, DimensionProducts, DimensionArticles, FamilyRelationGraph, RadarChart, HealthDimensionsSection, AIFloatingAvatar, FamilyRelationshipSection },
   data() {
   data() {
     return {
     return {
       isLoggedIn: false,
       isLoggedIn: false,
@@ -274,22 +267,15 @@ export default {
       bodyArticles: [],
       bodyArticles: [],
       dimensionData: null,
       dimensionData: null,
       funcList: [
       funcList: [
-        { icon: '\u{1F3C3}', label: '运动', needLogin: true, page: '/pages/health/exercise-index' },
-        { icon: '\u{1F957}', label: '饮食', needLogin: true, page: '/pages/health/diet-index' },
-        { icon: '\u{1F634}', label: '作息', needLogin: true, page: '/pages/health/sleep-index' },
-        { icon: '\u{1F9A0}', label: '菌群', needLogin: true, page: '/pages/health/gut-index' },
+        { icon: '\u{1F3C3}', label: '运动', needLogin: true, page: '/pages/body/exercise-record' },
+        { icon: '\u{1F957}', label: '饮食', needLogin: true, page: '/pages/body/meal-record' },
+        { icon: '\u{1F634}', label: '作息', needLogin: true, page: '/pages/body/sleep-record' },
         { icon: '\u{1F9D8}', label: '冥想', needLogin: true, page: '/pages/health/meditation-index' },
         { icon: '\u{1F9D8}', label: '冥想', needLogin: true, page: '/pages/health/meditation-index' },
         { icon: '\u{1F4CA}', label: '舌诊', needLogin: true, page: '/pages/health/tongue-index' }
         { icon: '\u{1F4CA}', label: '舌诊', needLogin: true, page: '/pages/health/tongue-index' }
       ],
       ],
       contactList: [],
       contactList: [],
       healthAlerts: [],
       healthAlerts: [],
-      milestones: [],
-      healthTips: [
-        { id: 1, title: '儿童每日运动指南', summary: '不同年龄段儿童每天需要多少运动量?科学运动助力健康成长。' },
-        { id: 2, title: '五行饮食搭配法则', summary: '根据五行理论合理搭配膳食,均衡营养从每一餐开始。' },
-        { id: 3, title: '优质睡眠从小养成', summary: '帮助孩子建立规律的作息习惯,提升睡眠质量。' },
-        { id: 4, title: '肠道菌群与免疫力', summary: '了解益生菌对儿童健康的重要作用,从内部提升抵抗力。' }
-      ]
+      milestones: []
     }
     }
   },
   },
   computed: {
   computed: {
@@ -518,6 +504,7 @@ export default {
       uni.navigateTo({ url: '/pages/tasks/tasks' })
       uni.navigateTo({ url: '/pages/tasks/tasks' })
     },
     },
     goActivityDetail: function(act) {
     goActivityDetail: function(act) {
+      if (!act || !act.id) return
       uni.navigateTo({ url: '/pages/discover-detail/activity-detail/activity-detail?id=' + act.id })
       uni.navigateTo({ url: '/pages/discover-detail/activity-detail/activity-detail?id=' + act.id })
     },
     },
     goMoreActivities: function() {
     goMoreActivities: function() {
@@ -568,31 +555,33 @@ export default {
       if (item.page) uni.navigateTo({ url: item.page })
       if (item.page) uni.navigateTo({ url: item.page })
     },
     },
     goToHealthReport: function() {
     goToHealthReport: function() {
-      if (!this.activeChildId) {
-        uni.showToast({ title: '请先选择孩子', icon: 'none' })
+      var id = this.activeChildId || uni.getStorageSync('currentChildId')
+      if (!id) {
+        uni.showToast({ title: '请先选择成员', icon: 'none' })
         return
         return
       }
       }
-      uni.navigateTo({ url: '/pages/body-detail/health-report?childId=' + this.activeChildId })
+      uni.navigateTo({ url: '/pages/body-detail/health-report?childId=' + id })
     },
     },
     goDimensionDetail: function(dimKey) {
     goDimensionDetail: function(dimKey) {
       uni.navigateTo({ url: '/pages/body/dimension-detail?dimension=' + dimKey + '&childId=' + this.activeChildId })
       uni.navigateTo({ url: '/pages/body/dimension-detail?dimension=' + dimKey + '&childId=' + this.activeChildId })
     },
     },
     goToCheckin: function() {
     goToCheckin: function() {
-      if (!this.activeChildId) {
-        uni.showToast({ title: '请先选择孩子', icon: 'none' })
+      var id = this.activeChildId || uni.getStorageSync('currentChildId')
+      if (!id) {
+        uni.showToast({ title: '请先选择成员', icon: 'none' })
         return
         return
       }
       }
-      uni.navigateTo({ url: '/pages/body-detail/checkin?childId=' + this.activeChildId })
+      uni.navigateTo({ url: '/pages/body-detail/checkin?childId=' + id })
     },
     },
     goToNutritionProfile: function() {
     goToNutritionProfile: function() {
-      if (!this.activeChildId) {
-        uni.showToast({ title: '请先选择孩子', icon: 'none' })
+      var id = this.activeChildId || uni.getStorageSync('currentChildId')
+      if (!id) {
+        uni.showToast({ title: '请先选择成员', icon: 'none' })
         return
         return
       }
       }
-      uni.navigateTo({ url: '/pages/health/nutrition-profile?childId=' + this.activeChildId })
+      uni.navigateTo({ url: '/pages/health/nutrition-profile?childId=' + id })
     },
     },
     goToUploadReport: function() {
     goToUploadReport: function() {
-      // 使用当前选中孩子;如果 loadChildren 异步未完成则尝试从 storage 读取
       var id = this.activeChildId || uni.getStorageSync('currentChildId') || ''
       var id = this.activeChildId || uni.getStorageSync('currentChildId') || ''
       uni.navigateTo({ url: '/pages/health/report-upload?childId=' + id + '&from=body' })
       uni.navigateTo({ url: '/pages/health/report-upload?childId=' + id + '&from=body' })
     },
     },
@@ -833,32 +822,6 @@ export default {
   margin-top: 8rpx;
   margin-top: 8rpx;
 }
 }
 
 
-/* ===== 健康小贴士 ===== */
-.tips-list {
-  display: flex;
-  flex-direction: column;
-}
-.tip-card {
-  background: #fff;
-  border-radius: 20rpx;
-  padding: 24rpx;
-  margin-bottom: 16rpx;
-  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
-}
-.tip-title {
-  font-size: 28rpx;
-  font-weight: bold;
-  color: #333;
-  display: block;
-  margin-bottom: 10rpx;
-}
-.tip-summary {
-  font-size: 24rpx;
-  color: #888;
-  line-height: 1.5;
-  display: block;
-}
-
 /* ===== 快捷操作 ===== */
 /* ===== 快捷操作 ===== */
 .quick-actions {
 .quick-actions {
   display: flex;
   display: flex;

+ 48 - 7
cfc-frontend/pages/body/meal-record.vue

@@ -6,6 +6,14 @@
       <view class="nav-placeholder"></view>
       <view class="nav-placeholder"></view>
     </view>
     </view>
 
 
+    <!-- 成员选择 -->
+    <view class="member-selector-card">
+      <text class="member-label">记录人</text>
+      <picker mode="selector" :range="memberOptions" :value="selectedMemberIndex" @change="onMemberChange">
+        <view class="picker-display">{{ memberOptions[selectedMemberIndex] }}</view>
+      </picker>
+    </view>
+
     <view class="section-card">
     <view class="section-card">
       <view class="section-title">今日饮食</view>
       <view class="section-title">今日饮食</view>
       <view class="meal-types">
       <view class="meal-types">
@@ -71,10 +79,15 @@
 
 
 <script>
 <script>
 import config from '@/config.js'
 import config from '@/config.js'
+import { getChildren } from '@/utils/api.js'
 
 
 export default {
 export default {
   data() {
   data() {
     return {
     return {
+      childId: null,
+      selectedMemberIndex: 0,
+      memberOptions: ['我自己'],
+      memberIds: [],
       activeMealType: 'breakfast',
       activeMealType: 'breakfast',
       foodName: '',
       foodName: '',
       foodAmount: '',
       foodAmount: '',
@@ -91,9 +104,28 @@ export default {
   },
   },
   onLoad: function(options) {
   onLoad: function(options) {
     this.childId = options.childId ? parseInt(options.childId) : null
     this.childId = options.childId ? parseInt(options.childId) : null
+    this.loadChildren()
     if (this.childId) this.loadHistory()
     if (this.childId) this.loadHistory()
   },
   },
   methods: {
   methods: {
+    loadChildren: function() {
+      var self = this
+      getChildren().then(function(res) {
+        if (res.code === 200 && res.data && res.data.length > 0) {
+          self.memberIds = res.data.map(function(c) { return c.childId })
+          self.memberOptions = ['我自己'].concat(res.data.map(function(c) { return c.name || c.nickname || '孩子' }))
+        }
+      }).catch(function() {})
+    },
+    onMemberChange: function(e) {
+      this.selectedMemberIndex = parseInt(e.detail.value)
+      if (this.selectedMemberIndex === 0) {
+        this.childId = null
+      } else {
+        this.childId = this.memberIds[this.selectedMemberIndex - 1]
+      }
+      if (this.childId) this.loadHistory()
+    },
     addFood: function() {
     addFood: function() {
       if (!this.foodName.trim()) {
       if (!this.foodName.trim()) {
         uni.showToast({ title: '请输入食物名称', icon: 'none' })
         uni.showToast({ title: '请输入食物名称', icon: 'none' })
@@ -115,16 +147,20 @@ export default {
         return
         return
       }
       }
       uni.showLoading({ title: '保存中...' })
       uni.showLoading({ title: '保存中...' })
+      var self = this
       var promises = this.todayFoods.map(function(food) {
       var promises = this.todayFoods.map(function(food) {
-        return healthRequest('/api/health/meal/create', {
-          childId: this.childId,
-          memberId: this.childId,
+        var data = {
           mealType: food.mealType,
           mealType: food.mealType,
           foodName: food.foodName,
           foodName: food.foodName,
           foodAmount: food.foodAmount,
           foodAmount: food.foodAmount,
           calories: food.calories
           calories: food.calories
-        })
-      }, this)
+        }
+        if (self.childId) {
+          data.childId = self.childId
+          data.memberId = self.childId
+        }
+        return healthRequest('/api/health/meal/create', data)
+      })
       Promise.all(promises).then(function() {
       Promise.all(promises).then(function() {
         uni.hideLoading()
         uni.hideLoading()
         uni.showToast({ title: '保存成功', icon: 'success' })
         uni.showToast({ title: '保存成功', icon: 'success' })
@@ -137,9 +173,11 @@ export default {
     },
     },
     loadHistory: function() {
     loadHistory: function() {
       var self = this
       var self = this
-      healthRequest('/api/health/meal/list', { childId: this.childId }).then(function(res) {
+      var params = {}
+      if (this.childId) params.childId = this.childId
+      healthRequest('/api/health/meal/list', params).then(function(res) {
         if (res.data) self.mealHistory = res.data
         if (res.data) self.mealHistory = res.data
-      }).catch(function() {})
+      }).catch(function() { self.mealHistory = [] })
     },
     },
     goBack: function() { uni.navigateBack() }
     goBack: function() { uni.navigateBack() }
   }
   }
@@ -172,6 +210,9 @@ function healthRequest(url, data) {
 .nav-back { font-size: 32rpx; color: #5B9BD5; }
 .nav-back { font-size: 32rpx; color: #5B9BD5; }
 .nav-title { font-size: 32rpx; font-weight: bold; color: #333; }
 .nav-title { font-size: 32rpx; font-weight: bold; color: #333; }
 .nav-placeholder { width: 80rpx; }
 .nav-placeholder { width: 80rpx; }
+.member-selector-card { margin: 20rpx 30rpx; background: #fff; border-radius: 20rpx; padding: 20rpx 24rpx; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); display: flex; flex-direction: row; align-items: center; justify-content: space-between; }
+.member-label { font-size: 28rpx; color: #666; }
+.picker-display { font-size: 28rpx; color: #333; font-weight: 500; }
 .section-card { margin: 20rpx 30rpx; background: #fff; border-radius: 20rpx; padding: 24rpx; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); }
 .section-card { margin: 20rpx 30rpx; background: #fff; border-radius: 20rpx; padding: 24rpx; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); }
 .section-title { font-size: 30rpx; font-weight: bold; color: #333; margin-bottom: 20rpx; }
 .section-title { font-size: 30rpx; font-weight: bold; color: #333; margin-bottom: 20rpx; }
 .meal-types { display: flex; flex-direction: row; margin-bottom: 20rpx; }
 .meal-types { display: flex; flex-direction: row; margin-bottom: 20rpx; }

+ 45 - 7
cfc-frontend/pages/body/sleep-record.vue

@@ -6,6 +6,14 @@
       <view class="nav-placeholder"></view>
       <view class="nav-placeholder"></view>
     </view>
     </view>
 
 
+    <!-- 成员选择 -->
+    <view class="member-selector-card">
+      <text class="member-label">记录人</text>
+      <picker mode="selector" :range="memberOptions" :value="selectedMemberIndex" @change="onMemberChange">
+        <view class="picker-display">{{ memberOptions[selectedMemberIndex] }}</view>
+      </picker>
+    </view>
+
     <view class="section-card">
     <view class="section-card">
       <view class="section-title">记录睡眠</view>
       <view class="section-title">记录睡眠</view>
       <view class="input-row">
       <view class="input-row">
@@ -78,11 +86,15 @@
 
 
 <script>
 <script>
 import config from '@/config.js'
 import config from '@/config.js'
+import { getChildren } from '@/utils/api.js'
 
 
 export default {
 export default {
   data() {
   data() {
     return {
     return {
       childId: null,
       childId: null,
+      selectedMemberIndex: 0,
+      memberOptions: ['我自己'],
+      memberIds: [],
       sleepTime: '',
       sleepTime: '',
       wakeTime: '',
       wakeTime: '',
       quality: '',
       quality: '',
@@ -101,9 +113,28 @@ export default {
   },
   },
   onLoad: function(options) {
   onLoad: function(options) {
     this.childId = options.childId ? parseInt(options.childId) : null
     this.childId = options.childId ? parseInt(options.childId) : null
+    this.loadChildren()
     if (this.childId) this.loadHistory()
     if (this.childId) this.loadHistory()
   },
   },
   methods: {
   methods: {
+    loadChildren: function() {
+      var self = this
+      getChildren().then(function(res) {
+        if (res.code === 200 && res.data && res.data.length > 0) {
+          self.memberIds = res.data.map(function(c) { return c.childId })
+          self.memberOptions = ['我自己'].concat(res.data.map(function(c) { return c.name || c.nickname || '孩子' }))
+        }
+      }).catch(function() {})
+    },
+    onMemberChange: function(e) {
+      this.selectedMemberIndex = parseInt(e.detail.value)
+      if (this.selectedMemberIndex === 0) {
+        this.childId = null
+      } else {
+        this.childId = this.memberIds[this.selectedMemberIndex - 1]
+      }
+      if (this.childId) this.loadHistory()
+    },
     onSleepTimeChange: function(e) { this.sleepTime = e.detail.value },
     onSleepTimeChange: function(e) { this.sleepTime = e.detail.value },
     onWakeTimeChange: function(e) { this.wakeTime = e.detail.value },
     onWakeTimeChange: function(e) { this.wakeTime = e.detail.value },
     getQualityEmoji: function(score) {
     getQualityEmoji: function(score) {
@@ -118,15 +149,18 @@ export default {
         uni.showToast({ title: '请选择睡眠时间', icon: 'none' })
         uni.showToast({ title: '请选择睡眠时间', icon: 'none' })
         return
         return
       }
       }
-      uni.showLoading({ title: '保存中...' })
-      healthRequest('/api/health/sleep/create', {
-        childId: this.childId,
-        memberId: this.childId,
+      var data = {
         sleepTime: this.sleepTime,
         sleepTime: this.sleepTime,
         wakeTime: this.wakeTime,
         wakeTime: this.wakeTime,
         qualityScore: this.quality ? { excellent: 9, good: 7, fair: 5, poor: 2 }[this.quality] : null,
         qualityScore: this.quality ? { excellent: 9, good: 7, fair: 5, poor: 2 }[this.quality] : null,
         remark: this.remark
         remark: this.remark
-      }).then(function() {
+      }
+      if (this.childId) {
+        data.childId = this.childId
+        data.memberId = this.childId
+      }
+      uni.showLoading({ title: '保存中...' })
+      healthRequest('/api/health/sleep/create', data).then(function() {
         uni.hideLoading()
         uni.hideLoading()
         uni.showToast({ title: '保存成功', icon: 'success' })
         uni.showToast({ title: '保存成功', icon: 'success' })
         this.sleepTime = ''
         this.sleepTime = ''
@@ -141,14 +175,16 @@ export default {
     },
     },
     loadHistory: function() {
     loadHistory: function() {
       var self = this
       var self = this
-      healthRequest('/api/health/sleep/list', { childId: this.childId }).then(function(res) {
+      var params = {}
+      if (this.childId) params.childId = this.childId
+      healthRequest('/api/health/sleep/list', params).then(function(res) {
         if (res.data) {
         if (res.data) {
           self.sleepHistory = res.data.list || res.data
           self.sleepHistory = res.data.list || res.data
           self.avgDuration = res.data.avgDuration || null
           self.avgDuration = res.data.avgDuration || null
           self.avgQuality = res.data.avgQuality || null
           self.avgQuality = res.data.avgQuality || null
           self.totalRecords = res.data.totalRecords || self.sleepHistory.length
           self.totalRecords = res.data.totalRecords || self.sleepHistory.length
         }
         }
-      }).catch(function() {})
+      }).catch(function() { self.sleepHistory = [] })
     },
     },
     goBack: function() { uni.navigateBack() }
     goBack: function() { uni.navigateBack() }
   }
   }
@@ -175,6 +211,8 @@ function healthRequest(url, data) {
 .nav-back { font-size: 32rpx; color: #5B9BD5; }
 .nav-back { font-size: 32rpx; color: #5B9BD5; }
 .nav-title { font-size: 32rpx; font-weight: bold; color: #333; }
 .nav-title { font-size: 32rpx; font-weight: bold; color: #333; }
 .nav-placeholder { width: 80rpx; }
 .nav-placeholder { width: 80rpx; }
+.member-selector-card { margin: 20rpx 30rpx; background: #fff; border-radius: 20rpx; padding: 20rpx 24rpx; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); display: flex; flex-direction: row; align-items: center; justify-content: space-between; }
+.member-label { font-size: 28rpx; color: #666; }
 .section-card { margin: 20rpx 30rpx; background: #fff; border-radius: 20rpx; padding: 24rpx; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); }
 .section-card { margin: 20rpx 30rpx; background: #fff; border-radius: 20rpx; padding: 24rpx; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); }
 .section-title { font-size: 30rpx; font-weight: bold; color: #333; margin-bottom: 20rpx; }
 .section-title { font-size: 30rpx; font-weight: bold; color: #333; margin-bottom: 20rpx; }
 .input-row { display: flex; flex-direction: row; align-items: center; margin-bottom: 16rpx; }
 .input-row { display: flex; flex-direction: row; align-items: center; margin-bottom: 16rpx; }

+ 8 - 1
cfc-frontend/pages/discover-detail/activity-detail/activity-detail.vue

@@ -105,8 +105,9 @@
             <text>取消报名</text>
             <text>取消报名</text>
           </button>
           </button>
           
           
-          <!-- 签到按钮 -->
+          <!-- 签到按钮(无需报名时始终显示;需报名时仅审核通过后显示) -->
           <button
           <button
+            v-if="!activity.requireRegistration || registrationStatus === 'approved'"
             class="btn-checkin"
             class="btn-checkin"
             :class="{ 'btn-checkin-done': checkinDone, 'btn-checkin-loading': checkinLoading }"
             :class="{ 'btn-checkin-done': checkinDone, 'btn-checkin-loading': checkinLoading }"
             :disabled="checkinLoading || checkinDone"
             :disabled="checkinLoading || checkinDone"
@@ -231,6 +232,12 @@ export default {
       var self = this
       var self = this
       if (self.checkinLoading || self.checkinDone) return
       if (self.checkinLoading || self.checkinDone) return
 
 
+      // 需报名的活动:未审核通过不允许签到
+      if (self.activity.requireRegistration === 1 && self.registrationStatus !== 'approved') {
+        uni.showToast({ title: '请先报名活动', icon: 'none' })
+        return
+      }
+
       var childId = uni.getStorageSync('currentChildId')
       var childId = uni.getStorageSync('currentChildId')
       if (!childId) {
       if (!childId) {
         uni.showToast({ title: '请选择孩子身份', icon: 'none' })
         uni.showToast({ title: '请选择孩子身份', icon: 'none' })

+ 230 - 88
cfc-frontend/pages/health/meditation-index.vue

@@ -4,81 +4,55 @@
       <view class="nav-back" @tap="goBack">
       <view class="nav-back" @tap="goBack">
         <text class="back-text">‹ 返回</text>
         <text class="back-text">‹ 返回</text>
       </view>
       </view>
-      <text class="nav-title">冥想</text>
+      <text class="nav-title">冥想记录</text>
+    </view>
+
+    <!-- 成员选择 -->
+    <view class="member-selector-card">
+      <text class="member-label">记录人</text>
+      <picker mode="selector" :range="memberOptions" :value="selectedMemberIndex" @change="onMemberChange">
+        <view class="picker-display">{{ memberOptions[selectedMemberIndex] }}</view>
+      </picker>
     </view>
     </view>
 
 
     <scroll-view class="content" scroll-y>
     <scroll-view class="content" scroll-y>
+      <!-- 记录表单 -->
       <view class="card">
       <view class="card">
         <view class="card-accent"></view>
         <view class="card-accent"></view>
         <view class="card-body">
         <view class="card-body">
-          <text class="card-title">冥想的好处</text>
-          <view class="info-list">
-            <view class="info-item">
-              <text class="info-age">减轻压力</text>
-              <text class="info-desc">冥想能降低皮质醇水平,缓解焦虑和紧张,让身心回归平静状态。</text>
-            </view>
-            <view class="info-item">
-              <text class="info-age">提升专注力</text>
-              <text class="info-desc">定期冥想训练能增强注意力持续时间,改善学习和工作效率。</text>
-            </view>
-            <view class="info-item">
-              <text class="info-age">情绪调节</text>
-              <text class="info-desc">培养对自己情绪的觉察能力,减少情绪波动,增强心理韧性。</text>
-            </view>
-            <view class="info-item">
-              <text class="info-age">改善睡眠</text>
-              <text class="info-desc">睡前冥想帮助放松身心,改善入睡困难和睡眠质量。</text>
+          <text class="card-title">记录冥想</text>
+          <view class="form-item">
+            <text class="form-label">冥想类型</text>
+            <view class="type-options">
+              <view class="type-chip" v-for="t in meditationTypes" :key="t.value"
+                :class="{ 'type-active': meditationType === t.value }"
+                @click="meditationType = t.value">{{ t.label }}</view>
             </view>
             </view>
           </view>
           </view>
-        </view>
-      </view>
-
-      <view class="card">
-        <view class="card-accent"></view>
-        <view class="card-body">
-          <text class="card-title">入门冥想方法</text>
-          <view class="info-list">
-            <view class="info-item">
-              <text class="info-age">呼吸觉察</text>
-              <text class="info-desc">专注于自己的呼吸,感受气息进出鼻腔和腹部的起伏,思绪游离时温和地带回呼吸上。</text>
-            </view>
-            <view class="info-item">
-              <text class="info-age">身体扫描</text>
-              <text class="info-desc">从头顶到脚趾,依次关注身体各个部位,觉察感受而不评判,释放紧张。</text>
-            </view>
-            <view class="info-item">
-              <text class="info-age">慈心冥想</text>
-              <text class="info-desc">在心中默念祝福语,将善意和温暖从自己扩展到家人、朋友乃至所有人。</text>
-            </view>
-            <view class="info-item">
-              <text class="info-age">行走冥想</text>
-              <text class="info-desc">在缓慢行走中关注脚步的每一个动作,感受脚底与地面的接触,动静结合。</text>
-            </view>
+          <view class="form-item">
+            <text class="form-label">时长(分钟)</text>
+            <input class="form-input" v-model="duration" type="number" placeholder="输入冥想时长" />
           </view>
           </view>
+          <view class="form-item">
+            <text class="form-label">感受备注</text>
+            <textarea class="form-textarea" v-model="remark" placeholder="记录当下的感受(可选)" maxlength="200" />
+          </view>
+          <view class="submit-btn" @click="submitMeditation">保存记录</view>
         </view>
         </view>
       </view>
       </view>
 
 
+      <!-- 历史记录 -->
       <view class="card">
       <view class="card">
         <view class="card-accent"></view>
         <view class="card-accent"></view>
         <view class="card-body">
         <view class="card-body">
-          <text class="card-title">亲子冥想</text>
-          <view class="info-list">
-            <view class="info-item">
-              <text class="info-age">短时开始</text>
-              <text class="info-desc">从每次 3-5 分钟开始,逐渐延长,保持轻松有趣,不要强迫孩子。</text>
-            </view>
-            <view class="info-item">
-              <text class="info-age">引导想象</text>
-              <text class="info-desc">用生动的故事引导孩子想象,如在森林中散步、躺在云朵上,激发好奇心。</text>
-            </view>
-            <view class="info-item">
-              <text class="info-age">融入游戏</text>
-              <text class="info-desc">用趣味方式练习,如"气球呼吸"(吸气鼓腹像气球)、"小火山"(呼气释放能量)。</text>
-            </view>
-            <view class="info-item">
-              <text class="info-age">睡前实践</text>
-              <text class="info-desc">将冥想融入睡前 routine,帮助孩子安静下来,提升睡眠质量和安全感。</text>
+          <text class="card-title">历史记录</text>
+          <view class="history-list">
+            <view class="history-item" v-for="item in history" :key="item.id">
+              <text class="history-type">{{ getTypeLabel(item.meditationType) }}</text>
+              <text class="history-duration">{{ item.durationMinutes }} 分钟</text>
+              <text class="history-date">{{ item.createdAt ? item.createdAt.substring(0,10) : '' }}</text>
             </view>
             </view>
+            <view class="history-empty" v-if="history.length === 0"><text>暂无冥想记录</text></view>
           </view>
           </view>
         </view>
         </view>
       </view>
       </view>
@@ -87,16 +61,109 @@
 </template>
 </template>
 
 
 <script>
 <script>
+import config from '@/config.js'
+import { getChildren } from '@/utils/api.js'
+
 export default {
 export default {
   data() {
   data() {
-    return {}
+    return {
+      childId: null,
+      selectedMemberIndex: 0,
+      memberOptions: ['我自己'],
+      memberIds: [],
+      meditationType: 'breathing',
+      duration: '',
+      remark: '',
+      history: [],
+      meditationTypes: [
+        { label: '呼吸觉察', value: 'breathing' },
+        { label: '身体扫描', value: 'body_scan' },
+        { label: '慈心冥想', value: 'loving_kindness' },
+        { label: '行走冥想', value: 'walking' },
+        { label: '正念观察', value: 'mindful_observing' }
+      ]
+    }
+  },
+  onLoad: function() {
+    this.loadChildren()
+    this.loadHistory()
   },
   },
   methods: {
   methods: {
+    loadChildren: function() {
+      var self = this
+      getChildren().then(function(res) {
+        if (res.code === 200 && res.data && res.data.length > 0) {
+          self.memberIds = res.data.map(function(c) { return c.childId })
+          self.memberOptions = ['我自己'].concat(res.data.map(function(c) { return c.name || c.nickname || '孩子' }))
+        }
+      }).catch(function() {})
+    },
+    onMemberChange: function(e) {
+      this.selectedMemberIndex = parseInt(e.detail.value)
+      if (this.selectedMemberIndex === 0) {
+        this.childId = null
+      } else {
+        this.childId = this.memberIds[this.selectedMemberIndex - 1]
+      }
+      this.loadHistory()
+    },
+    submitMeditation: function() {
+      if (!this.duration || parseInt(this.duration) <= 0) {
+        uni.showToast({ title: '请输入有效时长', icon: 'none' })
+        return
+      }
+      var data = {
+        meditationType: this.meditationType,
+        durationMinutes: parseInt(this.duration),
+        remark: this.remark
+      }
+      if (this.childId) {
+        data.childId = this.childId
+        data.memberId = this.childId
+      }
+      uni.showLoading({ title: '保存中...' })
+      healthRequest('/api/health/meditation/create', data).then(function() {
+        uni.hideLoading()
+        uni.showToast({ title: '保存成功', icon: 'success' })
+        this.duration = ''
+        this.remark = ''
+        this.loadHistory()
+      }.bind(this)).catch(function() {
+        uni.hideLoading()
+        uni.showToast({ title: '保存失败', icon: 'none' })
+      })
+    },
+    loadHistory: function() {
+      var self = this
+      var params = {}
+      if (this.childId) params.childId = this.childId
+      healthRequest('/api/health/meditation/list', params).then(function(res) {
+        if (res.data) self.history = res.data
+      }).catch(function() { self.history = [] })
+    },
+    getTypeLabel: function(type) {
+      var map = { breathing: '呼吸觉察', body_scan: '身体扫描', loving_kindness: '慈心冥想', walking: '行走冥想', mindful_observing: '正念观察' }
+      return map[type] || type
+    },
     goBack: function() {
     goBack: function() {
       uni.navigateBack()
       uni.navigateBack()
     }
     }
   }
   }
 }
 }
+
+function healthRequest(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) }
+    })
+  })
+}
 </script>
 </script>
 
 
 <style scoped>
 <style scoped>
@@ -106,7 +173,6 @@ export default {
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
 }
 }
-
 .nav-bar {
 .nav-bar {
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
@@ -117,7 +183,6 @@ export default {
   position: relative;
   position: relative;
   border-bottom: 1rpx solid #f0f0f0;
   border-bottom: 1rpx solid #f0f0f0;
 }
 }
-
 .nav-back {
 .nav-back {
   position: absolute;
   position: absolute;
   left: 30rpx;
   left: 30rpx;
@@ -125,23 +190,39 @@ export default {
   transform: translateY(-50%);
   transform: translateY(-50%);
   padding: 10rpx;
   padding: 10rpx;
 }
 }
-
 .back-text {
 .back-text {
   font-size: 28rpx;
   font-size: 28rpx;
   color: #333;
   color: #333;
 }
 }
-
 .nav-title {
 .nav-title {
   font-size: 32rpx;
   font-size: 32rpx;
   font-weight: 600;
   font-weight: 600;
   color: #333;
   color: #333;
 }
 }
-
 .content {
 .content {
   flex: 1;
   flex: 1;
   padding: 30rpx;
   padding: 30rpx;
 }
 }
-
+.member-selector-card {
+  margin: 0 30rpx 20rpx;
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 20rpx 24rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+}
+.member-label {
+  font-size: 28rpx;
+  color: #666;
+}
+.picker-display {
+  font-size: 28rpx;
+  color: #333;
+  font-weight: 500;
+}
 .card {
 .card {
   display: flex;
   display: flex;
   background-color: #FFFFFF;
   background-color: #FFFFFF;
@@ -150,18 +231,15 @@ export default {
   box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
   box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
   overflow: hidden;
   overflow: hidden;
 }
 }
-
 .card-accent {
 .card-accent {
   width: 8rpx;
   width: 8rpx;
-  background-color: #FF8C42;
+  background-color: #6366F1;
   flex-shrink: 0;
   flex-shrink: 0;
 }
 }
-
 .card-body {
 .card-body {
   flex: 1;
   flex: 1;
   padding: 30rpx;
   padding: 30rpx;
 }
 }
-
 .card-title {
 .card-title {
   font-size: 30rpx;
   font-size: 30rpx;
   font-weight: 600;
   font-weight: 600;
@@ -169,30 +247,94 @@ export default {
   margin-bottom: 24rpx;
   margin-bottom: 24rpx;
   display: block;
   display: block;
 }
 }
-
-.info-list {
+.form-item {
+  margin-bottom: 20rpx;
+}
+.form-label {
+  font-size: 26rpx;
+  color: #666;
+  display: block;
+  margin-bottom: 12rpx;
+}
+.type-options {
   display: flex;
   display: flex;
-  flex-direction: column;
-  gap: 20rpx;
+  flex-direction: row;
+  flex-wrap: wrap;
 }
 }
-
-.info-item {
+.type-chip {
+  padding: 10rpx 24rpx;
+  border-radius: 24rpx;
+  font-size: 24rpx;
+  color: #666;
+  background: #f0f0f0;
+  margin-right: 16rpx;
+  margin-bottom: 12rpx;
+}
+.type-active {
+  background: #EDE9FE;
+  color: #6366F1;
+  font-weight: 500;
+}
+.form-input {
+  width: 100%;
+  box-sizing: border-box;
+  height: 80rpx;
+  padding: 0 24rpx;
+  border: 2rpx solid #e5e5e5;
+  border-radius: 12rpx;
+  font-size: 28rpx;
+  background: #fafafa;
+}
+.form-textarea {
+  width: 100%;
+  box-sizing: border-box;
+  height: 160rpx;
+  padding: 16rpx 24rpx;
+  border: 2rpx solid #e5e5e5;
+  border-radius: 12rpx;
+  font-size: 28rpx;
+  background: #fafafa;
+  resize: none;
+}
+.submit-btn {
+  background: linear-gradient(135deg, #6366F1, #818CF8);
+  color: #fff;
+  font-size: 32rpx;
+  font-weight: bold;
+  text-align: center;
+  padding: 22rpx;
+  border-radius: 40rpx;
+  margin-top: 16rpx;
+}
+.history-list {
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
-  gap: 6rpx;
 }
 }
-
-.info-age {
-  font-size: 26rpx;
-  font-weight: 500;
-  color: #FF8C42;
-  display: block;
+.history-item {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  padding: 14rpx 0;
+  border-bottom: 1rpx solid #f5f5f5;
 }
 }
-
-.info-desc {
+.history-type {
+  font-size: 28rpx;
+  color: #333;
+  flex: 1;
+}
+.history-duration {
   font-size: 24rpx;
   font-size: 24rpx;
-  color: #666;
-  line-height: 1.6;
-  display: block;
+  color: #6366F1;
+  margin-right: 16rpx;
+}
+.history-date {
+  font-size: 22rpx;
+  color: #999;
+}
+.history-empty {
+  text-align: center;
+  padding: 30rpx;
+  color: #999;
+  font-size: 26rpx;
 }
 }
 </style>
 </style>

+ 1 - 1
cfc-frontend/pages/health/nutrition-profile.vue

@@ -45,7 +45,7 @@
     <view class="empty-state" v-if="!scores && !loading">
     <view class="empty-state" v-if="!scores && !loading">
       <text class="empty-icon">📋</text>
       <text class="empty-icon">📋</text>
       <text class="empty-title">暂无健康报告</text>
       <text class="empty-title">暂无健康报告</text>
-      <text class="empty-desc">完成首次健康检测后,这里将展示孩子的营养档案</text>
+      <text class="empty-desc">完成首次健康检测后,这里将展示您的营养档案</text>
       <view class="empty-btn" @click="goToAssessment">
       <view class="empty-btn" @click="goToAssessment">
         <text class="empty-btn-text">预约健康检测</text>
         <text class="empty-btn-text">预约健康检测</text>
       </view>
       </view>

+ 28 - 8
cfc-frontend/pages/health/tongue-index.vue

@@ -18,9 +18,13 @@
       </view>
       </view>
     </view>
     </view>
 
 
-    <!-- Step 1: 选成员 -->
+    <!-- Step 1: 选成员(可选,默认我自己) -->
     <view class="step-content" v-if="step === 1">
     <view class="step-content" v-if="step === 1">
-      <view class="section-title">选择家庭成员</view>
+      <view class="section-title">选择家庭成员(可选)</view>
+      <view class="member-item self-first" @click="selectSelf">
+        <text class="member-avatar self-avatar">我</text>
+        <text class="member-name">我自己</text>
+      </view>
       <view class="member-list">
       <view class="member-list">
         <view class="member-item"
         <view class="member-item"
           v-for="m in familyMembers"
           v-for="m in familyMembers"
@@ -31,7 +35,7 @@
           <text class="member-name">{{ m.name }}</text>
           <text class="member-name">{{ m.name }}</text>
         </view>
         </view>
       </view>
       </view>
-      <button class="btn-primary" @click="step = 2" :disabled="!selectedMember">下一步</button>
+      <button class="btn-primary" @click="step = 2" :disabled="!canProceed">下一步</button>
     </view>
     </view>
 
 
     <!-- Step 2: 拍照 -->
     <!-- Step 2: 拍照 -->
@@ -89,6 +93,7 @@ export default {
       step: 1,
       step: 1,
       familyMembers: [],
       familyMembers: [],
       selectedMember: null,
       selectedMember: null,
+      selectedSelf: true,
       photoPath: null,
       photoPath: null,
       uploading: false,
       uploading: false,
       saving: false,
       saving: false,
@@ -100,10 +105,19 @@ export default {
       indicators: []
       indicators: []
     }
     }
   },
   },
+  computed: {
+    canProceed: function() {
+      return this.selectedMember || this.selectedSelf
+    }
+  },
   onLoad: function() {
   onLoad: function() {
     this.loadFamilyMembers()
     this.loadFamilyMembers()
   },
   },
   methods: {
   methods: {
+    selectSelf: function() {
+      this.selectedSelf = true
+      this.selectedMember = null
+    },
     loadFamilyMembers: function() {
     loadFamilyMembers: function() {
       var members = uni.getStorageSync('familyChildren') || []
       var members = uni.getStorageSync('familyChildren') || []
       if (members.length > 0) {
       if (members.length > 0) {
@@ -128,8 +142,9 @@ export default {
       this.uploading = true
       this.uploading = true
 
 
       var self = this
       var self = this
+      var memberId = this.selectedMember ? this.selectedMember.id : ''
       uni.uploadFile({
       uni.uploadFile({
-        url: getApp().globalData.baseUrl + '/api/health/report/parse-preview?type=tongue&memberId=' + this.selectedMember.id,
+        url: getApp().globalData.baseUrl + '/api/health/report/parse-preview?type=tongue' + (memberId ? '&memberId=' + memberId : ''),
         filePath: this.photoPath,
         filePath: this.photoPath,
         name: 'file',
         name: 'file',
         success: function(res) {
         success: function(res) {
@@ -174,11 +189,13 @@ export default {
       var modifiedIndicators = this.indicators.map(function(ind) {
       var modifiedIndicators = this.indicators.map(function(ind) {
         return { code: ind.code, value: ind.value }
         return { code: ind.code, value: ind.value }
       })
       })
+      var memberId = this.selectedMember ? this.selectedMember.id : ''
       uni.request({
       uni.request({
         url: getApp().globalData.baseUrl + '/api/health/report/confirm?type=tongue',
         url: getApp().globalData.baseUrl + '/api/health/report/confirm?type=tongue',
         method: 'POST',
         method: 'POST',
         data: {
         data: {
           recordId: this.tongueResult.recordId,
           recordId: this.tongueResult.recordId,
+          memberId: memberId || undefined,
           indicators: modifiedIndicators
           indicators: modifiedIndicators
         },
         },
         success: function(res) {
         success: function(res) {
@@ -206,10 +223,11 @@ export default {
         content: '确定放弃本次舌诊分析吗?',
         content: '确定放弃本次舌诊分析吗?',
         success: function(res) {
         success: function(res) {
           if (res.confirm) {
           if (res.confirm) {
-            uni.request({
-              url: getApp().globalData.baseUrl + '/api/health/report/discard?type=tongue',
-              method: 'POST',
-              data: { recordId: self.tongueResult.recordId }
+      var memberId = this.selectedMember ? this.selectedMember.id : ''
+      uni.request({
+        url: getApp().globalData.baseUrl + '/api/health/report/discard?type=tongue',
+        method: 'POST',
+        data: { recordId: self.tongueResult.recordId, memberId: memberId || undefined }
             })
             })
             uni.navigateBack()
             uni.navigateBack()
           }
           }
@@ -233,6 +251,8 @@ export default {
 .step-line.active { background: #10B981; }
 .step-line.active { background: #10B981; }
 
 
 .section-title { font-size: 32rpx; font-weight: bold; color: #1E293B; margin-bottom: 24rpx; }
 .section-title { font-size: 32rpx; font-weight: bold; color: #1E293B; margin-bottom: 24rpx; }
+.self-first { width: 140rpx; height: 160rpx; display: flex; flex-direction: column; align-items: center; justify-content: center; background: #FFF7ED; border-radius: 16rpx; border: 2rpx solid #F97316; margin-bottom: 20rpx; }
+.self-avatar { width: 64rpx; height: 64rpx; border-radius: 50%; background: #FED7AA; text-align: center; line-height: 64rpx; font-size: 28rpx; color: #9A3412; margin-bottom: 8rpx; }
 .member-list { display: flex; flex-wrap: wrap; gap: 20rpx; margin-bottom: 40rpx; }
 .member-list { display: flex; flex-wrap: wrap; gap: 20rpx; margin-bottom: 40rpx; }
 .member-item { width: 140rpx; height: 160rpx; display: flex; flex-direction: column; align-items: center; justify-content: center; background: #fff; border-radius: 16rpx; border: 2rpx solid #E2E8F0; }
 .member-item { width: 140rpx; height: 160rpx; display: flex; flex-direction: column; align-items: center; justify-content: center; background: #fff; border-radius: 16rpx; border: 2rpx solid #E2E8F0; }
 .member-item.selected { border-color: #F97316; background: #FFF7ED; }
 .member-item.selected { border-color: #F97316; background: #FFF7ED; }

+ 142 - 79
cfc-frontend/pages/index/index.vue

@@ -1,26 +1,41 @@
 <template>
 <template>
 <view>
 <view>
-  <!-- 首次进入的 splash loading 蒙板 — 覆盖首页直到数据加载完毕 -->
+  <!-- 冷启动 splash overlay — 覆盖首页直到数据加载完毕(tab 切回时不显示) -->
   <view class="splash-overlay" :class="{'fade-out': splashOverlayFading}" v-if="splashOverlay || splashOverlayFading">
   <view class="splash-overlay" :class="{'fade-out': splashOverlayFading}" v-if="splashOverlay || splashOverlayFading">
     <view class="splash-overlay-bg">
     <view class="splash-overlay-bg">
-      <view class="splash-overlay-circle c1"></view>
-      <view class="splash-overlay-circle c2"></view>
-      <view class="splash-overlay-circle c3"></view>
+      <view class="bg-circle bg-circle-1"></view>
+      <view class="bg-circle bg-circle-2"></view>
+      <view class="bg-circle bg-circle-3"></view>
     </view>
     </view>
     <view class="splash-overlay-body">
     <view class="splash-overlay-body">
-      <view class="splash-overlay-logo-wrap">
-        <image class="splash-overlay-logo" src="/static/logo.png" mode="aspectFit" />
+      <view class="splash-logo-wrap">
+        <image class="splash-logo-img" src="/static/logo.png" mode="aspectFit" />
       </view>
       </view>
-      <text class="splash-overlay-brand">浠艾福</text>
-      <text class="splash-overlay-en">Care Family</text>
-      <view class="splash-overlay-dims">
-        <view class="splash-overlay-dim"><text class="splash-overlay-dim-icon">🌏</text><text class="splash-overlay-dim-label">身</text></view>
-        <view class="splash-overlay-dim"><text class="splash-overlay-dim-icon">🔥</text><text class="splash-overlay-dim-label">心</text></view>
-        <view class="splash-overlay-dim"><text class="splash-overlay-dim-icon">⚡</text><text class="splash-overlay-dim-label">智</text></view>
-        <view class="splash-overlay-dim"><text class="splash-overlay-dim-icon">🌿</text><text class="splash-overlay-dim-label">行</text></view>
-        <view class="splash-overlay-dim"><text class="splash-overlay-dim-icon">💧</text><text class="splash-overlay-dim-label">富</text></view>
+      <text class="splash-brand-name">浠艾福</text>
+      <text class="splash-brand-en">Care Family</text>
+      <view class="splash-dims-row">
+        <view class="splash-dim-item">
+          <view class="splash-dim-circle" style="background:rgba(255,140,66,0.3);"><text class="splash-dim-icon">🌏</text></view>
+          <text class="splash-dim-label">身</text>
+        </view>
+        <view class="splash-dim-item">
+          <view class="splash-dim-circle" style="background:rgba(255,107,157,0.3);"><text class="splash-dim-icon">🔥</text></view>
+          <text class="splash-dim-label">心</text>
+        </view>
+        <view class="splash-dim-item">
+          <view class="splash-dim-circle" style="background:rgba(99,102,241,0.3);"><text class="splash-dim-icon">⚡</text></view>
+          <text class="splash-dim-label">智</text>
+        </view>
+        <view class="splash-dim-item">
+          <view class="splash-dim-circle" style="background:rgba(16,185,129,0.3);"><text class="splash-dim-icon">🌿</text></view>
+          <text class="splash-dim-label">行</text>
+        </view>
+        <view class="splash-dim-item">
+          <view class="splash-dim-circle" style="background:rgba(245,158,11,0.3);"><text class="splash-dim-icon">💧</text></view>
+          <text class="splash-dim-label">富</text>
+        </view>
       </view>
       </view>
-      <text class="splash-overlay-tagline">给全家的一站式幸福提案</text>
+      <text class="splash-tagline">给全家的一站式幸福提案</text>
       <view class="splash-overlay-loading">
       <view class="splash-overlay-loading">
         <view class="splash-overlay-dot"></view>
         <view class="splash-overlay-dot"></view>
         <view class="splash-overlay-dot"></view>
         <view class="splash-overlay-dot"></view>
@@ -228,13 +243,15 @@ export default {
     if (_token) {
     if (_token) {
       _currentRole = uni.getStorageSync('currentRole') || uni.getStorageSync('role') || ''
       _currentRole = uni.getStorageSync('currentRole') || uni.getStorageSync('role') || ''
     }
     }
+    // 冷启动标记(由 App.vue onLaunch 设置,仅在冷启动时存在)
+    var _isFirstLaunch = (uni.getStorageSync('_showSplashOverlay') === '1')
     return {
     return {
       currentRole: _currentRole,
       currentRole: _currentRole,
-      // TabTransition 始终默认 true,确保首次渲染即出现
+      // TabTransition 始终默认 true,确保首次渲染即出现;冷启动时被 splash overlay 覆盖不可见
       showTabTransition: true,
       showTabTransition: true,
 
 
-      // splash loading overlay 已移除,不再展示
-      splashOverlay: false,
+      // splash loading overlay(冷启动时展示,数据加载完关闭)
+      splashOverlay: _isFirstLaunch,
       splashOverlayFading: false,
       splashOverlayFading: false,
       _splashReady: false,
       _splashReady: false,
 
 
@@ -258,10 +275,28 @@ export default {
     this._doInit(options)
     this._doInit(options)
   },
   },
   onShow() {
   onShow() {
-    // TabTransition 已在 data() 中为 true,确保首次渲染即出现
-    // 但组件缓存后再次切回时可能已被 _watchPageReady 置为 false,需要重置
-    this.showTabTransition = true
-    this._watchPageReady()
+    // splash overlay 已在 data() 中根据 storage 标记初始化,此处只需配置关闭条件
+    if (this.splashOverlay) {
+      uni.removeStorageSync('_showSplashOverlay')
+      this._splashLoadCount = 0
+      this._splashMinPassed = false
+
+      var self = this
+      // 最低展示 800ms
+      setTimeout(function() {
+        self._splashMinPassed = true
+        self._tryHideSplash()
+      }, 800)
+      // 最多展示 5s
+      setTimeout(function() {
+        if (self.splashOverlay) self._hideSplashOverlay()
+      }, 5000)
+    } else {
+      // tab 切回:TabTransition 已在 data() 中为 true,确保首次渲染即出现
+      // 但组件缓存后再次切回时可能已被 _watchPageReady 置为 false,需要重置
+      this.showTabTransition = true
+      this._watchPageReady()
+    }
 
 
     // 始终执行初始化,确保登录态变更后及时切换视图
     // 始终执行初始化,确保登录态变更后及时切换视图
     this._doInit()
     this._doInit()
@@ -925,13 +960,10 @@ export default {
   color: #bbb;
   color: #bbb;
 }
 }
 
 
-/* ===== Splash Loading Overlay(首次进入蒙板) ===== */
+/* ===== Splash Loading Overlay(冷启动蒙板,来自 merge splash.vue) ===== */
 .splash-overlay {
 .splash-overlay {
   position: fixed;
   position: fixed;
-  top: 0;
-  left: 0;
-  right: 0;
-  bottom: 0;
+  top: 0; left: 0; right: 0; bottom: 0;
   z-index: 9999;
   z-index: 9999;
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
@@ -945,96 +977,127 @@ export default {
   opacity: 0;
   opacity: 0;
   pointer-events: none;
   pointer-events: none;
 }
 }
-.splash-overlay-bg {
-  position: absolute;
-  top: 0;
-  left: 0;
-  right: 0;
-  bottom: 0;
-  pointer-events: none;
-}
-.splash-overlay-circle {
+
+/* ---- bg circles from splash.vue ---- */
+.splash-overlay-bg { position: absolute; top: 0; left: 0; right: 0; bottom: 0; pointer-events: none; }
+.bg-circle {
   position: absolute;
   position: absolute;
   border-radius: 50%;
   border-radius: 50%;
-  opacity: 0.15;
+  opacity: 0.12;
 }
 }
-.splash-overlay-circle.c1 {
+.bg-circle-1 {
   width: 400rpx; height: 400rpx;
   width: 400rpx; height: 400rpx;
   background: #FF8C42;
   background: #FF8C42;
-  top: -100rpx; right: -100rpx;
-  animation: splash-float 6s ease-in-out infinite;
-}
-.splash-overlay-circle.c2 {
-  width: 300rpx; height: 300rpx;
-  background: #F59E0B;
-  bottom: 100rpx; left: -80rpx;
-  animation: splash-float 8s ease-in-out infinite reverse;
+  top: -100rpx; right: -80rpx;
+  animation: splash-float-1 4s ease-in-out infinite;
 }
 }
-.splash-overlay-circle.c3 {
-  width: 200rpx; height: 200rpx;
+.bg-circle-2 {
+  width: 280rpx; height: 280rpx;
   background: #FF6B9D;
   background: #FF6B9D;
-  top: 300rpx; right: 50rpx;
-  animation: splash-float 5s ease-in-out infinite 1s;
+  bottom: 80rpx; left: -60rpx;
+  animation: splash-float-2 5s ease-in-out infinite;
 }
 }
-@keyframes splash-float {
+.bg-circle-3 {
+  width: 180rpx; height: 180rpx;
+  background: #6366F1;
+  top: 50%; right: -40rpx;
+  animation: splash-float-3 6s ease-in-out infinite;
+}
+@keyframes splash-float-1 {
+  0%, 100% { transform: translateY(0) scale(1); }
+  50% { transform: translateY(-30rpx) scale(1.05); }
+}
+@keyframes splash-float-2 {
   0%, 100% { transform: translateY(0) scale(1); }
   0%, 100% { transform: translateY(0) scale(1); }
-  50% { transform: translateY(-20rpx) scale(1.05); }
+  50% { transform: translateY(20rpx) scale(1.08); }
+}
+@keyframes splash-float-3 {
+  0%, 100% { transform: translateY(0) rotate(0deg); }
+  50% { transform: translateY(-20rpx) rotate(10deg); }
 }
 }
 
 
+/* ---- body fade-in ---- */
 .splash-overlay-body {
 .splash-overlay-body {
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
   align-items: center;
   align-items: center;
-  animation: splash-overlay-fade-in 0.8s ease forwards;
+  animation: splash-body-in 0.8s cubic-bezier(0.16,1,0.3,1) forwards;
 }
 }
-@keyframes splash-overlay-fade-in {
-  from { opacity: 0; transform: translateY(30rpx); }
-  to   { opacity: 1; transform: translateY(0); }
+@keyframes splash-body-in {
+  from { opacity: 0; transform: translateY(40rpx) scale(0.96); }
+  to   { opacity: 1; transform: translateY(0) scale(1); }
 }
 }
 
 
-.splash-overlay-logo-wrap {
+/* ---- logo ---- */
+.splash-logo-wrap {
   width: 160rpx; height: 160rpx;
   width: 160rpx; height: 160rpx;
   border-radius: 40rpx;
   border-radius: 40rpx;
   background: rgba(255,255,255,0.95);
   background: rgba(255,255,255,0.95);
   display: flex; align-items: center; justify-content: center;
   display: flex; align-items: center; justify-content: center;
-  margin-bottom: 24rpx;
+  margin-bottom: 16rpx;
   box-shadow: 0 8rpx 32rpx rgba(249,115,22,0.25);
   box-shadow: 0 8rpx 32rpx rgba(249,115,22,0.25);
+  animation: splash-logo-pulse 2.5s ease-in-out infinite;
 }
 }
-.splash-overlay-logo {
-  width: 100rpx; height: 100rpx;
+@keyframes splash-logo-pulse {
+  0%, 100% { box-shadow: 0 8rpx 32rpx rgba(249,115,22,0.25); }
+  50% { box-shadow: 0 8rpx 48rpx rgba(249,115,22,0.45); }
 }
 }
+.splash-logo-img { width: 100rpx; height: 100rpx; }
 
 
-.splash-overlay-brand {
-  font-size: 60rpx; font-weight: 700;
+/* ---- brand ---- */
+.splash-brand-name {
+  font-size: 64rpx; font-weight: 800;
   color: #7C2D12;
   color: #7C2D12;
-  letter-spacing: 8rpx; margin-bottom: 4rpx;
-  text-shadow: 0 2rpx 12rpx rgba(0,0,0,0.1);
+  letter-spacing: 10rpx;
+  margin-bottom: 2rpx;
 }
 }
-.splash-overlay-en {
-  font-size: 22rpx;
-  color: rgba(124,45,18,0.6);
-  letter-spacing: 4rpx; margin-bottom: 20rpx;
+.splash-brand-en {
+  font-size: 20rpx;
+  color: rgba(124,45,18,0.5);
+  letter-spacing: 6rpx;
+  margin-bottom: 30rpx;
+}
+.splash-tagline {
+  font-size: 26rpx;
+  color: rgba(124,45,18,0.7);
+  letter-spacing: 4rpx;
+  margin-top: 24rpx;
 }
 }
 
 
-.splash-overlay-dims {
+/* ---- dims row from splash.vue (rounded circles) ---- */
+.splash-dims-row {
   display: flex; flex-direction: row;
   display: flex; flex-direction: row;
   align-items: center; justify-content: center;
   align-items: center; justify-content: center;
-  gap: 20rpx; margin: 24rpx 0;
+  gap: 28rpx;
 }
 }
-.splash-overlay-dim {
+.splash-dim-item {
   display: flex; flex-direction: column;
   display: flex; flex-direction: column;
-  align-items: center; gap: 6rpx;
+  align-items: center; gap: 8rpx;
+}
+.splash-dim-circle {
+  width: 80rpx; height: 80rpx;
+  border-radius: 50%;
+  display: flex; align-items: center; justify-content: center;
+}
+.splash-dim-icon { font-size: 36rpx; line-height: 1; }
+.splash-dim-label {
+  font-size: 22rpx; font-weight: 500;
+  color: rgba(124,45,18,0.75);
+  line-height: 1;
 }
 }
-.splash-overlay-dim-icon { font-size: 36rpx; line-height: 1; }
-.splash-overlay-dim-label { font-size: 20rpx; color: rgba(124,45,18,0.7); line-height: 1; font-weight: 500; }
 
 
-.splash-overlay-tagline {
-  font-size: 28rpx;
-  color: rgba(124,45,18,0.8);
-  letter-spacing: 2rpx; font-weight: 500;
+/* ---- staggered dim entry from splash.vue ---- */
+.splash-dim-item:nth-child(1) { animation: splash-dim-in 0.5s ease 0.1s both; }
+.splash-dim-item:nth-child(2) { animation: splash-dim-in 0.5s ease 0.2s both; }
+.splash-dim-item:nth-child(3) { animation: splash-dim-in 0.5s ease 0.3s both; }
+.splash-dim-item:nth-child(4) { animation: splash-dim-in 0.5s ease 0.4s both; }
+.splash-dim-item:nth-child(5) { animation: splash-dim-in 0.5s ease 0.5s both; }
+@keyframes splash-dim-in {
+  from { opacity: 0; transform: translateY(16rpx) scale(0.8); }
+  to   { opacity: 1; transform: translateY(0) scale(1); }
 }
 }
 
 
-/* Loading dots */
+/* ---- loading dots ---- */
 .splash-overlay-loading {
 .splash-overlay-loading {
   position: absolute;
   position: absolute;
   bottom: 120rpx;
   bottom: 120rpx;

+ 42 - 65
cfc-frontend/pages/mind/index.vue

@@ -71,8 +71,8 @@
       </view>
       </view>
     </view>
     </view>
 
 
-    <!-- ===== 大五人格雷达图 ===== -->
-    <view class="emi-section" v-if="emiData">
+    <!-- ===== 大五人格雷达图(始终显示) ===== -->
+    <view class="emi-section">
       <view class="emi-card">
       <view class="emi-card">
         <view class="personality-header">
         <view class="personality-header">
           <text class="emi-card-title">大五人格</text>
           <text class="emi-card-title">大五人格</text>
@@ -80,46 +80,31 @@
             <text class="test-btn-text">测试</text>
             <text class="test-btn-text">测试</text>
           </view>
           </view>
         </view>
         </view>
-        <RadarChart
-          :dimensions="personalityDimensions"
-          :scores="personalityScores"
-          :childName="currentChildName || '孩子'"
-          fillColor="#8B5CF6"
-          gridColor="#EDE9FE"
-          labelColor="#6D28D9" />
-        <view class="emi-footer">
-          <text class="emi-overall">综合EQ分: {{ emiData.overallScore || 0 }}</text>
-          <view class="emi-link" @click="goEmotionReport">
-            <text class="emi-link-text">查看完整报告</text>
-            <text class="emi-link-arrow">&#x2192;</text>
+        <template v-if="emiData">
+          <RadarChart
+            :dimensions="personalityDimensions"
+            :scores="personalityScores"
+            :childName="currentChildName || '孩子'"
+            fillColor="#8B5CF6"
+            gridColor="#EDE9FE"
+            labelColor="#6D28D9" />
+          <view class="emi-footer">
+            <text class="emi-overall">综合EQ分: {{ emiData.overallScore || 0 }}</text>
+            <view class="emi-link" @click="goEmotionReport">
+              <text class="emi-link-text">查看完整报告</text>
+              <text class="emi-link-arrow">&#x2192;</text>
+            </view>
           </view>
           </view>
-        </view>
-      </view>
-    </view>
-
-    <!-- 无 EMI 数据占位 -->
-    <view class="placeholder-card" v-if="!emiData && !emiLoading">
-      <text class="placeholder-icon">&#x1F9E0;</text>
-      <text class="placeholder-title">心理评估</text>
-      <text class="placeholder-desc">完成 EMI 心理测评后,可在此查看详细的心理分析报告</text>
-      <view class="placeholder-btn" @click="goAssessment" v-if="isLoggedIn">预约测评</view>
-      <view class="placeholder-btn" @click="goLogin" v-else>立即登录</view>
-    </view>
-
-    <!-- 每日心理 -->
-    <view class="section" v-if="sectionVisible('daily_tip')">
-      <view class="section-header">
-        <text class="section-title">每日心理</text>
-      </view>
-      <view class="tip-card">
-        <view class="tip-left-bar"></view>
-        <view class="tip-body">
-          <text class="tip-title">{{ dailyTip.title }}</text>
-          <text class="tip-content">{{ dailyTip.content }}</text>
-          <view class="tip-footer">
-            <text class="tip-source">{{ dailyTip.source }}</text>
+        </template>
+        <template v-else-if="!emiLoading">
+          <view class="placeholder-content">
+            <text class="placeholder-icon">&#x1F9E0;</text>
+            <text class="placeholder-title">心理评估</text>
+            <text class="placeholder-desc">完成 EMI 心理测评后,可在此查看大五人格雷达图</text>
+            <view class="placeholder-btn" @click="goAssessment" v-if="isLoggedIn">预约测评</view>
+            <view class="placeholder-btn" @click="goLogin" v-else>立即登录</view>
           </view>
           </view>
-        </view>
+        </template>
       </view>
       </view>
     </view>
     </view>
 
 
@@ -276,7 +261,7 @@ import FamilyRelationGraph from '../../components/FamilyRelationGraph.vue'
 import FamilyRelationshipSection from '../../components/FamilyRelationshipSection.vue'
 import FamilyRelationshipSection from '../../components/FamilyRelationshipSection.vue'
 import PsychCrisisBanner from '../../components/PsychCrisisBanner.vue'
 import PsychCrisisBanner from '../../components/PsychCrisisBanner.vue'
 import AIFloatingAvatar from '../../components/AIFloatingAvatar.vue'
 import AIFloatingAvatar from '../../components/AIFloatingAvatar.vue'
-import { getVisibleSections, getFeaturedArticles, getEmiReport, getDailyTip, getFamilyEnergySandbox, getTodayTasksByCategory, getActivityList, getProductsByDomain, getChildren, getVisibleFamilyMembers, getEnergyOverview, getContactList, getHealthAlerts, getMilestones } from '../../utils/api.js'
+import { getVisibleSections, getFeaturedArticles, getEmiReport, getFamilyEnergySandbox, getTodayTasksByCategory, getActivityList, getProductsByDomain, getChildren, getVisibleFamilyMembers, getEnergyOverview, getContactList, getHealthAlerts, getMilestones } from '../../utils/api.js'
 import config from '../../config.js'
 import config from '../../config.js'
 
 
 export default {
 export default {
@@ -293,6 +278,13 @@ export default {
       // 心理 Tab 数据
       // 心理 Tab 数据
       emiData: null,
       emiData: null,
       emiLoading: true,
       emiLoading: true,
+      emiDimensions: [
+        { key: 'emotionScore', label: '情绪商数', color: '#FF6B35' },
+        { key: 'resilienceScore', label: '心理韧性', color: '#F97316' },
+        { key: 'stressCopingScore', label: '压力应对', color: '#FB923C' },
+        { key: 'selfAwarenessScore', label: '自我认知', color: '#FBBF24' }
+      ],
+      // 大五维度(用于雷达图)
       personalityDimensions: [
       personalityDimensions: [
         { key: 'opennessScore', label: '开放性' },
         { key: 'opennessScore', label: '开放性' },
         { key: 'conscientiousnessScore', label: '尽责性' },
         { key: 'conscientiousnessScore', label: '尽责性' },
@@ -300,17 +292,6 @@ export default {
         { key: 'agreeablenessScore', label: '宜人性' },
         { key: 'agreeablenessScore', label: '宜人性' },
         { key: 'neuroticismScore', label: '神经质' }
         { key: 'neuroticismScore', label: '神经质' }
       ],
       ],
-      dailyTip: {
-        title: '成长型思维',
-        content: '拥有成长型思维的孩子相信自己的能力可以通过努力提升,他们更愿意接受挑战,从失败中学习。作为家长,多鼓励孩子的努力而非天赋,可以帮助孩子建立成长型思维。',
-        source: '—— 卡罗尔·德韦克《终身成长》'
-      },
-      emiDimensions: [
-        { key: 'emotionScore', label: '情绪商数', color: '#FF6B35' },
-        { key: 'resilienceScore', label: '心理韧性', color: '#F97316' },
-        { key: 'stressCopingScore', label: '压力应对', color: '#FB923C' },
-        { key: 'selfAwarenessScore', label: '自我认知', color: '#FBBF24' }
-      ],
 
 
       // 功能入口
       // 功能入口
       funcList: [
       funcList: [
@@ -446,7 +427,6 @@ export default {
     }
     }
     this.loadSectionConfig()
     this.loadSectionConfig()
     this.loadFeaturedArticles()
     this.loadFeaturedArticles()
-    this.loadDailyTip()
     if (this.isLoggedIn) {
     if (this.isLoggedIn) {
       this.loadChildren()
       this.loadChildren()
       this.loadFamilyMembersVisible()
       this.loadFamilyMembersVisible()
@@ -508,16 +488,6 @@ export default {
       })
       })
     },
     },
 
 
-    // ===== 每日心理 =====
-    loadDailyTip: function() {
-      var self = this
-      getDailyTip().then(function(res) {
-        if (res.code === 200 && res.data) {
-          self.dailyTip = res.data
-        }
-      }).catch(function() {})
-    },
-
     // ===== 肠脑轴情绪指标 =====
     // ===== 肠脑轴情绪指标 =====
     loadGutData: function() {
     loadGutData: function() {
       var self = this
       var self = this
@@ -864,8 +834,7 @@ export default {
         if (self.isLoggedIn) {
         if (self.isLoggedIn) {
           return !!(self.emiData || self.energyOverview || (self.dimensionActivities && self.dimensionActivities.length > 0))
           return !!(self.emiData || self.energyOverview || (self.dimensionActivities && self.dimensionActivities.length > 0))
         }
         }
-        return !!(self.dimensionActivities && self.dimensionActivities.length > 0) ||
-               !!(self.dailyTip)
+        return !!(self.dimensionActivities && self.dimensionActivities.length > 0)
       }, function(val) {
       }, function(val) {
         if (val) {
         if (val) {
           if (unwatch) unwatch()
           if (unwatch) unwatch()
@@ -1048,6 +1017,14 @@ export default {
   margin-left: 8rpx;
   margin-left: 8rpx;
 }
 }
 
 
+/* 雷达图内的空状态 */
+.placeholder-content {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 40rpx 20rpx;
+}
+
 /* ===== 占位卡 ===== */
 /* ===== 占位卡 ===== */
 .placeholder-card {
 .placeholder-card {
   margin: 30rpx;
   margin: 30rpx;

+ 0 - 255
cfc-frontend/pages/splash/splash.vue

@@ -1,255 +0,0 @@
-<template>
-  <view class="splash-root">
-    <!-- 背景装饰 -->
-    <view class="splash-bg-decor">
-      <view class="bg-circle bg-circle-1"></view>
-      <view class="bg-circle bg-circle-2"></view>
-      <view class="bg-circle bg-circle-3"></view>
-    </view>
-
-    <view class="splash-content">
-      <!-- Logo 图标 -->
-      <view class="splash-logo-wrap">
-        <image class="splash-logo-img" src="/static/logo.png" mode="aspectFit" />
-      </view>
-
-      <!-- Brand Name -->
-      <text class="splash-brand-name">浠艾福</text>
-      <text class="splash-brand-en">Care Family</text>
-
-      <!-- 五维图标行 -->
-      <view class="splash-dims-row">
-        <view class="splash-dim-item">
-          <view class="splash-dim-circle" style="background:rgba(255,140,66,0.3);">
-            <text class="splash-dim-icon">🌏</text>
-          </view>
-          <text class="splash-dim-label">身</text>
-        </view>
-        <view class="splash-dim-item">
-          <view class="splash-dim-circle" style="background:rgba(255,107,157,0.3);">
-            <text class="splash-dim-icon">🔥</text>
-          </view>
-          <text class="splash-dim-label">心</text>
-        </view>
-        <view class="splash-dim-item">
-          <view class="splash-dim-circle" style="background:rgba(99,102,241,0.3);">
-            <text class="splash-dim-icon">⚡</text>
-          </view>
-          <text class="splash-dim-label">智</text>
-        </view>
-        <view class="splash-dim-item">
-          <view class="splash-dim-circle" style="background:rgba(16,185,129,0.3);">
-            <text class="splash-dim-icon">🌿</text>
-          </view>
-          <text class="splash-dim-label">行</text>
-        </view>
-        <view class="splash-dim-item">
-          <view class="splash-dim-circle" style="background:rgba(245,158,11,0.3);">
-            <text class="splash-dim-icon">💧</text>
-          </view>
-          <text class="splash-dim-label">富</text>
-        </view>
-      </view>
-
-      <!-- Tagline -->
-      <text class="splash-tagline">给全家的一站式幸福提案</text>
-    </view>
-  </view>
-</template>
-
-<script>
-export default {
-  name: 'SplashPage',
-  onReady() {
-    // 标记需要在首页显示 splash overlay(仅在首次进入时)
-    uni.setStorageSync('_showSplashOverlay', '1')
-    // 立即跳转首页,首页会展示 splash overlay 并加载数据
-    uni.switchTab({ url: '/pages/index/index' })
-  }
-}
-</script>
-
-<style scoped>
-.splash-root {
-  position: fixed;
-  top: 0;
-  left: 0;
-  right: 0;
-  bottom: 0;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  background: linear-gradient(160deg, #FFF7ED 0%, #FFEDD5 50%, #FED7AA 100%);
-  z-index: 9999;
-  overflow: hidden;
-}
-
-/* ===== 背景装饰 ===== */
-.splash-bg-decor {
-  position: absolute;
-  top: 0;
-  left: 0;
-  right: 0;
-  bottom: 0;
-  pointer-events: none;
-}
-
-.bg-circle {
-  position: absolute;
-  border-radius: 50%;
-  opacity: 0.15;
-}
-
-.bg-circle-1 {
-  width: 400rpx;
-  height: 400rpx;
-  background: #FF8C42;
-  top: -100rpx;
-  right: -100rpx;
-  animation: float 6s ease-in-out infinite;
-}
-
-.bg-circle-2 {
-  width: 300rpx;
-  height: 300rpx;
-  background: #F59E0B;
-  bottom: 100rpx;
-  left: -80rpx;
-  animation: float 8s ease-in-out infinite reverse;
-}
-
-.bg-circle-3 {
-  width: 200rpx;
-  height: 200rpx;
-  background: #FF6B9D;
-  top: 300rpx;
-  right: 50rpx;
-  animation: float 5s ease-in-out infinite 1s;
-}
-
-@keyframes float {
-  0%, 100% { transform: translateY(0) scale(1); }
-  50% { transform: translateY(-20rpx) scale(1.05); }
-}
-
-/* ===== 主内容 ===== */
-.splash-content {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  opacity: 0;
-  animation: splash-fade-in 0.8s ease forwards;
-}
-
-@keyframes splash-fade-in {
-  from {
-    opacity: 0;
-    transform: translateY(30rpx);
-  }
-  to {
-    opacity: 1;
-    transform: translateY(0);
-  }
-}
-
-/* ===== Logo ===== */
-.splash-logo-wrap {
-  width: 160rpx;
-  height: 160rpx;
-  border-radius: 40rpx;
-  background: rgba(255, 255, 255, 0.95);
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  margin-bottom: 24rpx;
-  box-shadow: 0 8rpx 32rpx rgba(249, 115, 22, 0.25);
-}
-
-.splash-logo-img {
-  width: 100rpx;
-  height: 100rpx;
-}
-
-/* ===== Brand Name ===== */
-.splash-brand-name {
-  font-size: 60rpx;
-  font-weight: 700;
-  color: #7C2D12;
-  letter-spacing: 8rpx;
-  margin-bottom: 4rpx;
-  text-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
-}
-
-.splash-brand-en {
-  font-size: 22rpx;
-  color: rgba(124, 45, 18, 0.6);
-  letter-spacing: 4rpx;
-  margin-bottom: 20rpx;
-}
-
-/* ===== Tagline ===== */
-.splash-tagline {
-  font-size: 28rpx;
-  color: rgba(124, 45, 18, 0.8);
-  letter-spacing: 2rpx;
-  font-weight: 500;
-}
-
-/* ===== 五维图标行 ===== */
-.splash-dims-row {
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  justify-content: center;
-  gap: 20rpx;
-  margin: 24rpx 0 24rpx;
-}
-
-.splash-dim-item {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  gap: 6rpx;
-  opacity: 0;
-  transform: translateY(20rpx);
-  animation: splash-dim-enter 0.4s ease forwards;
-}
-
-.splash-dim-item:nth-child(1) { animation-delay: 0.4s; }
-.splash-dim-item:nth-child(2) { animation-delay: 0.55s; }
-.splash-dim-item:nth-child(3) { animation-delay: 0.7s; }
-.splash-dim-item:nth-child(4) { animation-delay: 0.85s; }
-.splash-dim-item:nth-child(5) { animation-delay: 1.0s; }
-
-.splash-dim-circle {
-  width: 68rpx;
-  height: 68rpx;
-  border-radius: 50%;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-}
-
-.splash-dim-icon {
-  font-size: 30rpx;
-  line-height: 1;
-}
-
-.splash-dim-label {
-  font-size: 20rpx;
-  color: rgba(124, 45, 18, 0.7);
-  line-height: 1;
-  font-weight: 500;
-}
-
-@keyframes splash-dim-enter {
-  from {
-    opacity: 0;
-    transform: translateY(20rpx);
-  }
-  to {
-    opacity: 1;
-    transform: translateY(0);
-  }
-}
-</style>

+ 2 - 1
cfc-frontend/pages/wisdom/index.vue

@@ -421,7 +421,8 @@ export default {
       uni.navigateTo({ url: '/pages/activity/index' })
       uni.navigateTo({ url: '/pages/activity/index' })
     },
     },
     goActivityDetail: function(act) {
     goActivityDetail: function(act) {
-      if (act && act.id) uni.navigateTo({ url: '/pages/discover-detail/activity-detail/activity-detail?id=' + act.id })
+      if (!act || !act.id) return
+      uni.navigateTo({ url: '/pages/discover-detail/activity-detail/activity-detail?id=' + act.id })
     },
     },
     goProductDetail: function(prod) {
     goProductDetail: function(prod) {
       if (prod && prod.id) {
       if (prod && prod.id) {

+ 6 - 0
cfc-web/src/router/index.js

@@ -450,6 +450,12 @@ const routes = [
         component: () => import('@/views/admin/data-source-config'),
         component: () => import('@/views/admin/data-source-config'),
         meta: { title: '健康数据源配置', perm: 'system:config' }
         meta: { title: '健康数据源配置', perm: 'system:config' }
       },
       },
+      {
+        path: 'health-energy-config',
+        name: 'HealthEnergyConfig',
+        component: () => import('@/views/admin/HealthEnergyConfig'),
+        meta: { title: '七维能量配置', perm: 'system:config' }
+      },
       {
       {
         path: 'indicators',
         path: 'indicators',
         name: 'IndicatorManage',
         name: 'IndicatorManage',

+ 1 - 0
cfc-web/src/views/Layout.vue

@@ -272,6 +272,7 @@ export default {
               children: [
               children: [
                 { path: '/health-norm-config', label: '健康常模', icon: 'el-icon-data-line', perm: 'system:config' },
                 { path: '/health-norm-config', label: '健康常模', icon: 'el-icon-data-line', perm: 'system:config' },
                 { path: '/health-data-source', label: '健康数据源', icon: 'el-icon-data-line', perm: 'system:config' },
                 { path: '/health-data-source', label: '健康数据源', icon: 'el-icon-data-line', perm: 'system:config' },
+                { path: '/health-energy-config', label: '七维能量配置', icon: 'el-icon-data-line', perm: 'system:config' },
               ]},
               ]},
             // --- 人生维度 ---
             // --- 人生维度 ---
             { title: '人生维度', icon: 'el-icon-s-custom', perm: 'system:config',
             { title: '人生维度', icon: 'el-icon-s-custom', perm: 'system:config',

+ 216 - 0
cfc-web/src/views/admin/HealthEnergyConfig.vue

@@ -0,0 +1,216 @@
+<template>
+  <div class="health-energy-config">
+    <el-card>
+      <div slot="header">
+        <span>七维健康能量配置</span>
+        <el-button style="float: right" type="primary" size="small" @click="handleSave" :loading="submitting">保存配置</el-button>
+      </div>
+
+      <el-form ref="form" :model="form" label-width="140px" v-loading="loading">
+        <!-- 维度权重配置 -->
+        <el-divider content-position="left">各维度权重配置</el-divider>
+        <el-row :gutter="20">
+          <el-col :span="12" v-for="dim in dimensions" :key="dim.code">
+            <el-form-item :label="dim.label + ' 权重'">
+              <el-slider
+                v-model="form.weights[dim.code]"
+                :min="0"
+                :max="100"
+                :step="1"
+                show-input
+                :format-tooltip="val => val + '%'"
+                @change="onWeightChange(dim.code)"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-alert
+          v-if="weightSum !== 100"
+          :title="'权重总和不等于 100%(当前: ' + weightSum + '%')'"
+          type="warning"
+          :closable="false"
+          style="margin-bottom: 20px;"
+        />
+        <el-alert
+          v-else
+          title="权重总和 = 100%,配置有效"
+          type="success"
+          :closable="false"
+          style="margin-bottom: 20px;"
+        />
+
+        <el-divider content-position="left">同步系数配置</el-divider>
+        <el-form-item label="能量同步系数">
+          <el-slider
+            v-model="form.syncFactor"
+            :min="0"
+            :max="100"
+            :step="1"
+            show-input
+            :format-tooltip="val => (val / 100).toFixed(2)"
+            style="max-width: 400px;"
+          />
+          <div style="color: #909399; font-size: 12px; margin-top: 4px;">
+            系数决定七维评分转换为身能量的比例(当前: {{ (form.syncFactor / 100).toFixed(2) }})
+            公式: 身能量 = 加权评分 × 系数
+          </div>
+        </el-form-item>
+
+        <el-divider content-position="left">配置说明</el-divider>
+        <el-alert
+          title="计算逻辑"
+          type="info"
+          :closable="false"
+          style="margin-bottom: 10px;"
+        >
+          <template slot>
+            <ul style="margin: 8px 0 0 0; padding-left: 20px; font-size: 13px; color: #606266; line-height: 1.8;">
+              <li>每次录入七维健康评分后,系统计算加权评分:<code>加权评分 = Σ(各维度评分 × 权重) / 权重总和</code></li>
+              <li>然后转换为身能量值:<code>身能量 = 加权评分 × 同步系数</code></li>
+              <li>权重越高,该维度评分对身能量的影响越大</li>
+              <li>同步系数控制能量发放速度(系数越大,每次同步获得的能量越多)</li>
+            </ul>
+          </template>
+        </el-alert>
+      </el-form>
+    </el-card>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'HealthEnergyConfig',
+  data() {
+    return {
+      loading: false,
+      submitting: false,
+      form: {
+        weights: {
+          growth: 20,
+          sleep: 15,
+          vision: 10,
+          immunity: 15,
+          nutrition: 15,
+          gut: 10,
+          exercise: 15
+        },
+        syncFactor: 50
+      },
+      dimensions: [
+        { code: 'growth', label: '生长发育' },
+        { code: 'sleep', label: '睡眠质量' },
+        { code: 'vision', label: '视力健康' },
+        { code: 'immunity', label: '免疫力' },
+        { code: 'nutrition', label: '营养均衡' },
+        { code: 'gut', label: '肠胃健康' },
+        { code: 'exercise', label: '运动活力' }
+      ]
+    }
+  },
+  computed: {
+    weightSum() {
+      return Object.values(this.form.weights).reduce((sum, v) => sum + v, 0)
+    }
+  },
+  mounted() {
+    this.loadConfig()
+  },
+  methods: {
+    loadConfig() {
+      this.loading = true
+      this.$axios.post('/api/admin/config/all').then(res => {
+        if (res.data.code === 200) {
+          const config = res.data.data || {}
+
+          // 加载权重配置
+          const weightsJson = config['health_dimension_weights']
+          if (weightsJson) {
+            try {
+              const weights = JSON.parse(weightsJson)
+              if (weights && typeof weights === 'object') {
+                for (const key of Object.keys(this.form.weights)) {
+                  if (weights[key] !== undefined) {
+                    this.form.weights[key] = Math.round(weights[key] * 100)
+                  }
+                }
+              }
+            } catch (e) {
+              this.$message.warning('权重配置解析失败,使用默认值')
+            }
+          }
+
+          // 加载同步系数
+          const factorStr = config['health_dimension_sync_factor']
+          if (factorStr) {
+            const factor = parseFloat(factorStr)
+            if (!isNaN(factor)) {
+              this.form.syncFactor = Math.round(factor * 100)
+            }
+          }
+        }
+      }).catch(() => {
+        this.$message.error('加载配置失败')
+      }).finally(() => {
+        this.loading = false
+      })
+    },
+    onWeightChange() {
+      // 归一化处理:如果总和超过100,等比例压缩
+      const sum = this.weightSum
+      if (sum > 0 && sum !== 100) {
+        const ratio = 100 / sum
+        for (const key of Object.keys(this.form.weights)) {
+          this.form.weights[key] = Math.round(this.form.weights[key] * ratio)
+        }
+      }
+    },
+    handleSave() {
+      if (this.weightSum !== 100) {
+        this.$message.warning('权重总和必须等于 100%')
+        return
+      }
+      this.submitting = true
+
+      // 将 0-100 的权重值转换为 0-1 的小数
+      const normalizedWeights = {}
+      for (const key of Object.keys(this.form.weights)) {
+        normalizedWeights[key] = this.form.weights[key] / 100
+      }
+
+      const promises = [
+        this.$axios.post('/api/admin/config/update', {
+          configKey: 'health_dimension_weights',
+          configValue: JSON.stringify(normalizedWeights)
+        }),
+        this.$axios.post('/api/admin/config/update', {
+          configKey: 'health_dimension_sync_factor',
+          configValue: (this.form.syncFactor / 100).toFixed(2)
+        })
+      ]
+
+      Promise.all(promises).then(res => {
+        const allSuccess = res.every(r => r.data.code === 200)
+        if (allSuccess) {
+          this.$message.success('保存成功')
+        } else {
+          this.$message.error('部分配置保存失败')
+        }
+      }).catch(() => {
+        this.$message.error('保存失败')
+      }).finally(() => {
+        this.submitting = false
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.health-energy-config {
+  padding: 20px;
+}
+.health-energy-config .el-col {
+  margin-bottom: 0;
+}
+</style>