Browse Source

Merge branch 'cfclub' of http://git.iwintrue.com/liaoxg/cfc into cfclub

# Conflicts:
#	cfc-frontend/pages/body/index.vue
#	cfc-frontend/pages/wealth/index.vue
openhands 2 months ago
parent
commit
6222650340
29 changed files with 1657 additions and 646 deletions
  1. 43 18
      cfc-backend/src/main/java/com/etotem/cfc/controller/GrowthTaskController.java
  2. 1 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/ProductSkuController.java
  3. 66 10
      cfc-backend/src/main/java/com/etotem/cfc/controller/ai/AIChatController.java
  4. 11 6
      cfc-backend/src/main/java/com/etotem/cfc/dto/ProductSkuDTO.java
  5. 2 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/ProductSkuMapper.java
  6. 24 0
      cfc-backend/src/main/java/com/etotem/cfc/service/FamilyContextService.java
  7. 37 14
      cfc-backend/src/main/java/com/etotem/cfc/service/GrowthTaskService.java
  8. 4 0
      cfc-backend/src/main/java/com/etotem/cfc/service/ProductSkuService.java
  9. 4 0
      cfc-backend/src/main/resources/mapper/ProductSkuMapper.xml
  10. 229 0
      cfc-frontend/components/AITaskCard.vue
  11. 1 3
      cfc-frontend/manifest.json
  12. 7 14
      cfc-frontend/pages/action/index.vue
  13. 96 52
      cfc-frontend/pages/ai/chat.vue
  14. 10 193
      cfc-frontend/pages/body/index.vue
  15. 23 0
      cfc-frontend/pages/health/gut-flora-detail.vue
  16. 40 5
      cfc-frontend/pages/health/physical-exam-detail.vue
  17. 126 22
      cfc-frontend/pages/mind-detail/traditional-mirror.vue
  18. 280 16
      cfc-frontend/pages/mind/index.vue
  19. 126 22
      cfc-frontend/pages/mind/traditional-mirror.vue
  20. 37 2
      cfc-frontend/pages/profile/index.vue
  21. 314 255
      cfc-frontend/pages/wealth/index.vue
  22. 10 6
      cfc-frontend/pages/wisdom/index.vue
  23. 1 1
      cfc-web/.last_build_commit
  24. 2 2
      cfc-web/package-lock.json
  25. 1 1
      cfc-web/package.json
  26. 46 0
      cfc-web/public/CHANGELOG-v1.0.md
  27. 47 1
      cfc-web/public/CHANGELOG.md
  28. 10 2
      cfc-web/src/views/admin/components/ProductSkuEditor.vue
  29. 59 0
      docs/superpowers/plans/2026-07-19-report-upload-purchase-links.md

+ 43 - 18
cfc-backend/src/main/java/com/etotem/cfc/controller/GrowthTaskController.java

@@ -1,6 +1,8 @@
 package com.etotem.cfc.controller;
 
 import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.GrowthTask;
+import com.etotem.cfc.mapper.GrowthTaskMapper;
 import com.etotem.cfc.service.GrowthTaskService;
 import org.springframework.web.bind.annotation.*;
 
@@ -12,27 +14,50 @@ import java.util.Map;
 @RequestMapping("/api/growth-task")
 public class GrowthTaskController {
 
-    @Resource
-    private GrowthTaskService growthTaskService;
+ @Resource
+ private GrowthTaskService growthTaskService;
+
+ @Resource
+ private GrowthTaskMapper growthTaskMapper;
 
     @PostMapping("/list")
     public Result<List<Map<String, Object>>> list(@RequestAttribute("userId") Long userId,
                                                    @RequestBody Map<String, String> params) {
-        String type = params.getOrDefault("type", "DAILY");
-        return Result.success(growthTaskService.getTaskList(userId, type));
+return Result.success(growthTaskService.getTaskList(userId, params));
     }
 
-    @PostMapping("/claim")
-    public Result<String> claim(@RequestAttribute("userId") Long userId,
-                                @RequestBody Map<String, Long> params) {
-        Long taskLogId = params.get("taskLogId");
-        if (taskLogId == null) {
-            return Result.error("taskLogId不能为空");
-        }
-        String msg = growthTaskService.claimReward(userId, taskLogId);
-        if ("领取成功".equals(msg)) {
-            return Result.success(msg);
-        }
-        return Result.error(msg);
-    }
-}
+ @PostMapping("/claim")
+ public Result<String> claim(@RequestAttribute("userId") Long userId,
+     @RequestBody Map<String, Long> params) {
+   Long taskLogId = params.get("taskLogId");
+   if (taskLogId == null) {
+     return Result.error("taskLogId不能为空");
+   }
+   String msg = growthTaskService.claimReward(userId, taskLogId);
+   if ("领取成功".equals(msg)) {
+     return Result.success(msg);
+   }
+   return Result.error(msg);
+ }
+
+ @PostMapping("/accept-dynamic")
+ public Result<Map<String, Object>> acceptDynamic(@RequestAttribute("userId") Long userId,
+     @RequestBody Map<String, Object> params) {
+   Object taskIdObj = params.get("taskId");
+   if (taskIdObj == null) {
+     return Result.error("taskId不能为空");
+   }
+   Long taskId = ((Number) taskIdObj).longValue();
+   GrowthTask growthTask = growthTaskMapper.selectById(taskId);
+   if (growthTask == null) {
+     return Result.error("任务不存在");
+   }
+   Map<String, Object> taskInfo = new java.util.HashMap<>();
+   taskInfo.put("id", growthTask.getId());
+   taskInfo.put("title", growthTask.getTitle());
+   taskInfo.put("description", growthTask.getDescription());
+   taskInfo.put("dimension", growthTask.getDimension());
+   taskInfo.put("rewardPoints", growthTask.getRewardPoints());
+   return Result.success(taskInfo);
+ }
+ }

+ 1 - 1
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/ProductSkuController.java

@@ -27,7 +27,7 @@ public class ProductSkuController {
         Object productIdObj = params.get("productId");
         if (productIdObj == null) return Result.error("productId不能为空");
         Long productId = Long.parseLong(productIdObj.toString());
-        List<ProductSku> skus = skuService.listByProductId(productId);
+        List<ProductSku> skus = skuService.listByProductIdAll(productId);
         List<ProductSkuDTO> dtos = skus.stream()
             .map(ProductSkuDTO::from)
             .collect(Collectors.toList());

+ 66 - 10
cfc-backend/src/main/java/com/etotem/cfc/controller/ai/AIChatController.java

@@ -4,10 +4,12 @@ import com.etotem.cfc.common.Result;
 import com.etotem.cfc.dto.HealthAnalysisResult;
 import com.etotem.cfc.dto.RecommendationQuery;
 import com.etotem.cfc.dto.RecommendationResult;
+import com.etotem.cfc.dto.TaskParseResult;
 import com.etotem.cfc.service.AIService;
 import com.etotem.cfc.service.FamilyContextService;
 import com.etotem.cfc.service.HealthAnalysisService;
 import com.etotem.cfc.service.RecommendationService;
+import com.etotem.cfc.service.TaskParseService;
 import com.etotem.cfc.service.UserService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
@@ -44,6 +46,9 @@ public class AIChatController {
     @Resource
     private com.etotem.cfc.service.GrowthTaskService growthTaskService;
 
+    @Resource
+    private TaskParseService taskParseService;
+
     @Resource
     private UserService userService;
 
@@ -107,10 +112,37 @@ public class AIChatController {
                 query, String.valueOf(userId),
                 conversationId, inputs);
 
+        String answer = (String) difyResp.getOrDefault("answer", "");
         Map<String, Object> result = new LinkedHashMap<>();
-        result.put("answer", difyResp.getOrDefault("answer", ""));
+        result.put("answer", answer);
         result.put("conversationId", difyResp.getOrDefault("conversationId", ""));
 
+        // Parse [TASK] markers and create task records
+        List<Map<String, Object>> tasks = new ArrayList<>();
+        List<TaskParseResult> parsedTasks = taskParseService.parseTasks(answer);
+        String cleanAnswer = taskParseService.stripTaskMarkers(answer);
+        
+        for (TaskParseResult taskResult : parsedTasks) {
+            com.etotem.cfc.entity.GrowthTask task = growthTaskService.createDynamicTask(
+                userId,
+                taskResult.getTitle(),
+                taskResult.getDescription(),
+                taskResult.getDimension(),
+                taskResult.getRewardPoints(),
+                (String) difyResp.getOrDefault("conversationId", "")
+            );
+            Map<String, Object> taskInfo = new LinkedHashMap<>();
+            taskInfo.put("id", task.getId());
+            taskInfo.put("title", task.getTitle());
+            taskInfo.put("description", task.getDescription());
+            taskInfo.put("dimension", task.getDimension());
+            taskInfo.put("rewardPoints", task.getRewardPoints());
+            tasks.add(taskInfo);
+        }
+        
+        result.put("answer", cleanAnswer);
+        result.put("tasks", tasks);
+
         try { growthTaskService.updateProgress(userId, "DAILY_AI", 1); } catch (Exception e) { log.warn("成长任务AI对话进度更新失败: userId={}, error={}", userId, e.getMessage()); }
 
         return Result.success(result);
@@ -274,16 +306,40 @@ public class AIChatController {
             } catch (Exception e) {
                 log.warn("解析 [RECOMMEND] 标记失败, 忽略: {}", e.getMessage());
             }
-            // 从回答中移除标记
-            cleanAnswer = matcher.replaceAll("").trim();
-        }
+        // 从回答中移除标记
+        cleanAnswer = matcher.replaceAll("").trim();
+    }
 
-        // 5. 组装结果
-        Map<String, Object> result = new LinkedHashMap<>();
-        result.put("answer", cleanAnswer);
-        result.put("conversationId", difyResp.getOrDefault("conversationId", ""));
-        result.put("recommendations", recommendations != null ? recommendations : Collections.emptyList());
-        return Result.success(result);
+    // Parse [TASK] markers and create task records
+    List<Map<String, Object>> tasks = new ArrayList<>();
+    List<TaskParseResult> parsedTasks = taskParseService.parseTasks(answer);
+    cleanAnswer = taskParseService.stripTaskMarkers(cleanAnswer);
+    
+    for (TaskParseResult taskResult : parsedTasks) {
+        com.etotem.cfc.entity.GrowthTask task = growthTaskService.createDynamicTask(
+            userId,
+            taskResult.getTitle(),
+            taskResult.getDescription(),
+            taskResult.getDimension(),
+            taskResult.getRewardPoints(),
+            null
+        );
+        Map<String, Object> taskInfo = new LinkedHashMap<>();
+        taskInfo.put("id", task.getId());
+        taskInfo.put("title", task.getTitle());
+        taskInfo.put("description", task.getDescription());
+        taskInfo.put("dimension", task.getDimension());
+        taskInfo.put("rewardPoints", task.getRewardPoints());
+        tasks.add(taskInfo);
+    }
+
+    // 5. 组装结果
+    Map<String, Object> result = new LinkedHashMap<>();
+    result.put("answer", cleanAnswer);
+    result.put("conversationId", difyResp.getOrDefault("conversationId", ""));
+    result.put("recommendations", recommendations != null ? recommendations : Collections.emptyList());
+    result.put("tasks", tasks);
+    return Result.success(result);
     }
 
     /**

+ 11 - 6
cfc-backend/src/main/java/com/etotem/cfc/dto/ProductSkuDTO.java

@@ -5,7 +5,6 @@ import com.alibaba.fastjson.TypeReference;
 import com.etotem.cfc.entity.ProductSku;
 import lombok.Data;
 import java.math.BigDecimal;
-import java.util.List;
 import java.util.Map;
 
 @Data
@@ -13,7 +12,7 @@ public class ProductSkuDTO {
     private Long id;
     private Long productId;
     private String skuCode;
-    private List<Map<String, String>> specs;
+    private Map<String, String> specs;
     private BigDecimal price;
     private BigDecimal originalPrice;
     private Integer stock;
@@ -31,12 +30,18 @@ public class ProductSkuDTO {
         dto.setStock(sku.getStock());
         dto.setImage(sku.getImage());
         dto.setEnabled(sku.getEnabled() != null && sku.getEnabled() == 1);
-        // Parse specs JSON to List<Map>
+        // Parse specs JSON to Map (stored as JSON object in DB)
         if (sku.getSpecs() != null) {
             try {
-                List<Map<String, String>> specsList = JSON.parseObject(sku.getSpecs().toString(),
-                    new TypeReference<List<Map<String, String>>>() {});
-                dto.setSpecs(specsList);
+                String specsStr;
+                if (sku.getSpecs() instanceof String) {
+                    specsStr = (String) sku.getSpecs();
+                } else {
+                    specsStr = JSON.toJSONString(sku.getSpecs());
+                }
+                Map<String, String> specsMap = JSON.parseObject(specsStr,
+                    new TypeReference<Map<String, String>>() {});
+                dto.setSpecs(specsMap);
             } catch (Exception e) {
                 dto.setSpecs(null);
             }

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/ProductSkuMapper.java

@@ -9,6 +9,8 @@ public interface ProductSkuMapper extends BaseMapper<ProductSku> {
 
     List<ProductSku> findByProductId(@Param("productId") Long productId);
 
+    List<ProductSku> findByProductIdAll(@Param("productId") Long productId);
+
     ProductSku findById(@Param("id") Long id);
 
     int updateStock(@Param("id") Long id, @Param("quantity") Integer quantity);

+ 24 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/FamilyContextService.java

@@ -51,6 +51,9 @@ public class FamilyContextService {
     @Resource
     private ProductMapper productMapper;
 
+    @Resource
+    private GrowthTaskMapper growthTaskMapper;
+
     /**
      * 构建当前用户的家庭上下文
      *
@@ -116,6 +119,27 @@ public class FamilyContextService {
                     c.put("healthReport", reportInfo);
                 }
 
+                List<GrowthTask> aiTasks = growthTaskMapper.selectList(
+                        new LambdaQueryWrapper<GrowthTask>()
+                                .eq(GrowthTask::getType, "AI_GENERATED")
+                                .eq(GrowthTask::getEnabled, 1)
+                                .orderByDesc(GrowthTask::getCreatedAt)
+                                .last("LIMIT 10"));
+                if (aiTasks != null && !aiTasks.isEmpty()) {
+                    List<Map<String, Object>> taskList = new ArrayList<>();
+                    for (GrowthTask gt : aiTasks) {
+                        Map<String, Object> t = new LinkedHashMap<>();
+                        t.put("id", gt.getId());
+                        t.put("title", gt.getTitle());
+                        t.put("description", gt.getDescription());
+                        t.put("dimension", gt.getDimension());
+                        t.put("rewardPoints", gt.getRewardPoints());
+                        t.put("sourceConversationId", gt.getSourceConversationId());
+                        taskList.add(t);
+                    }
+                    c.put("aiGeneratedTasks", taskList);
+                }
+
                 childList.add(c);
             }
         }

+ 37 - 14
cfc-backend/src/main/java/com/etotem/cfc/service/GrowthTaskService.java

@@ -38,17 +38,24 @@ public class GrowthTaskService {
     /**
      * Get task list with user's progress for today (daily) or all-time (newbie).
      */
-    public List<Map<String, Object>> getTaskList(Long userId, String type) {
-        if (type == null || type.isEmpty()) {
-            type = "DAILY";
-        }
+public List<Map<String, Object>> getTaskList(Long userId, Map<String, String> params) {
+	String type = params != null ? params.getOrDefault("type", "DAILY") : "DAILY";
+	String dimension = params != null ? params.get("dimension") : null;
 
-        List<GrowthTask> tasks = growthTaskMapper.selectList(
-                new LambdaQueryWrapper<GrowthTask>()
-                        .eq(GrowthTask::getType, type)
-                        .eq(GrowthTask::getEnabled, 1)
-                        .orderByAsc(GrowthTask::getSortOrder)
-        );
+	if (type == null || type.isEmpty()) {
+		type = "DAILY";
+	}
+
+	LambdaQueryWrapper<GrowthTask> queryWrapper = new LambdaQueryWrapper<GrowthTask>()
+		.eq(GrowthTask::getType, type)
+		.eq(GrowthTask::getEnabled, 1)
+		.orderByAsc(GrowthTask::getSortOrder);
+
+	if (dimension != null && !dimension.isEmpty()) {
+		queryWrapper.eq(GrowthTask::getDimension, dimension);
+	}
+
+	List<GrowthTask> tasks = growthTaskMapper.selectList(queryWrapper);
 
         String today = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
 
@@ -62,9 +69,10 @@ public class GrowthTaskService {
             item.put("rewardPoints", task.getRewardPoints());
             item.put("rewardEnergy", task.getRewardEnergy());
             item.put("targetValue", task.getTargetValue());
-            item.put("taskKey", task.getTaskKey());
+item.put("taskKey", task.getTaskKey());
+item.put("dimension", task.getDimension());
 
-            // Find user's log for this task
+// Find user's log for this task
             LambdaQueryWrapper<GrowthTaskLog> logWrapper = new LambdaQueryWrapper<GrowthTaskLog>()
                     .eq(GrowthTaskLog::getUserId, userId)
                     .eq(GrowthTaskLog::getTaskId, task.getId());
@@ -250,7 +258,22 @@ public class GrowthTaskService {
             taskLog.setClaimed(0);
             taskLog.setDate(null); // NEWBIE tasks are all-time, no date
             taskLog.setCreatedAt(new Date());
-            growthTaskLogMapper.insert(taskLog);
+        growthTaskLogMapper.insert(taskLog);
+        }
+        }
+
+        @Transactional
+        public GrowthTask createDynamicTask(Long userId, String title, String description, String dimension, Integer rewardPoints, String sourceConversationId) {
+            GrowthTask task = new GrowthTask();
+            task.setType("AI_GENERATED");
+            task.setTitle(title);
+            task.setDescription(description);
+            task.setDimension(dimension != null ? dimension : "body");
+            task.setRewardPoints(rewardPoints != null ? rewardPoints : 10);
+            task.setEnabled(1);
+            task.setSourceConversationId(sourceConversationId);
+            growthTaskMapper.insert(task);
+            log.info("创建AI动态任务: userId={}, taskId={}, title={}", userId, task.getId(), title);
+            return task;
         }
-    }
 }

+ 4 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ProductSkuService.java

@@ -22,6 +22,10 @@ public class ProductSkuService {
         return skuMapper.findByProductId(productId);
     }
 
+    public List<ProductSku> listByProductIdAll(Long productId) {
+        return skuMapper.findByProductIdAll(productId);
+    }
+
     public ProductSku getById(Long id) {
         return skuMapper.findById(id);
     }

+ 4 - 0
cfc-backend/src/main/resources/mapper/ProductSkuMapper.xml

@@ -20,6 +20,10 @@
         SELECT * FROM product_skus WHERE product_id = #{productId} AND enabled = 1 ORDER BY id ASC
     </select>
 
+    <select id="findByProductIdAll" resultMap="BaseResultMap">
+        SELECT * FROM product_skus WHERE product_id = #{productId} ORDER BY id ASC
+    </select>
+
     <select id="findById" resultMap="BaseResultMap">
         SELECT * FROM product_skus WHERE id = #{id}
     </select>

+ 229 - 0
cfc-frontend/components/AITaskCard.vue

@@ -0,0 +1,229 @@
+<template>
+  <view class="ai-task-card">
+    <view class="card-header">
+      <text class="card-title">{{ task.title }}</text>
+      <view class="dimension-badge" :style="{ backgroundColor: dimensionColor }">
+        <text class="dimension-text">{{ task.dimension }}</text>
+      </view>
+    </view>
+
+    <view class="card-body">
+      <text class="card-description">{{ task.description }}</text>
+    </view>
+
+    <view class="card-footer">
+      <view class="reward-info">
+        <text class="reward-label">奖励能量</text>
+        <text class="reward-points">+{{ task.rewardPoints }}</text>
+      </view>
+
+      <button
+        class="accept-btn"
+        :loading="loading"
+        :disabled="loading"
+        @click="handleAccept"
+      >
+        <text class="btn-text">{{ loading ? '接受中...' : '接受任务' }}</text>
+      </button>
+    </view>
+  </view>
+</template>
+
+<script>
+export default {
+  name: 'AITaskCard',
+
+  props: {
+    task: {
+      type: Object,
+      default: function() {
+        return {
+          title: '',
+          description: '',
+          dimension: '',
+          rewardPoints: 0,
+          id: null
+        }
+      }
+    },
+    conversationId: {
+      type: String,
+      default: ''
+    }
+  },
+
+  data: function() {
+    return {
+      loading: false
+    }
+  },
+
+  computed: {
+    dimensionColor: function() {
+      var colorMap = {
+        '身': '#10B981',
+        '心': '#F59E0B',
+        '智': '#3B82F6',
+        '行': '#8B5CF6',
+        '富': '#F97316'
+      }
+      return colorMap[this.task.dimension] || '#F97316'
+    }
+  },
+
+  methods: {
+    handleAccept: function() {
+      var self = this
+      if (self.loading) {
+        return
+      }
+
+      var token = uni.getStorageSync('token')
+      if (!token) {
+        uni.showToast({
+          title: '请先登录',
+          icon: 'none'
+        })
+        return
+      }
+
+      self.loading = true
+
+      uni.request({
+        url: self.$apiBase + '/api/growth-task/accept-dynamic',
+        method: 'POST',
+        header: {
+          'Authorization': 'Bearer ' + token,
+          'Content-Type': 'application/json'
+        },
+        data: {
+          taskId: self.task.id,
+          conversationId: self.conversationId
+        },
+        success: function(res) {
+          if (res.statusCode === 200 && res.data && res.data.code === 200) {
+            uni.showToast({
+              title: '任务已接受',
+              icon: 'success'
+            })
+            self.$emit('accepted', self.task)
+          } else {
+            var message = (res.data && res.data.message) || '接受失败'
+            uni.showToast({
+              title: message,
+              icon: 'none'
+            })
+          }
+        },
+        fail: function() {
+          uni.showToast({
+            title: '网络请求失败',
+            icon: 'none'
+          })
+        },
+        complete: function() {
+          self.loading = false
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.ai-task-card {
+  background-color: #FFFFFF;
+  border-radius: 16rpx;
+  padding: 32rpx;
+  margin: 24rpx;
+  box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
+}
+
+.card-header {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 24rpx;
+}
+
+.card-title {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #1F2937;
+  flex: 1;
+  margin-right: 16rpx;
+}
+
+.dimension-badge {
+  padding: 8rpx 20rpx;
+  border-radius: 24rpx;
+  flex-shrink: 0;
+}
+
+.dimension-text {
+  font-size: 24rpx;
+  font-weight: 500;
+  color: #FFFFFF;
+}
+
+.card-body {
+  margin-bottom: 32rpx;
+}
+
+.card-description {
+  font-size: 28rpx;
+  color: #6B7280;
+  line-height: 44rpx;
+}
+
+.card-footer {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+  padding-top: 24rpx;
+  border-top: 1rpx solid #F3F4F6;
+}
+
+.reward-info {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+
+.reward-label {
+  font-size: 26rpx;
+  color: #9CA3AF;
+  margin-right: 12rpx;
+}
+
+.reward-points {
+  font-size: 32rpx;
+  font-weight: 700;
+  color: #F97316;
+}
+
+.accept-btn {
+  background-color: #F97316;
+  color: #FFFFFF;
+  border: none;
+  border-radius: 12rpx;
+  padding: 20rpx 40rpx;
+  font-size: 28rpx;
+  font-weight: 500;
+  line-height: 1.2;
+}
+
+.accept-btn:active {
+  background-color: #EA580C;
+}
+
+.accept-btn:disabled {
+  background-color: #FDBA74;
+}
+
+.btn-text {
+  color: #FFFFFF;
+}
+</style>

+ 1 - 3
cfc-frontend/manifest.json

@@ -52,9 +52,7 @@
   },
   "mp-alipay": {},
   "mp-baidu": {},
-  "mp-toutiao": {
-    "usingComponents": true
-  },
+  "mp-toutiao": {},
   "uniStatistics": {
     "enable": false
   },

+ 7 - 14
cfc-frontend/pages/action/index.vue

@@ -82,12 +82,11 @@
       @articleClick="goArticleDetail"
       @moreArticles="goMoreArticles" />
 
-    <!-- ===== 重要关系维护 + 关系健康概览 ===== -->
-    <OctopusGraph
-      :contactList="contactList"
-      :healthAlerts="healthAlerts"
-      :milestones="milestones"
-      :isLoggedIn="isLoggedIn" />
+        <!-- ===== 重要关系维护 ===== -->
+        <OctopusGraph
+          :contactList="contactList"
+          :healthAlerts="healthAlerts"
+          :isLoggedIn="isLoggedIn" />
 
     <!-- 功能入口(4个:活动、商品、课程) -->
     <view class="func-section" v-if="sectionVisible('func_entries')">
@@ -151,7 +150,7 @@ export default {
       contactList: [],
       showImportModal: false,
       healthAlerts: [],
-      milestones: [],
+    
       funcList: [
         { icon: '\u{1F3CB}', label: '任务', needLogin: true, page: '/pages/tasks/tasks' },
         { icon: '\u{1F3C0}', label: '活动', needLogin: false, page: '/pages/discover/index?type=activity' },
@@ -470,13 +469,7 @@ export default {
       }).catch(function() {
         self.healthAlerts = []
       })
-      getMilestones({}, 30).then(function(res) {
-        if (res.code === 200 && res.data) {
-          self.milestones = Array.isArray(res.data) ? res.data : []
-        }
-      }).catch(function() {
-        self.milestones = []
-      })
+      
     },
     goInteractionLog: function(memberId) {
       if (memberId) {

+ 96 - 52
cfc-frontend/pages/ai/chat.vue

@@ -75,18 +75,26 @@
             <view class="message-avatar">{{ mascotIcon || '🤖' }}</view>
             <view class="message-bubble ai-bubble">
               <text class="ai-answer-text">{{ msg.answer }}</text>
-              <!-- 推荐卡片 -->
-              <view class="rec-list" v-if="msg.recommendations && msg.recommendations.length > 0">
-                <view class="rec-card" v-for="(rec, ri) in msg.recommendations" :key="ri" @click="gotoRec(rec)">
-                  <image class="rec-cover" v-if="rec.coverImage" :src="rec.coverImage" mode="aspectFill" />
-                  <view class="rec-info">
-                    <text class="rec-type-tag">{{ rec.type === 'product' ? '商品' : rec.type === 'activity' ? '活动' : '文章' }}</text>
-                    <text class="rec-name">{{ rec.name }}</text>
-                    <text class="rec-desc">{{ rec.description }}</text>
-                    <text class="rec-price" v-if="rec.price">¥{{ rec.price }}</text>
-                  </view>
-                </view>
-              </view>
+        <!-- 推荐卡片 -->
+        <view class="rec-list" v-if="msg.recommendations && msg.recommendations.length > 0">
+          <view class="rec-card" v-for="(rec, ri) in msg.recommendations" :key="ri" @click="gotoRec(rec)">
+            <image class="rec-cover" v-if="rec.coverImage" :src="rec.coverImage" mode="aspectFill" />
+            <view class="rec-info">
+              <text class="rec-type-tag">{{ rec.type === 'product' ? '商品' : rec.type === 'activity' ? '活动' : '文章' }}</text>
+              <text class="rec-name">{{ rec.name }}</text>
+              <text class="rec-desc">{{ rec.description }}</text>
+              <text class="rec-price" v-if="rec.price">¥{{ rec.price }}</text>
+            </view>
+          </view>
+        </view>
+        <!-- AI 任务卡片 -->
+        <view class="task-list" v-if="msg.tasks && msg.tasks.length > 0">
+          <AITaskCard v-for="(task, ti) in msg.tasks" :key="ti" :task="task" :conversationId="currentConversationId" @accepted="onTaskAccepted" />
+        </view>
+        <!-- 报告来源标识 -->
+        <view class="source-label" v-if="msg.sourceReport">
+          来源:{{ msg.sourceReport.type === 'nutrition' ? '营养报告' : '测评报告' }} #{{ msg.sourceReport.id }}
+        </view>
             </view>
           </view>
         </view>
@@ -122,9 +130,10 @@
 <script>
 import { aiSendMessage, aiGetConversations, aiGetMessages, aiDeleteConversation, aiSendNutritionMessage } from '../../utils/api.js'
 import BaseEmpty from '../../components/BaseEmpty.vue'
+import AITaskCard from '../../components/AITaskCard.vue'
 
 export default {
-  components: { BaseEmpty },
+  components: { BaseEmpty, AITaskCard },
   data() {
     return {
       inputText: '',
@@ -138,6 +147,7 @@ export default {
       pendingFirstMessage: '',
       fromPage: '',
       mode: '',
+      reportId: '',
       recommendations: [],
       mascotCode: '',
       mascotName: '',
@@ -174,6 +184,7 @@ export default {
   onLoad: function(options) {
     this.mode = options.mode || ''
     this.fromPage = options.fromPage ? decodeURIComponent(options.fromPage) : ''
+    this.reportId = options.reportId || ''
     if (options.firstMessage) {
       this.pendingFirstMessage = decodeURIComponent(options.firstMessage)
     }
@@ -211,6 +222,10 @@ export default {
     goBack() {
       uni.navigateBack()
     },
+    
+    onTaskAccepted() {
+      this.loadConversations()
+    },
 
     async loadConversations() {
       try {
@@ -249,53 +264,65 @@ export default {
       })
     },
 
-    async sendMessage() {
-      const text = this.inputText.trim()
-      if (!text || this.sending) return
+  async sendMessage() {
+    const text = this.inputText.trim()
+    if (!text || this.sending) return
 
-      // 立即显示用户消息
-      const tempMsg = { id: 'temp_' + Date.now(), query: text, answer: '' }
-      this.messages.push(tempMsg)
-      this.inputText = ''
-      this.sending = true
+    // 立即显示用户消息
+    const tempMsg = { id: 'temp_' + Date.now(), query: text, answer: '' }
+    this.messages.push(tempMsg)
+    this.inputText = ''
+    this.sending = true
 
-      this.$nextTick(() => {
-        this.scrollToBottom()
-      })
+    this.$nextTick(() => {
+      this.scrollToBottom()
+    })
 
-      try {
-        var params = {
-          query: text,
-          conversationId: this.currentConversationId
-        }
+    try {
+      var params = {
+        query: text,
+        conversationId: this.currentConversationId
+      }
+      if (this.reportId) {
+        params.reportId = this.reportId
+      }
 
-        var res, data
-        if (this.mode === 'nutrition') {
-          // 营养模式:调精准营养助手
-          res = await aiSendNutritionMessage(params)
-          data = res.data || {}
-          // 更新最后一条消息的回复 + 推荐列表
-          const lastMsg = this.messages[this.messages.length - 1]
-          if (lastMsg) {
-            lastMsg.answer = data.answer || ''
-            lastMsg.recommendations = data.recommendations || []
+      var res, data
+      if (this.mode === 'nutrition') {
+        // 营养模式:调精准营养助手
+        res = await aiSendNutritionMessage(params)
+        data = res.data || {}
+        // 更新最后一条消息的回复 + 推荐列表 + 任务
+        const lastMsg = this.messages[this.messages.length - 1]
+        if (lastMsg) {
+          lastMsg.answer = data.answer || ''
+          lastMsg.recommendations = data.recommendations || []
+          lastMsg.tasks = data.tasks || []
+          if (this.reportId) {
+            lastMsg.sourceReport = { id: this.reportId, type: 'nutrition' }
           }
-        } else {
-          res = await aiSendMessage(params)
-          data = res.data || {}
-          // 更新最后一条消息的回复
-          const lastMsg = this.messages[this.messages.length - 1]
-          if (lastMsg) {
-            lastMsg.answer = data.answer || ''
+        }
+      } else {
+        res = await aiSendMessage(params)
+        data = res.data || {}
+        // 更新最后一条消息的回复 + 推荐列表 + 任务
+        const lastMsg = this.messages[this.messages.length - 1]
+        if (lastMsg) {
+          lastMsg.answer = data.answer || ''
+          lastMsg.recommendations = data.recommendations || []
+          lastMsg.tasks = data.tasks || []
+          if (this.reportId) {
+            lastMsg.sourceReport = { id: this.reportId, type: 'assessment' }
           }
         }
+      }
 
-        // 更新 conversationId(新建会话时 Dify 返回)
-        if (data.conversationId && !this.currentConversationId) {
-          this.currentConversationId = data.conversationId
-          // 刷新会话列表
-          this.loadConversations()
-        }
+      // 更新 conversationId(新建会话时 Dify 返回)
+      if (data.conversationId && !this.currentConversationId) {
+        this.currentConversationId = data.conversationId
+        // 刷新会话列表
+        this.loadConversations()
+      }
       } catch (e) {
         // 显示错误
         const lastMsg = this.messages[this.messages.length - 1]
@@ -711,6 +738,23 @@ export default {
   gap: 12rpx;
 }
 
+/* 任务卡片列表 */
+.task-list {
+  border-top: 1rpx solid var(--border-light, #E2E8F0);
+  padding-top: 16rpx;
+  display: flex;
+  flex-direction: column;
+  gap: 12rpx;
+}
+
+/* 报告来源标识 */
+.source-label {
+  margin-top: 16rpx;
+  font-size: 22rpx;
+  color: var(--muted, #94A3B8);
+  text-align: right;
+}
+
 .rec-card {
   display: flex;
   flex-direction: row;

+ 10 - 193
cfc-frontend/pages/body/index.vue

@@ -68,7 +68,7 @@
     <HealthDimensionsSection
       v-if="isLoggedIn && dimensionData"
       :healthData="dimensionData"
-      :activeChildId="activeChildId || ($store.state && $store.state.currentChildId)" />
+      :activeChildId="activeChildId" />
 
     <!-- 功能入口 -->
     <view class="func-section" v-if="sectionVisible('func_entries')">
@@ -82,25 +82,6 @@
       </view>
     </view>
 
-    <!-- ===== 精准营养(含上传报告入口) ===== -->
-    <view class="nutrition-section section" v-if="isLoggedIn">
-      <view class="nutrition-report-card" @click="goToUploadReport">
-        <view class="nutrition-report-left">
-          <text class="nutrition-icon-large">🏥</text>
-        </view>
-        <view class="nutrition-report-right">
-          <text class="nutrition-title">上传健康报告</text>
-          <text class="nutrition-desc">上传体检报告或肠道检测报告,获取 AI 深度解读与精准营养推荐</text>
-          <view class="nutrition-report-tags">
-            <text class="tag">体检报告</text>
-            <text class="tag">肠道检测</text>
-            <text class="tag">AI分析</text>
-          </view>
-        </view>
-        <text class="nutrition-arrow">›</text>
-      </view>
-    </view>
-
     <!-- ===== 精准营养(上传报告+产品+文章) ===== -->
     <view class="section nutrition-section" v-if="isLoggedIn">
       <view class="section-header">
@@ -182,34 +163,6 @@ import HealthCheckinCard from '../../components/HealthCheckinCard.vue'
 import AIFloatingAvatar from '../../components/AIFloatingAvatar.vue'
 
 import { getVisibleSections, getEnergyOverview, getChildren, getTodayTasksByCategory, getActivityList, getProductsByDomain, getFamilyEnergySandbox, getVisibleFamilyMembers, getDimensionOverview, getFeaturedArticles } from '../../utils/api.js'
-import config from '../../config.js'
-
-var BASE_URL = config.api('')
-
-const healthRequest = function(url, data) {
-  var token = uni.getStorageSync('token')
-  return new Promise(function(resolve, reject) {
-    uni.request({
-      url: BASE_URL + url,
-      method: 'POST',
-      data: data,
-      header: {
-        'Content-Type': 'application/json',
-        'Authorization': token ? 'Bearer ' + token : ''
-      },
-      success: function(res) {
-        if (res.data.code === 200) {
-          resolve(res.data)
-        } else {
-          reject(res.data)
-        }
-      },
-      fail: function(err) {
-        reject(err)
-      }
-    })
-  })
-}
 
 export default {
   components: { TabTransition, PageBanner, LoginGuideCard, FamilyEnergyBar, DimensionTasks, DimensionActivities, DimensionProducts, DimensionProductList, DimensionArticles, FamilyRelationGraph, RadarChart, HealthDimensionsSection, HealthCheckinCard, AIFloatingAvatar },
@@ -220,14 +173,13 @@ export default {
       showTabTransition: true,
       activeChildId: null,
       activeChildName: '',
-      currentUserId: null,
       userName: '',
       children: [],
       familyMembersVisible: [],
       sandboxData: null,
       dualDimension: null,
       visibleSections: [],
-      latestReport: null,
+
       dimensionTasks: [],
       dimensionActivities: [],
       dimensionProducts: [],
@@ -245,10 +197,6 @@ export default {
     }
   },
   computed: {
-    reportTypeLabel: function() {
-      if (!this.latestReport) return ''
-      return this.latestReport.reportType === 'gut_flora' ? '肠道检测报告' : '体检报告'
-    },
     energyMapForGraph: function() {
       var map = {}
       if (this.sandboxData && this.sandboxData.members) {
@@ -301,13 +249,6 @@ export default {
     if (this.isLoggedIn) {
       this.role = uni.getStorageSync('currentRole') || uni.getStorageSync('role') || null
       this.userName = uni.getStorageSync('nickname') || uni.getStorageSync('userName') || '家长'
-      this.currentUserId = uni.getStorageSync('userId')
-      // FIX: 同步 Vuex store 的 currentView 和 currentChildId
-      if (this.$store.state && this.$store.state.currentView) {
-        if (this.$store.state.currentView === 'member' && this.$store.state.currentChildId) {
-          this.activeChildId = this.$store.state.currentChildId
-        }
-      }
     }
     this.loadSectionConfig()
     if (this.isLoggedIn) {
@@ -370,20 +311,18 @@ export default {
       })
     },
     loadDashboardData: function() {
-      // FIX: 不再阻塞于 activeChildId 为空。
-      // 沙盒数据(家庭级别)始终加载;子维度数据按需加载
+      if (!this.activeChildId) return
+      this.loadEnergyData()
+      this.loadDimensionTasks()
+      this.loadDimensionActivities()
+      this.loadDimensionProducts()
       this.loadSandboxData()
       this.loadHealthDimensions()
-      if (this.activeChildId) {
-        this.loadHealthReport()
-        this.loadEnergyData()
-        this.loadDimensionTasks()
-      }
     },
     loadHealthDimensions: function() {
-      var memberId = this.activeChildId || this.currentUserId
+      var memberId = this.activeChildId || (this.userInfo && this.userInfo.userId)
       if (!memberId) {
-        console.log('[body] loadHealthDimensions skipped: no memberId available')
+        console.log('[body] loadHealthDimensions skipped: no memberId')
         return
       }
       var self = this
@@ -408,16 +347,6 @@ export default {
         console.log('获取沙盘数据失败', e)
       })
     },
-    loadHealthReport: function() {
-      var self = this
-      healthRequest('/api/health/report/latest', { childId: this.activeChildId }).then(function(res) {
-        if (res.data) {
-          self.latestReport = res.data
-        }
-      }).catch(function(e) {
-        console.log('获取健康报告失败', e)
-      })
-    },
     loadEnergyData: function() {
       var self = this
       getEnergyOverview(this.activeChildId).then(function(res) {
@@ -482,7 +411,6 @@ export default {
               categoryColor: gradientColors[index % gradientColors.length],
               title: item.title || '',
               summary: item.summary || '',
-              dimensionWeights: item.dimensionWeights || null,
               views: item.viewCount ? String(item.viewCount) : '0',
               date: item.publishedAt ? item.publishedAt.slice(0, 10) : ''
             }
@@ -493,8 +421,6 @@ export default {
     onChildChanged: function(child) {
       this.activeChildId = child.childId
       this.activeChildName = child.name || child.nickname || '孩子'
-      // FIX: 同步提交到 Vuex store,确保 currentView 和 currentChildId 一致
-      this.$store.commit('switchToChild', child.childId)
       this.loadDashboardData()
     },
     loadFamilyMembersVisible: function() {
@@ -540,7 +466,7 @@ export default {
       })
     },
     goFamilyMembers: function() {
-      uni.navigateTo({ url: '/pages/profile-extra/family-members' })
+      uni.navigateTo({ url: '/pages/profile/family-members' })
     },
     sectionVisible: function(key) {
       return this.visibleSections.length === 0 || this.visibleSections.indexOf(key) !== -1
@@ -566,14 +492,6 @@ export default {
       }
       if (item.page) uni.navigateTo({ url: item.page })
     },
-    goToHealthReport: function() {
-      var id = this.activeChildId || uni.getStorageSync('currentChildId')
-      if (!id) {
-        uni.showToast({ title: '请先选择成员', icon: 'none' })
-        return
-      }
-      uni.navigateTo({ url: '/pages/body-detail/health-report?childId=' + id })
-    },
     goDimensionDetail: function(dimKey) {
       uni.navigateTo({ url: '/pages/body/dimension-detail?dimension=' + dimKey + '&childId=' + this.activeChildId })
     },
@@ -597,18 +515,6 @@ export default {
       var id = this.activeChildId || uni.getStorageSync('currentChildId') || ''
       uni.navigateTo({ url: '/pages/health/report-upload?childId=' + id + '&from=body' })
     },
-    formatDate: function(dateStr) {
-      if (!dateStr) return '--'
-      try {
-        var d = new Date(dateStr)
-        var y = d.getFullYear()
-        var m = ('' + (d.getMonth() + 1)).padStart(2, '0')
-        var day = ('' + d.getDate()).padStart(2, '0')
-        return y + '-' + m + '-' + day
-      } catch (e) {
-        return dateStr
-      }
-    },
     _watchPageReady() {
       var self = this
       var unwatch = this.$watch(function() {
@@ -701,95 +607,6 @@ export default {
   border-radius: 20rpx;
   margin-left: 12rpx;
 }
-.section-more {
-  font-size: 24rpx;
-  color: #5B9BD5;
-}
-
-/* ===== 健康报告 ===== */
-.report-card {
-  background: #fff;
-  border-radius: 20rpx;
-  padding: 24rpx;
-  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
-}
-.report-type-tag {
-  display: inline-block;
-  background: #E8F5E9;
-  color: #2E7D32;
-  font-size: 22rpx;
-  padding: 4rpx 16rpx;
-  border-radius: 20rpx;
-  margin-bottom: 16rpx;
-}
-.report-score-row {
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-}
-.score-circle {
-  width: 140rpx;
-  height: 140rpx;
-  border-radius: 70rpx;
-  background: linear-gradient(135deg, #E3F2FD, #BBDEFB);
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
-  margin-right: 24rpx;
-}
-.score-value {
-  font-size: 48rpx;
-  font-weight: bold;
-  color: #1565C0;
-}
-.score-label {
-  font-size: 20rpx;
-  color: #64B5F6;
-}
-.report-meta {
-  flex: 1;
-}
-.meta-item {
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  margin-bottom: 12rpx;
-}
-.meta-label {
-  font-size: 24rpx;
-  color: #666;
-  width: 140rpx;
-}
-.meta-value {
-  font-size: 26rpx;
-  color: #333;
-  font-weight: 500;
-}
-.report-date {
-  font-size: 22rpx;
-  color: #bbb;
-  margin-top: 16rpx;
-}
-.report-empty {
-  background: #fff;
-  border-radius: 20rpx;
-  padding: 40rpx;
-  text-align: center;
-  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
-}
-.empty-tip {
-  font-size: 28rpx;
-  color: #999;
-  display: block;
-}
-.empty-sub {
-  font-size: 24rpx;
-  color: #ccc;
-  display: block;
-  margin-top: 8rpx;
-}
-
 /* ===== 快捷操作 ===== */
 .quick-actions {
   display: flex;

+ 23 - 0
cfc-frontend/pages/health/gut-flora-detail.vue

@@ -388,6 +388,12 @@ export default {
     },
     goToIndicators: function() {
       this.accordionOpen.nutri = true
+    },
+    goAIChat: function() {
+      if (!this.reportId) return
+      uni.navigateTo({
+        url: '/pages/ai/chat?mode=nutrition&reportId=' + this.reportId + '&firstMessage=' + encodeURIComponent('请根据这份肠道菌群检测报告,分析我孩子的肠道健康状况并提出建议')
+      })
     }
   }
 }
@@ -484,4 +490,21 @@ export default {
   color: #795548;
   line-height: 1.6;
 }
+
+/* AI 解读入口 */
+.ai-fab {
+  position: fixed;
+  right: 30rpx;
+  bottom: 100rpx;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  background: linear-gradient(135deg, #6366F1, #8B5CF6);
+  border-radius: 40rpx;
+  padding: 16rpx 28rpx;
+  box-shadow: 0 4rpx 16rpx rgba(99, 102, 241, 0.4);
+  z-index: 100;
+}
+.ai-fab .fab-icon { font-size: 32rpx; margin-right: 12rpx; }
+.ai-fab .fab-text { font-size: 26rpx; color: #fff; font-weight: 500; }
 </style>

+ 40 - 5
cfc-frontend/pages/health/physical-exam-detail.vue

@@ -69,8 +69,14 @@
       </view>
     </template>
 
-    <view class="bottom-spacer"></view>
-  </view>
+<view class="bottom-spacer"></view>
+
+<!-- AI 解读入口 -->
+<view class="ai-fab" @click="goAIChat" v-if="report && reportId">
+  <text class="fab-icon">🤖</text>
+  <text class="fab-text">AI 解读报告</text>
+</view>
+</view>
 </template>
 
 <script>
@@ -190,9 +196,15 @@ export default {
       }
       return ''
     },
-    goBack: function() {
-      uni.navigateBack()
-    }
+goAIChat: function() {
+  if (!this.reportId) return
+  uni.navigateTo({
+    url: '/pages/ai/chat?mode=nutrition&reportId=' + this.reportId + '&firstMessage=' + encodeURIComponent('请根据这份体检报告,分析我孩子的健康状况并提出建议')
+  })
+},
+goBack: function() {
+  uni.navigateBack()
+}
   }
 }
 </script>
@@ -323,4 +335,27 @@ export default {
 .empty-text { font-size: 28rpx; color: #999; }
 
 .bottom-spacer { height: 60rpx; }
+
+.ai-fab {
+  position: fixed;
+  right: 30rpx;
+  bottom: 100rpx;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  background: linear-gradient(135deg, #6366F1, #8B5CF6);
+  border-radius: 40rpx;
+  padding: 16rpx 28rpx;
+  box-shadow: 0 4rpx 16rpx rgba(99, 102, 241, 0.4);
+  z-index: 100;
+}
+.ai-fab .fab-icon {
+  font-size: 32rpx;
+  margin-right: 12rpx;
+}
+.ai-fab .fab-text {
+  font-size: 26rpx;
+  color: #fff;
+  font-weight: 500;
+}
 </style>

+ 126 - 22
cfc-frontend/pages/mind-detail/traditional-mirror.vue

@@ -39,11 +39,20 @@
           </view>
         </view>
 
-        <!-- 五行解读 -->
-        <view class="reading-card" v-if="wuxingReading">
-          <text class="reading-label">五行解读</text>
-          <text class="reading-text" v-if="wuxingReading.personalTrait">{{ wuxingReading.personalTrait }}</text>
-          <text class="reading-text" v-if="wuxingReading.mindGrowthAdvice">{{ wuxingReading.mindGrowthAdvice }}</text>
+        <!-- 今日能量文 -->
+        <view class="energy-card" v-if="wuxingReading">
+          <view class="energy-header">
+            <text class="energy-icon">&#x2728;</text>
+            <text class="energy-label">今日能量文</text>
+          </view>
+          <view class="energy-divider"></view>
+          <text class="energy-quote" v-if="wuxingReading.personalTrait">{{ wuxingReading.personalTrait }}</text>
+          <text class="energy-advice" v-if="wuxingReading.mindGrowthAdvice">{{ wuxingReading.mindGrowthAdvice }}</text>
+          <view class="energy-footer">
+            <text class="energy-footer-dot"></text>
+            <text class="energy-footer-dot"></text>
+            <text class="energy-footer-dot"></text>
+          </view>
         </view>
       </view>
 
@@ -287,28 +296,123 @@ export default {
   text-align: right;
 }
 
-.reading-card {
-  margin-top: 20rpx;
-  padding: 16rpx;
-  background: #FFFBEB;
-  border-radius: 12rpx;
-  border-left: 6rpx solid #F59E0B;
+.energy-card {
+  margin-top: 24rpx;
+  padding: 28rpx 24rpx;
+  background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
+  border-radius: 20rpx;
+  position: relative;
+  overflow: hidden;
 }
 
-.reading-label {
-  font-size: 24rpx;
-  font-weight: 600;
-  color: #D97706;
-  display: block;
-  margin-bottom: 8rpx;
+.energy-card::before {
+  content: '';
+  position: absolute;
+  top: -60rpx;
+  right: -60rpx;
+  width: 200rpx;
+  height: 200rpx;
+  background: radial-gradient(circle, rgba(255,215,0,0.15) 0%, transparent 70%);
+  border-radius: 50%;
 }
 
-.reading-text {
-  font-size: 24rpx;
-  color: #666;
-  line-height: 1.6;
+.energy-card::after {
+  content: '';
+  position: absolute;
+  bottom: -40rpx;
+  left: -40rpx;
+  width: 160rpx;
+  height: 160rpx;
+  background: radial-gradient(circle, rgba(99,102,241,0.12) 0%, transparent 70%);
+  border-radius: 50%;
+}
+
+.energy-header {
+  display: flex;
+  align-items: center;
+  margin-bottom: 16rpx;
+  position: relative;
+  z-index: 1;
+}
+
+.energy-icon {
+  font-size: 36rpx;
+  margin-right: 12rpx;
+}
+
+.energy-label {
+  font-size: 30rpx;
+  font-weight: 700;
+  background: linear-gradient(135deg, #F59E0B, #FF6B9D);
+  -webkit-background-clip: text;
+  -webkit-text-fill-color: transparent;
+  background-clip: text;
+}
+
+.energy-divider {
+  height: 2rpx;
+  background: linear-gradient(90deg, rgba(255,215,0,0.5), rgba(255,107,157,0.3), transparent);
+  margin-bottom: 20rpx;
+  position: relative;
+  z-index: 1;
+}
+
+.energy-quote {
+  font-size: 28rpx;
+  color: #E2E8F0;
+  line-height: 1.8;
   display: block;
-  margin-top: 8rpx;
+  margin-bottom: 16rpx;
+  position: relative;
+  z-index: 1;
+  padding-left: 24rpx;
+  letter-spacing: 1rpx;
+}
+
+.energy-quote::before {
+  content: '';
+  position: absolute;
+  left: 0;
+  top: 4rpx;
+  bottom: 4rpx;
+  width: 4rpx;
+  background: linear-gradient(180deg, #F59E0B, #FF6B9D);
+  border-radius: 2rpx;
+}
+
+.energy-advice {
+  font-size: 26rpx;
+  color: #94A3B8;
+  line-height: 1.7;
+  display: block;
+  position: relative;
+  z-index: 1;
+  padding: 16rpx 20rpx;
+  background: rgba(255,255,255,0.05);
+  border-radius: 12rpx;
+  border: 1rpx solid rgba(255,255,255,0.08);
+}
+
+.energy-footer {
+  display: flex;
+  justify-content: center;
+  gap: 12rpx;
+  margin-top: 20rpx;
+  position: relative;
+  z-index: 1;
+}
+
+.energy-footer-dot {
+  width: 8rpx;
+  height: 8rpx;
+  border-radius: 50%;
+  background: rgba(255,255,255,0.3);
+}
+
+.energy-footer-dot:nth-child(2) {
+  background: rgba(255,215,0,0.6);
+  width: 10rpx;
+  height: 10rpx;
 }
 
 .zodiac-display {

+ 280 - 16
cfc-frontend/pages/mind/index.vue

@@ -34,14 +34,47 @@
         ]" />
     </template>
 
-    <!-- 登录后:五维能量条 -->
-    <FamilyEnergyBar
-      v-if="isLoggedIn"
-      dimensionCode="mind"
-      :sandboxData="sandboxData" />
+        <!-- 登录后:五维能量条 -->
+        <FamilyEnergyBar
+          v-if="isLoggedIn"
+          dimensionCode="mind"
+          :sandboxData="sandboxData" />
 
-    <!-- 关系图谱(登录后可见) -->
-    <FamilyRelationGraph
+        <!-- 心理报告卡片(登录后可见) -->
+        <view class="emi-card" v-if="isLoggedIn && emiData">
+          <view class="emi-header">
+            <text class="emi-title">心理测评报告</text>
+            <view class="emi-overall-wrap">
+              <text class="emi-overall-label">综合EQ分</text>
+              <text class="emi-overall-value">{{ emiData.overallScore || 0 }}</text>
+              <text :class="['emi-overall-level', 'level-' + getScoreLevel(emiData.overallScore || 0)]">
+                {{ getScoreLevelText(emiData.overallScore || 0) }}
+              </text>
+            </view>
+          </view>
+          <view class="emi-dimensions">
+            <view class="emi-dimension" v-for="dim in emiDimensions" :key="dim.key">
+              <view class="emi-dim-header">
+                <text class="emi-dim-label">{{ dim.label }}</text>
+                <view class="emi-dim-values">
+                  <text class="emi-dim-score">{{ emiData[dim.key] || 0 }}</text>
+                  <text :class="['emi-dim-level', 'level-' + getScoreLevel(emiData[dim.key] || 0)]">
+                    {{ getScoreLevelText(emiData[dim.key] || 0) }}
+                  </text>
+                </view>
+              </view>
+              <view class="emi-dim-bar">
+                <view class="emi-dim-bar-fill" :style="{ width: (emiData[dim.key] || 0) + '%', background: dim.color }"></view>
+              </view>
+            </view>
+          </view>
+          <view class="emi-footer" @click="goEmotionReport">
+            <text class="emi-footer-text">查看详情</text>
+            <text class="emi-footer-icon">&#x2192;</text>
+          </view>
+        </view>
+        <!-- 关系图谱(登录后可见) -->
+        <FamilyRelationGraph
       v-if="isLoggedIn && familyMembersVisible.length > 0"
       dimensionCode="mind"
       :selfId="activeChildId"
@@ -165,8 +198,19 @@
             <text class="wuxing-value">{{ wx.value }}%</text>
           </view>
         </view>
-        <view class="wuxing-reading" v-if="traditionalData.wuxingReading">
-          <text>{{ traditionalData.wuxingReading.personalTrait || '' }}</text>
+        <view class="energy-card" v-if="traditionalData.wuxingReading">
+          <view class="energy-header">
+            <text class="energy-icon">&#x2728;</text>
+            <text class="energy-label">今日能量文</text>
+          </view>
+          <view class="energy-divider"></view>
+          <text class="energy-quote">{{ traditionalData.wuxingReading.personalTrait || '' }}</text>
+          <text class="energy-advice" v-if="traditionalData.wuxingReading.mindGrowthAdvice">{{ traditionalData.wuxingReading.mindGrowthAdvice }}</text>
+          <view class="energy-footer">
+            <text class="energy-footer-dot"></text>
+            <text class="energy-footer-dot"></text>
+            <text class="energy-footer-dot"></text>
+          </view>
         </view>
       </view>
 
@@ -1357,6 +1401,117 @@ export default {
   margin-left: 8rpx;
 }
 
+/* ===== EMI 心理报告卡片 ===== */
+.emi-card {
+  margin: 30rpx;
+  background: #fff;
+  border-radius: 24rpx;
+  padding: 30rpx;
+  box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06);
+}
+.emi-header {
+  margin-bottom: 24rpx;
+}
+.emi-title {
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #333;
+  display: block;
+}
+.emi-overall-wrap {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 32rpx 0;
+  background: linear-gradient(135deg, #FF6B35, #FFD700);
+  border-radius: 16rpx;
+  margin: 20rpx 0;
+  color: #fff;
+}
+.emi-overall-label {
+  font-size: 26rpx;
+  margin-bottom: 6rpx;
+}
+.emi-overall-value {
+  font-size: 64rpx;
+  font-weight: bold;
+  margin-bottom: 6rpx;
+}
+.emi-overall-level {
+  font-size: 24rpx;
+  padding: 4rpx 16rpx;
+  border-radius: 12rpx;
+  background: rgba(255,255,255,0.2);
+}
+.emi-dimensions {
+  display: flex;
+  flex-direction: column;
+  gap: 20rpx;
+}
+.emi-dimension {
+  display: flex;
+  flex-direction: column;
+}
+.emi-dim-header {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 8rpx;
+}
+.emi-dim-label {
+  font-size: 26rpx;
+  color: #666;
+}
+.emi-dim-values {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+.emi-dim-score {
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #FF6B35;
+  margin-right: 12rpx;
+}
+.emi-dim-level {
+  font-size: 20rpx;
+  padding: 2rpx 12rpx;
+  border-radius: 8rpx;
+}
+.emi-dim-bar {
+  height: 14rpx;
+  background: #f0f0f0;
+  border-radius: 7rpx;
+  overflow: hidden;
+}
+.emi-dim-bar-fill {
+  height: 100%;
+  border-radius: 7rpx;
+  transition: width 0.5s;
+}
+.emi-footer {
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+  padding-top: 20rpx;
+  border-top: 1rpx solid #f0f0f0;
+  margin-top: 20rpx;
+}
+.emi-footer-text {
+  font-size: 24rpx;
+  color: #F97316;
+  font-weight: 500;
+}
+.emi-footer-icon {
+  font-size: 24rpx;
+  margin-left: 8rpx;
+}
+.level-excellent { color: #10B981; background: #ECFDF5; }
+.level-good { color: #3B82F6; background: #EFF6FF; }
+.level-average { color: #F97316; background: #FFF7ED; }
+.level-low { color: #EF4444; background: #FEF2F2; }
+
 /* 雷达图内的空状态 */
 .placeholder-content {
   display: flex;
@@ -1491,14 +1646,123 @@ export default {
   width: 56rpx;
   text-align: right;
 }
-.wuxing-reading {
-  margin-top: 12rpx;
-  padding: 12rpx 14rpx;
-  background: #F8F6F0;
-  border-radius: 12rpx;
+.energy-card {
+  margin-top: 20rpx;
+  padding: 24rpx 20rpx;
+  background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
+  border-radius: 20rpx;
+  position: relative;
+  overflow: hidden;
+}
+
+.energy-card::before {
+  content: '';
+  position: absolute;
+  top: -60rpx;
+  right: -60rpx;
+  width: 200rpx;
+  height: 200rpx;
+  background: radial-gradient(circle, rgba(255,215,0,0.15) 0%, transparent 70%);
+  border-radius: 50%;
+}
+
+.energy-card::after {
+  content: '';
+  position: absolute;
+  bottom: -40rpx;
+  left: -40rpx;
+  width: 160rpx;
+  height: 160rpx;
+  background: radial-gradient(circle, rgba(99,102,241,0.12) 0%, transparent 70%);
+  border-radius: 50%;
+}
+
+.energy-header {
+  display: flex;
+  align-items: center;
+  margin-bottom: 14rpx;
+  position: relative;
+  z-index: 1;
+}
+
+.energy-icon {
+  font-size: 32rpx;
+  margin-right: 10rpx;
+}
+
+.energy-label {
+  font-size: 28rpx;
+  font-weight: 700;
+  background: linear-gradient(135deg, #F59E0B, #FF6B9D);
+  -webkit-background-clip: text;
+  -webkit-text-fill-color: transparent;
+  background-clip: text;
+}
+
+.energy-divider {
+  height: 2rpx;
+  background: linear-gradient(90deg, rgba(255,215,0,0.5), rgba(255,107,157,0.3), transparent);
+  margin-bottom: 18rpx;
+  position: relative;
+  z-index: 1;
+}
+
+.energy-quote {
+  font-size: 26rpx;
+  color: #E2E8F0;
+  line-height: 1.8;
+  display: block;
+  margin-bottom: 14rpx;
+  position: relative;
+  z-index: 1;
+  padding-left: 20rpx;
+  letter-spacing: 1rpx;
+}
+
+.energy-quote::before {
+  content: '';
+  position: absolute;
+  left: 0;
+  top: 4rpx;
+  bottom: 4rpx;
+  width: 4rpx;
+  background: linear-gradient(180deg, #F59E0B, #FF6B9D);
+  border-radius: 2rpx;
+}
+
+.energy-advice {
   font-size: 24rpx;
-  color: #666;
-  line-height: 1.5;
+  color: #94A3B8;
+  line-height: 1.7;
+  display: block;
+  position: relative;
+  z-index: 1;
+  padding: 14rpx 18rpx;
+  background: rgba(255,255,255,0.05);
+  border-radius: 12rpx;
+  border: 1rpx solid rgba(255,255,255,0.08);
+}
+
+.energy-footer {
+  display: flex;
+  justify-content: center;
+  gap: 12rpx;
+  margin-top: 18rpx;
+  position: relative;
+  z-index: 1;
+}
+
+.energy-footer-dot {
+  width: 8rpx;
+  height: 8rpx;
+  border-radius: 50%;
+  background: rgba(255,255,255,0.3);
+}
+
+.energy-footer-dot:nth-child(2) {
+  background: rgba(255,215,0,0.6);
+  width: 10rpx;
+  height: 10rpx;
 }
 /* 星座+灵数双栏 */
 .culture-dual {

+ 126 - 22
cfc-frontend/pages/mind/traditional-mirror.vue

@@ -39,11 +39,20 @@
           </view>
         </view>
 
-        <!-- 五行解读 -->
-        <view class="reading-card" v-if="wuxingReading">
-          <text class="reading-label">五行解读</text>
-          <text class="reading-text" v-if="wuxingReading.personalTrait">{{ wuxingReading.personalTrait }}</text>
-          <text class="reading-text" v-if="wuxingReading.mindGrowthAdvice">{{ wuxingReading.mindGrowthAdvice }}</text>
+        <!-- 今日能量文 -->
+        <view class="energy-card" v-if="wuxingReading">
+          <view class="energy-header">
+            <text class="energy-icon">&#x2728;</text>
+            <text class="energy-label">今日能量文</text>
+          </view>
+          <view class="energy-divider"></view>
+          <text class="energy-quote" v-if="wuxingReading.personalTrait">{{ wuxingReading.personalTrait }}</text>
+          <text class="energy-advice" v-if="wuxingReading.mindGrowthAdvice">{{ wuxingReading.mindGrowthAdvice }}</text>
+          <view class="energy-footer">
+            <text class="energy-footer-dot"></text>
+            <text class="energy-footer-dot"></text>
+            <text class="energy-footer-dot"></text>
+          </view>
         </view>
       </view>
 
@@ -287,28 +296,123 @@ export default {
   text-align: right;
 }
 
-.reading-card {
-  margin-top: 20rpx;
-  padding: 16rpx;
-  background: #FFFBEB;
-  border-radius: 12rpx;
-  border-left: 6rpx solid #F59E0B;
+.energy-card {
+  margin-top: 24rpx;
+  padding: 28rpx 24rpx;
+  background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
+  border-radius: 20rpx;
+  position: relative;
+  overflow: hidden;
 }
 
-.reading-label {
-  font-size: 24rpx;
-  font-weight: 600;
-  color: #D97706;
-  display: block;
-  margin-bottom: 8rpx;
+.energy-card::before {
+  content: '';
+  position: absolute;
+  top: -60rpx;
+  right: -60rpx;
+  width: 200rpx;
+  height: 200rpx;
+  background: radial-gradient(circle, rgba(255,215,0,0.15) 0%, transparent 70%);
+  border-radius: 50%;
 }
 
-.reading-text {
-  font-size: 24rpx;
-  color: #666;
-  line-height: 1.6;
+.energy-card::after {
+  content: '';
+  position: absolute;
+  bottom: -40rpx;
+  left: -40rpx;
+  width: 160rpx;
+  height: 160rpx;
+  background: radial-gradient(circle, rgba(99,102,241,0.12) 0%, transparent 70%);
+  border-radius: 50%;
+}
+
+.energy-header {
+  display: flex;
+  align-items: center;
+  margin-bottom: 16rpx;
+  position: relative;
+  z-index: 1;
+}
+
+.energy-icon {
+  font-size: 36rpx;
+  margin-right: 12rpx;
+}
+
+.energy-label {
+  font-size: 30rpx;
+  font-weight: 700;
+  background: linear-gradient(135deg, #F59E0B, #FF6B9D);
+  -webkit-background-clip: text;
+  -webkit-text-fill-color: transparent;
+  background-clip: text;
+}
+
+.energy-divider {
+  height: 2rpx;
+  background: linear-gradient(90deg, rgba(255,215,0,0.5), rgba(255,107,157,0.3), transparent);
+  margin-bottom: 20rpx;
+  position: relative;
+  z-index: 1;
+}
+
+.energy-quote {
+  font-size: 28rpx;
+  color: #E2E8F0;
+  line-height: 1.8;
   display: block;
-  margin-top: 8rpx;
+  margin-bottom: 16rpx;
+  position: relative;
+  z-index: 1;
+  padding-left: 24rpx;
+  letter-spacing: 1rpx;
+}
+
+.energy-quote::before {
+  content: '';
+  position: absolute;
+  left: 0;
+  top: 4rpx;
+  bottom: 4rpx;
+  width: 4rpx;
+  background: linear-gradient(180deg, #F59E0B, #FF6B9D);
+  border-radius: 2rpx;
+}
+
+.energy-advice {
+  font-size: 26rpx;
+  color: #94A3B8;
+  line-height: 1.7;
+  display: block;
+  position: relative;
+  z-index: 1;
+  padding: 16rpx 20rpx;
+  background: rgba(255,255,255,0.05);
+  border-radius: 12rpx;
+  border: 1rpx solid rgba(255,255,255,0.08);
+}
+
+.energy-footer {
+  display: flex;
+  justify-content: center;
+  gap: 12rpx;
+  margin-top: 20rpx;
+  position: relative;
+  z-index: 1;
+}
+
+.energy-footer-dot {
+  width: 8rpx;
+  height: 8rpx;
+  border-radius: 50%;
+  background: rgba(255,255,255,0.3);
+}
+
+.energy-footer-dot:nth-child(2) {
+  background: rgba(255,215,0,0.6);
+  width: 10rpx;
+  height: 10rpx;
 }
 
 .zodiac-display {

+ 37 - 2
cfc-frontend/pages/profile/index.vue

@@ -8,6 +8,11 @@
     </view>
     <template v-else>
       <PageBanner theme="wealth" tagline="个人中心" quote="" />
+      <!-- 从富页面进入的返回入口 -->
+      <view v-if="fromWealth" class="back-to-wealth" @click="goToWealth">
+        <text class="back-arrow">‹</text>
+        <text class="back-label">返回富页面</text>
+      </view>
       <ProfileHeader @avatar-click="goToEditInfo" />
       <ProfileMenu :role="role" @invite-generate="onInviteGenerate" />
     </template>
@@ -27,7 +32,8 @@ export default {
   },
   data() {
     return {
-      shareData: null
+      shareData: null,
+      fromWealth: false
     }
   },
   computed: {
@@ -38,7 +44,13 @@ export default {
       return uni.getStorageSync('currentRole') || uni.getStorageSync('role') || 'parent'
     }
   },
-  onShareAppMessage() {
+  onLoad(options) {
+      // 支持 wealth→profile 的跳转来源标识
+      if (options && options.from === 'wealth') {
+        this.fromWealth = true
+      }
+    },
+    onShareAppMessage() {
     if (this.shareData) {
       return {
         title: this.shareData.title,
@@ -48,6 +60,9 @@ export default {
     }
   },
   methods: {
+    goToWealth() {
+      uni.navigateTo({ url: '/pages/wealth/index' })
+    },
     goLogin() {
       uni.navigateTo({
         url: '/pages/login/login'
@@ -120,4 +135,24 @@ export default {
 .login-prompt .login-btn::after {
   border: none;
 }
+/* 从富页面返回入口 */
+.back-to-wealth {
+  display: flex;
+  align-items: center;
+  margin: 0 30rpx 20rpx;
+  padding: 20rpx 24rpx;
+  background: #fff;
+  border-radius: 16rpx;
+  box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.05);
+}
+.back-arrow {
+  font-size: 40rpx;
+  color: #F59E0B;
+  margin-right: 16rpx;
+  font-weight: bold;
+}
+.back-label {
+  font-size: 28rpx;
+  color: #666;
+}
 </style>

+ 314 - 255
cfc-frontend/pages/wealth/index.vue

@@ -3,162 +3,184 @@
     <!-- TabTransition overlay -->
     <tab-transition v-if="showTabTransition" dimCode="wealth" />
 
-    <!-- 品牌头部(始终显示) -->
-    <PageBanner theme="wealth"
-      tagline="利他创富 · 价值共享"
-      quote="富足源于利他与坚持" />
-
-    <!-- 游客专属区块 -->
-    <template v-if="!isLoggedIn">
-      <LoginGuideCard
-        title="登录后查看财富能量详情"
-        :items="[
-          '查看家庭财富能量与子维度评分',
-          '查看专属财富商品和活动推荐',
-          '查看创客中心和推广收益'
-        ]" />
-    </template>
-
-    <!-- 登录后:五维能量条 -->
-    <FamilyEnergyBar
-      v-if="isLoggedIn"
-      dimensionCode="wealth"
-      :sandboxData="sandboxData"
-      :dualDimension="dualDimension" />
-
-    <!-- 退出切换按钮 -->
-    <view class="exit-switch" v-if="isLoggedIn && isSwitchedChild" @click="exitSwitch">
-      <text>🔄 退出切换</text>
+    <!-- 未登录 -->
+    <view v-if="!isLoggedIn" class="login-prompt">
+      <view class="prompt-icon">👤</view>
+      <text class="prompt-title">登录浠艾福</text>
+      <text class="prompt-desc">登录后查看财富能量详情</text>
+      <button class="login-btn" @click="goLogin">登录 / 注册</button>
     </view>
 
-    <!-- 切换按钮(家长模式 + 未切换状态) -->
-    <view class="switch-section" v-if="isLoggedIn && role === 'parent' && !isSwitchedChild">
-      <button class="btn-switch" @click="onSwitchMode">切换到孩子视角</button>
-    </view>
+    <template v-else>
+      <!-- 品牌头部 -->
+      <PageBanner theme="wealth"
+        tagline="利他创富 · 价值共享"
+        quote="富足源于利他与坚持" />
+
+      <!-- 模式切换 / 退出切换入口(家长未切时显示"切换到孩子视角";已切时显示"退出切换") -->
+ <!-- 已切孩子态:点击"退出切换"回到家长模式 -->
+ <view v-if="isSwitchedChild" class="exit-switch" @click="exitSwitch">
+   <text>🔄 退出切换</text>
+ </view>
+ <!-- 家长且未切孩子态:此处展示"切换到孩子视角",点击完成模式切换 -->
+ <view v-else class="exit-switch" @click="onSwitchMode">
+   <text>👶 切换到孩子视角</text>
+ </view>
 
-    <!-- 财富子维度卡片 -->
-    <view class="section" v-if="isLoggedIn">
-      <view class="section-header">
-        <text class="section-title">💎 财富子维度</text>
+ <!-- 用户卡片 -->
+      <view class="user-card" @click="goToProfile">
+        <view class="avatar">👤</view>
+        <view class="user-info">
+          <text class="nickname">{{ nickname || '未设置昵称' }}</text>
+          <text class="role">{{ role === 'parent' ? '家长模式' : (role === 'child' ? '孩子模式' : '成长规划师模式') }}</text>
+        </view>
+        <text class="card-arrow">›</text>
       </view>
-      <!-- 家长版 -->
-      <view class="sub-dim-grid" v-if="role === 'parent'">
-        <view class="sub-dim-card" @click="goToIncome">
-          <text class="sub-dim-score">{{ wealthIncome || '-' }}</text>
-          <text class="sub-dim-label">金钱收入</text>
-          <text class="sub-dim-desc">累计佣金</text>
+
+<!-- 财富子维度卡片 -->
+      <view class="section">
+        <view class="section-header">
+          <text class="section-title">💎 财富子维度</text>
         </view>
-        <view class="sub-dim-card" @click="goToAchievements">
-          <text class="sub-dim-score">{{ wealthAchievement || '-' }}</text>
-          <text class="sub-dim-label">个人成就</text>
-          <text class="sub-dim-desc">认证成就</text>
+        <!-- 家长版 -->
+        <view class="sub-dim-grid" v-if="role === 'parent'">
+          <view class="sub-dim-card" @click="goToIncome">
+            <text class="sub-dim-score">{{ wealthIncome || '-' }}</text>
+            <text class="sub-dim-label">金钱收入</text>
+            <text class="sub-dim-desc">累计佣金</text>
+          </view>
+          <view class="sub-dim-card" @click="goToAchievements">
+            <text class="sub-dim-score">{{ wealthAchievement || '-' }}</text>
+            <text class="sub-dim-label">个人成就</text>
+            <text class="sub-dim-desc">认证成就</text>
+          </view>
+          <view class="sub-dim-card" @click="goToNetwork">
+            <text class="sub-dim-score">{{ wealthNetwork || '-' }}</text>
+            <text class="sub-dim-label">资源网络</text>
+            <text class="sub-dim-desc">推荐关系</text>
+          </view>
         </view>
-        <view class="sub-dim-card" @click="goToNetwork">
-          <text class="sub-dim-score">{{ wealthNetwork || '-' }}</text>
-          <text class="sub-dim-label">资源网络</text>
-          <text class="sub-dim-desc">推荐关系</text>
+        <!-- 孩子版 -->
+        <view class="sub-dim-grid" v-else>
+          <view class="sub-dim-card" @click="childSubDimClick('education')">
+            <text class="sub-dim-score">{{ wealthEducation || '-' }}</text>
+            <text class="sub-dim-label">学业成绩</text>
+            <text class="sub-dim-desc">测评综合</text>
+          </view>
+          <view class="sub-dim-card" @click="childSubDimClick('social')">
+            <text class="sub-dim-score">{{ wealthSocial || '-' }}</text>
+            <text class="sub-dim-label">社交筹码</text>
+            <text class="sub-dim-desc">活动参与</text>
+          </view>
+          <view class="sub-dim-card" @click="childSubDimClick('points')">
+            <text class="sub-dim-score">{{ wealthPoints || '-' }}</text>
+            <text class="sub-dim-label">规则博弈</text>
+            <text class="sub-dim-desc">积分效率</text>
+          </view>
         </view>
       </view>
-      <!-- 孩子版 -->
-      <view class="sub-dim-grid" v-else>
-        <view class="sub-dim-card" @click="childSubDimClick('education')">
-          <text class="sub-dim-score">{{ wealthEducation || '-' }}</text>
-          <text class="sub-dim-label">学业成绩</text>
-          <text class="sub-dim-desc">测评综合</text>
-        </view>
-        <view class="sub-dim-card" @click="childSubDimClick('social')">
-          <text class="sub-dim-score">{{ wealthSocial || '-' }}</text>
-          <text class="sub-dim-label">社交筹码</text>
-          <text class="sub-dim-desc">活动参与</text>
+
+      <!-- 心的子维度(仅孩子) -->
+      <view class="section" v-if="role !== 'parent'">
+        <view class="section-header">
+          <text class="section-title">🔥 心的子维度</text>
         </view>
-        <view class="sub-dim-card" @click="childSubDimClick('points')">
-          <text class="sub-dim-score">{{ wealthPoints || '-' }}</text>
-          <text class="sub-dim-label">规则博弈</text>
-          <text class="sub-dim-desc">积分效率</text>
+        <view class="sub-dim-grid heart-sub-dim">
+          <view class="sub-dim-card" @click="childSubDimClick('emotion')">
+            <text class="sub-dim-score">{{ heartEmotionStable || '-' }}</text>
+            <text class="sub-dim-label">情绪稳定</text>
+            <text class="sub-dim-desc">EMI情绪管理</text>
+          </view>
+          <view class="sub-dim-card" @click="childSubDimClick('understanding')">
+            <text class="sub-dim-score">{{ heartUnderstanding || '-' }}</text>
+            <text class="sub-dim-label">理解包容</text>
+            <text class="sub-dim-desc">共情+社交适应</text>
+          </view>
+          <view class="sub-dim-card" @click="childSubDimClick('legacy')">
+            <text class="sub-dim-score">{{ heartLegacy || '-' }}</text>
+            <text class="sub-dim-label">传承传递</text>
+            <text class="sub-dim-desc">未来开放</text>
+          </view>
         </view>
       </view>
-    </view>
 
-    <!-- 财富数据(仅家长) -->
-    <view class="section" v-if="isLoggedIn && role === 'parent'">
-      <view class="section-header">
-        <text class="section-title">💰 财富数据</text>
-      </view>
-      <view class="wealth-card">
-        <view class="wealth-item">
-          <text class="wealth-value">{{ wealthData.totalEarnings || 0 }}</text>
-          <text class="wealth-label">累计收益</text>
+      <!-- 财富数据(仅家长) -->
+      <view class="section" v-if="role === 'parent'">
+        <view class="section-header">
+          <text class="section-title">💰 财富数据</text>
         </view>
-        <view class="wealth-divider"></view>
-        <view class="wealth-item">
-          <text class="wealth-value">{{ wealthData.availableAmount || 0 }}</text>
-          <text class="wealth-label">可提现</text>
+        <view class="wealth-card">
+          <view class="wealth-item">
+            <text class="wealth-value">{{ wealthData.totalEarnings || 0 }}</text>
+            <text class="wealth-label">累计收益</text>
+          </view>
+          <view class="wealth-divider"></view>
+          <view class="wealth-item">
+            <text class="wealth-value">{{ wealthData.availableAmount || 0 }}</text>
+            <text class="wealth-label">可提现</text>
+          </view>
+          <view class="wealth-divider"></view>
+          <view class="wealth-item">
+            <text class="wealth-value">{{ wealthData.referralCount || 0 }}</text>
+            <text class="wealth-label">邀请人数</text>
+          </view>
         </view>
-        <view class="wealth-divider"></view>
-        <view class="wealth-item">
-          <text class="wealth-value">{{ wealthData.referralCount || 0 }}</text>
-          <text class="wealth-label">邀请人数</text>
+        <!-- 邀请码复制 -->
+        <view class="referral-code-bar" v-if="wealthData.referralCode" @click="copyReferralCode">
+          <text class="referral-code-label">我的邀请码</text>
+          <text class="referral-code-value">{{ wealthData.referralCode }}</text>
+          <text class="referral-code-copy">复制</text>
         </view>
       </view>
-      <!-- 邀请码复制 -->
-      <view class="referral-code-bar" v-if="wealthData.referralCode" @click="copyReferralCode">
-        <text class="referral-code-label">我的邀请码</text>
-        <text class="referral-code-value">{{ wealthData.referralCode }}</text>
-        <text class="referral-code-copy">复制</text>
-      </view>
-    </view>
 
-    <!-- 增值服务入口 -->
-    <view class="section" v-if="isLoggedIn">
-      <view class="section-header">
-        <text class="section-title">🚀 增值服务</text>
+      <!-- 增值服务入口 -->
+      <view class="section">
+        <view class="section-header">
+          <text class="section-title">🚀 增值服务</text>
+        </view>
+        <view class="service-grid">
+          <view class="service-item" @click="goToCheckin">
+            <text class="service-icon">📝</text>
+            <text class="service-label">记账打卡</text>
+          </view>
+          <view class="service-item" @click="goToInsurance">
+            <text class="service-icon">🛡️</text>
+            <text class="service-label">保单管理</text>
+          </view>
+          <view class="service-item" @click="goToCreator">
+            <text class="service-icon">🎨</text>
+            <text class="service-label">创客中心</text>
+          </view>
+        </view>
       </view>
-      <view class="service-grid">
-        <view class="service-item" @click="goToCheckin">
-          <text class="service-icon">📝</text>
-          <text class="service-label">记账打卡</text>
+
+      <!-- 商品推荐 -->
+      <DimensionProductList
+        v-if="isLoggedIn"
+        dimensionCode="wealth"
+        :familyId="activeChildId"
+        title="为你推荐" />
+
+      <!-- 底部菜单 -->
+      <view class="menu-list">
+        <view class="menu-item" @click="goToProfile">
+          <text>👤 个人中心</text>
+          <text class="arrow">›</text>
         </view>
-        <view class="service-item" @click="goToInsurancePlanning">
-          <text class="service-icon">📊</text>
-          <text class="service-label">保险规划</text>
+        <view class="menu-item" @click="goToPointsLogs">
+          <text>📊 积分记录</text>
+          <text class="arrow">›</text>
         </view>
-        <view class="service-item" @click="goToCreator">
-          <text class="service-icon">🎨</text>
-          <text class="service-label">创客中心</text>
+        <view class="menu-item" @click="goToPromotion">
+          <text>📣 推广中心</text>
+          <text class="arrow">›</text>
         </view>
-        <view class="service-item" @click="goToInsuranceList">
-          <text class="service-icon">🛡️</text>
-          <text class="service-label">保单管理</text>
+        <view class="menu-item logout-item" @click="logout">
+          <text>🚪 退出登录</text>
+          <text class="arrow">›</text>
         </view>
       </view>
-    </view>
-
-    <!-- 维度活动 -->
-    <DimensionActivities
-      dimensionCode="wealth"
-      :activities="dimensionActivities"
-      :isLoggedIn="isLoggedIn"
-      @activityClick="goActivityDetail"
-      @moreActivities="goMoreActivities" />
-
-    <!-- 商品推荐 -->
-    <DimensionProductList
-      v-if="isLoggedIn"
-      dimensionCode="wealth"
-      :familyId="activeChildId"
-      title="为你推荐" />
-
-    <!-- 推荐阅读 -->
-    <DimensionArticles
-      dimensionCode="wealth"
-      :articles="wealthArticles"
-      :isLoggedIn="isLoggedIn"
-      @articleClick="goArticleDetail"
-      @moreArticles="goMoreArticles" />
-
-    <!-- 底部占位 -->
-    <view class="bottom-spacer"></view>
-    <AIFloatingAvatar />
+    </template>
   </view>
 </template>
 
@@ -166,39 +188,20 @@
 import TabTransition from '../../components/tab-transition.vue'
 import PageBanner from '../../components/PageBanner.vue'
 import DimensionProductList from '../../components/DimensionProductList.vue'
-import DimensionActivities from '../../components/DimensionActivities.vue'
-import DimensionArticles from '../../components/DimensionArticles.vue'
-import FamilyEnergyBar from '../../components/FamilyEnergyBar.vue'
-import LoginGuideCard from '../../components/LoginGuideCard.vue'
-import AIFloatingAvatar from '../../components/AIFloatingAvatar.vue'
 import {
   verifyPassword,
   getChildren,
   getWealthDetail,
   getReferralCode,
   getReferralSummary,
-  getCommissionSummary,
-  getFamilyEnergySandbox,
-  getEnergyOverview,
-  getActivityList,
-  getFeaturedArticles
+  getCommissionSummary
 } from '../../utils/api.js'
 
 export default {
-  components: {
-    TabTransition,
-    PageBanner,
-    DimensionProductList,
-    DimensionActivities,
-    DimensionArticles,
-    FamilyEnergyBar,
-    LoginGuideCard,
-    AIFloatingAvatar
-  },
+  components: { TabTransition, PageBanner, DimensionProductList },
   data() {
     return {
       showTabTransition: true,
-      isLoggedIn: false,
       role: 'parent',
       isSwitchedChild: false,
       nickname: '',
@@ -208,6 +211,10 @@ export default {
       wisdomScore: 0,
       actionScore: 0,
       wealthScore: 0,
+      // 心的子维度(仅孩子有真实数据)
+      heartEmotionStable: null,
+      heartUnderstanding: null,
+      heartLegacy: null,
       // 子维度 - parent
       wealthIncome: null,
       wealthAchievement: null,
@@ -223,16 +230,13 @@ export default {
         referralCount: 0,
         referralCode: ''
       },
-      children: [],
-      // 能量条相关
-      sandboxData: null,
-      dualDimension: null,
-      // 维度活动和文章
-      dimensionActivities: [],
-      wealthArticles: []
+      children: []
     }
   },
   computed: {
+    isLoggedIn() {
+      return !!uni.getStorageSync('token')
+    },
     activeChildId() {
       if (this.role === 'child') {
         return uni.getStorageSync('currentChildId')
@@ -242,9 +246,8 @@ export default {
   },
   onShow() {
     this.showTabTransition = true
-    this.isLoggedIn = !!uni.getStorageSync('token')
 
-    if (!this.isLoggedIn) {
+    if (!uni.getStorageSync('token')) {
       this._watchPageReady()
       return
     }
@@ -263,13 +266,12 @@ export default {
     this.loadChildren()
     this.loadWealthDetail()
     this.loadWealthData()
-    this.loadSandboxData()
-    // 游客也能浏览活动和文章
-    this.loadDimensionActivities()
-    this.loadFeaturedArticles()
     this._watchPageReady()
   },
   methods: {
+    goLogin() {
+      uni.navigateTo({ url: '/pages/login/login' })
+    },
     _watchPageReady() {
       var self = this
       this.$nextTick(function() {
@@ -313,6 +315,9 @@ export default {
             this.wealthEducation = d.wealthEducation
             this.wealthSocial = d.wealthSocial
             this.wealthPoints = d.wealthPoints
+            this.heartEmotionStable = d.heartEmotionStable
+            this.heartUnderstanding = d.heartUnderstanding
+            this.heartLegacy = d.heartLegacy
           }
         }
       } catch (e) {
@@ -347,69 +352,6 @@ export default {
         console.error('获取佣金统计失败', e)
       }
     },
-    loadSandboxData() {
-      var self = this
-      getFamilyEnergySandbox().then(function(res) {
-        if (res && res.data) {
-          self.sandboxData = res.data
-        }
-      }).catch(function(e) {
-        console.log('获取沙盘数据失败', e)
-      })
-      // 加载双维度(财富与关系)
-      var childId = this.activeChildId
-      if (childId) {
-        getEnergyOverview(childId).then(function(res) {
-          if (res && res.data && res.data.dimensions) {
-            var dims = res.data.dimensions
-            for (var i = 0; i < dims.length; i++) {
-              if (dims[i].code === 'wealth') {
-                self.dualDimension = dims[i]
-                break
-              }
-            }
-          }
-        }).catch(function(e) {
-          console.log('获取能量概览失败', e)
-        })
-      }
-    },
-    loadDimensionActivities() {
-      var self = this
-      getActivityList({ dimensionCode: 'wealth', page: 1, size: 3 }).then(function(res) {
-        if (res && res.data) {
-          var list = Array.isArray(res.data) ? res.data : (res.data.records || [])
-          self.dimensionActivities = list.slice(0, 3)
-        }
-      }).catch(function() { self.dimensionActivities = [] })
-    },
-    loadFeaturedArticles() {
-      var self = this
-      getFeaturedArticles({ size: 5 }).then(function(res) {
-        if (res && res.code === 200 && res.data) {
-          var gradientColors = [
-            'linear-gradient(135deg, #F59E0B, #FBBF24)',
-            'linear-gradient(135deg, #42A5F5, #64B5F6)',
-            'linear-gradient(135deg, #FF8C42, #FFB074)',
-            'linear-gradient(135deg, #FF6B9D, #FF9DBD)',
-            'linear-gradient(135deg, #6366F1, #818CF8)'
-          ]
-          var list = Array.isArray(res.data) ? res.data : (res.data.records || [])
-          self.wealthArticles = list.map(function(item, index) {
-            return {
-              id: item.id,
-              category: item.categoryName || '财富创富',
-              categoryColor: gradientColors[index % gradientColors.length],
-              title: item.title || '',
-              summary: item.summary || '',
-              dimensionWeights: item.dimensionWeights || null,
-              views: item.viewCount ? String(item.viewCount) : '0',
-              date: item.publishedAt ? item.publishedAt.slice(0, 10) : ''
-            }
-          })
-        }
-      }).catch(function() {})
-    },
     copyReferralCode() {
       if (!this.wealthData.referralCode) return
       uni.setClipboardData({
@@ -433,7 +375,7 @@ export default {
             content: '您还没有添加孩子,是否现在去添加?',
             success: function(res) {
               if (res.confirm) {
-                uni.navigateTo({ url: '/pages/profile-extra/create-child' })
+                uni.navigateTo({ url: '/pages/profile/create-child' })
               }
             }
           })
@@ -491,18 +433,36 @@ export default {
         }
       })
     },
+    logout() {
+      uni.showModal({
+        title: '退出登录',
+        content: '确定要退出登录吗?',
+        success: function(res) {
+          if (res.confirm) {
+            uni.clearStorageSync()
+            uni.reLaunch({ url: '/pages/login/login' })
+          }
+        }
+      })
+    },
+    goToProfile() {
+      uni.navigateTo({ url: '/pages/profile/profile' })
+    },
     goToCheckin() {
       uni.navigateTo({ url: '/pages/wealth-sub/checkin' })
     },
-    goToInsurancePlanning() {
-      uni.navigateTo({ url: '/pages/wealth-sub/insurance-planning' })
-    },
-    goToInsuranceList() {
+    goToInsurance() {
       uni.navigateTo({ url: '/pages/wealth-sub/insurance-list' })
     },
     goToCreator() {
       uni.navigateTo({ url: '/pages/promotion/index' })
     },
+    goToPointsLogs() {
+      uni.navigateTo({ url: '/pages/points/points' })
+    },
+    goToPromotion() {
+      uni.navigateTo({ url: '/pages/promotion/index' })
+    },
     goToIncome() {
       uni.navigateTo({ url: '/pages/promotion/commission' })
     },
@@ -516,26 +476,15 @@ export default {
       var routes = {
         education: '/pages/assessment/results',
         social: '/pages/activity/index',
-        points: '/pages/points/mall'
+        points: '/pages/points/mall',
+        emotion: '/pages/mind-detail/emotion-report',
+        understanding: '/pages/mind-detail/emotion-trend',
+        legacy: '/pages/wishes/index'
       }
       var url = routes[key]
       if (url) {
         uni.navigateTo({ url: url })
       }
-    },
-    goActivityDetail(act) {
-      if (!act || !act.id) return
-      uni.navigateTo({ url: '/pages/discover-detail/activity-detail/activity-detail?id=' + act.id })
-    },
-    goMoreActivities() {
-      uni.navigateTo({ url: '/pages/activity/index' })
-    },
-    goArticleDetail(article) {
-      var id = article.id || article
-      uni.navigateTo({ url: '/pages/mind-detail/article-detail?id=' + id })
-    },
-    goMoreArticles() {
-      uni.navigateTo({ url: '/pages/article-center/index?dimension=wealth' })
     }
   }
 }
@@ -567,6 +516,47 @@ export default {
   color: #999;
 }
 
+/* 用户卡片 */
+.user-card {
+  display: flex;
+  align-items: center;
+  background: #fff;
+  margin: 0 30rpx 20rpx;
+  border-radius: 20rpx;
+  padding: 30rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
+}
+.avatar {
+  width: 100rpx;
+  height: 100rpx;
+  border-radius: 50%;
+  background: linear-gradient(135deg, #F59E0B, #FBBF24);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 48rpx;
+  margin-right: 24rpx;
+  flex-shrink: 0;
+}
+.user-info {
+  flex: 1;
+}
+.nickname {
+  font-size: 34rpx;
+  font-weight: bold;
+  color: #333;
+  margin-bottom: 8rpx;
+}
+.role {
+  font-size: 24rpx;
+  color: #999;
+}
+.card-arrow {
+  color: #ccc;
+  font-size: 40rpx;
+  margin-left: 16rpx;
+}
+
 /* 切换区域 */
 .switch-section {
   margin: 0 30rpx 20rpx;
@@ -665,6 +655,9 @@ export default {
   font-weight: bold;
   color: #F59E0B;
 }
+.heart-sub-dim .sub-dim-score {
+  color: #FF6B9D;
+}
 .sub-dim-label {
   font-size: 24rpx;
   color: #333;
@@ -702,6 +695,77 @@ export default {
   color: #666;
 }
 
+/* 底部菜单 */
+.menu-list {
+  margin: 30rpx 30rpx 0;
+  background: #fff;
+  border-radius: 16rpx;
+}
+.menu-item {
+  padding: 30rpx;
+  border-bottom: 1rpx solid #f0f0f0;
+  display: flex;
+  justify-content: space-between;
+  font-size: 28rpx;
+}
+.menu-item:last-child {
+  border-bottom: none;
+}
+.arrow {
+  color: #ccc;
+  font-size: 32rpx;
+}
+.logout-item {
+  color: #EF4444;
+}
+
+/* 未登录 */
+.login-prompt {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  min-height: 80vh;
+  padding: 60rpx;
+}
+.prompt-icon {
+  font-size: 120rpx;
+  margin-bottom: 30rpx;
+  width: 160rpx;
+  height: 160rpx;
+  line-height: 160rpx;
+  text-align: center;
+  background: #f5f5f5;
+  border-radius: 50%;
+}
+.prompt-title {
+  font-size: 36rpx;
+  font-weight: bold;
+  color: #333;
+  margin-bottom: 16rpx;
+}
+.prompt-desc {
+  font-size: 26rpx;
+  color: #999;
+  text-align: center;
+  margin-bottom: 40rpx;
+}
+.login-prompt .login-btn {
+  width: 60%;
+  height: 80rpx;
+  line-height: 80rpx;
+  background: linear-gradient(135deg, #5B9BD5, #3A7CC4);
+  color: #fff;
+  font-size: 30rpx;
+  font-weight: bold;
+  border-radius: 40rpx;
+  text-align: center;
+  border: none;
+}
+.login-prompt .login-btn::after {
+  border: none;
+}
+
 /* 退出切换 */
 .exit-switch {
   margin: 20rpx 30rpx 0;
@@ -712,9 +776,4 @@ export default {
   font-size: 26rpx;
   color: #92400E;
 }
-
-/* 底部占位 */
-.bottom-spacer {
-  height: 120rpx;
-}
 </style>

+ 10 - 6
cfc-frontend/pages/wisdom/index.vue

@@ -317,12 +317,16 @@ export default {
                 }
               }
             }
-            // FIX: 不再自动选择第一个孩子
-          }
-          // Load cognitive report only if activeChildId is set (user switched)
-          if (self.activeChildId) {
-            self.loadCognitiveReport()
-          }
+                // Fallback: 首次进入或未选孩子时,默认选第一个孩子以加载认知数据(仅本地状态,不触发全局切换)
+                if (!self.activeChildId && self.children.length > 0) {
+                  self.activeChildId = self.children[0].id
+                  self.activeChildName = self.children[0].nickname || self.children[0].name || ''
+                }
+            }
+            // Load cognitive report if activeChildId is available
+            if (self.activeChildId) {
+                self.loadCognitiveReport()
+            }
         }
         self.loadContacts()
       }).catch(function(e) {})

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-41ed561f1ea9afbd94c29248a6fd15d987dbdc69
+88bdfb6a014ade9a76c5d249ffafc141482e62d2

+ 2 - 2
cfc-web/package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "cfc-web",
-  "version": "1.0.410",
+  "version": "1.0.414",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "cfc-web",
-      "version": "1.0.410",
+      "version": "1.0.414",
       "dependencies": {
         "@wangeditor/editor": "^5.1.23",
         "@wangeditor/editor-for-vue": "^1.0.2",

+ 1 - 1
cfc-web/package.json

@@ -1,6 +1,6 @@
 {
   "name": "cfc-web",
-  "version": "1.0.411",
+  "version": "1.0.415",
   "private": true,
   "scripts": {
     "dev": "vue-cli-service serve",

+ 46 - 0
cfc-web/public/CHANGELOG-v1.0.md

@@ -4,6 +4,52 @@
 
 ---
 
+## v1.0.415 (2026-07-19)
+
+### 新功能
+- 行首页移除成长里程碑 - 移除OctopusGraph/milestones功能
+
+
+## v1.0.414 (2026-07-19)
+
+### Bug 修复
+- SKU编辑器适配Map类型specs + enabled Boolean转1/0
+- 新增findByProductIdAll供管理端SKU列表(无enabled过滤)
+- ProductSkuDTO specs类型从List<Map>改为Map<String,String>
+
+### 其他
+- Ultraworked with Sisyphus
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+- Ultraworked with Sisyphus
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+- Ultraworked with Sisyphus
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+
+
+## v1.0.413 (2026-07-19)
+
+### 新功能
+- 心页面添加心理报告卡片 - 完整EMID四维条形图展示
+
+
+## v1.0.412 (2026-07-19)
+
+### 新功能
+- AI任务生成+今日能量文卡片+体检AI入口
+
+### 其他
+- - AIChatController [TASK]标记解析集成
+- - FamilyContextService任务上下文注入
+- - chat.vue任务卡片渲染+报告解读模式
+- - 阅读卡片改为今日能量文并重新设计(深色渐变/辉光)
+- - 体检/菌群报告详情页AI解读FAB入口
+- - 富页面模式切换+profile返回入口
+- 
+
+
 ## v1.0.411 (2026-07-19)
 
 ### 新功能

+ 47 - 1
cfc-web/public/CHANGELOG.md

@@ -1,6 +1,6 @@
 # 更新日志
 
-> 当前版本: v1.0.411
+> 当前版本: v1.0.415
 
 ## 历史版本
 
@@ -8,6 +8,52 @@
 
 ---
 
+## v1.0.415 (2026-07-19)
+
+### 新功能
+- 行首页移除成长里程碑 - 移除OctopusGraph/milestones功能
+
+
+## v1.0.414 (2026-07-19)
+
+### Bug 修复
+- SKU编辑器适配Map类型specs + enabled Boolean转1/0
+- 新增findByProductIdAll供管理端SKU列表(无enabled过滤)
+- ProductSkuDTO specs类型从List<Map>改为Map<String,String>
+
+### 其他
+- Ultraworked with Sisyphus
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+- Ultraworked with Sisyphus
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+- Ultraworked with Sisyphus
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+
+
+## v1.0.413 (2026-07-19)
+
+### 新功能
+- 心页面添加心理报告卡片 - 完整EMID四维条形图展示
+
+
+## v1.0.412 (2026-07-19)
+
+### 新功能
+- AI任务生成+今日能量文卡片+体检AI入口
+
+### 其他
+- - AIChatController [TASK]标记解析集成
+- - FamilyContextService任务上下文注入
+- - chat.vue任务卡片渲染+报告解读模式
+- - 阅读卡片改为今日能量文并重新设计(深色渐变/辉光)
+- - 体检/菌群报告详情页AI解读FAB入口
+- - 富页面模式切换+profile返回入口
+- 
+
+
 ## v1.0.411 (2026-07-19)
 
 ### 新功能

+ 10 - 2
cfc-web/src/views/admin/components/ProductSkuEditor.vue

@@ -173,7 +173,7 @@ const baseURL = process.env.VUE_APP_BASE_API || ''
           originalPrice: sku.originalPrice ? parseFloat(sku.originalPrice) : 0,
           stock: sku.stock || 0,
           image: sku.image || '',
-          enabled: sku.enabled !== undefined ? sku.enabled : 1
+          enabled: sku.enabled !== undefined ? (sku.enabled ? 1 : 0) : 1
         }
       } else {
         this.editingId = null
@@ -210,8 +210,16 @@ const baseURL = process.env.VUE_APP_BASE_API || ''
     parseSpecs(specs) {
       if (!specs) return '-'
       try {
+        // Handle object (Map from DTO) directly
+        if (typeof specs === 'object') {
+          return Object.entries(specs).map(([k, v]) => k + ':' + v).join('; ')
+        }
+        // Handle string (JSON string)
         const obj = JSON.parse(specs)
-        return Object.entries(obj).map(([k, v]) => k + ':' + v).join('; ')
+        if (typeof obj === 'object' && obj !== null) {
+          return Object.entries(obj).map(([k, v]) => k + ':' + v).join('; ')
+        }
+        return specs
       } catch (e) {
         return specs
       }

+ 59 - 0
docs/superpowers/plans/2026-07-19-report-upload-purchase-links.md

@@ -0,0 +1,59 @@
+# 报告上传页购买链接 实施计划
+
+> **For agentic workers:** Use subagent-driven-development or executing-plans to implement this plan task-by-task.
+
+**Goal:** 在报告上传页面(健康报告 + DAN 测评)的类型选择区下方添加同类检测服务购买推荐
+
+**Architecture:** 前端调用已有 `POST /api/product/list` API(支持 `domain` + `productType` 过滤),展示匹配维度的测评商品卡片。无需后端改动。
+
+**Tech Stack:** uni-app Vue 2 (Options API), 微信小程序
+
+## Global Constraints
+
+- 禁止可选链 `?.`(用 `&&` 替代)
+- 禁止 CSS Grid(用 flexbox)
+- Vue 2 Options API
+- 调用 `POST /api/product/list` 传入 `{ domain, productType: 'assessment', size: 4 }`
+- 报告类型→domain 映射: `physical_exam/gut_flora/tongue → body`, `mind → mind`, `wisdom → wisdom`
+- 商品卡片点击跳转 `/pages/discover/product-detail/product-detail?id={id}`
+
+---
+
+### Task 1: health/report-upload.vue 添加购买推荐
+
+**Files:**
+- Modify: `cfc-frontend/pages/health/report-upload.vue`
+
+**Interfaces:**
+- Consumes: `productList` from `../../utils/api.js`
+- Produces: 类型选择器下方出现 "为家人购买检测服务" 推荐区
+
+- [x] **Step 1: 在 import 中添加 productList**
+- [x] **Step 2: 在 data() 中添加推荐商品状态**
+- [x] **Step 3: 添加报告类型→domain 映射常量**
+- [x] **Step 4: 添加 loadRecommendProducts 方法**
+- [x] **Step 5: 在 selectType 中触发推荐加载**
+- [x] **Step 6: 在 onLoad 中加载初始推荐**
+- [x] **Step 7: 在模板中添加推荐商品卡片区**
+- [x] **Step 8: 添加 goToProduct 方法和 formatPriceWithSymbol**
+- [x] **Step 9: 添加推荐区样式**
+- [x] **Step 10: 验证无语法错误**
+
+### Task 2: dan-assessment/report-upload.vue 添加购买推荐
+
+**Files:**
+- Modify: `cfc-frontend/pages/dan-assessment/report-upload.vue`
+
+**Interfaces:**
+- Consumes: `productList` from `../../utils/api.js`
+- Produces: 维度选择器下方出现 "为家人购买检测服务" 推荐区
+
+- [ ] **Step 1: 在 import 中添加 productList**
+- [ ] **Step 2: 在 data() 中添加推荐商品状态**
+- [ ] **Step 3: 添加 REPORT_DOMAIN_MAP 常量和 loadRecommendProducts**
+- [ ] **Step 4: 在 dimension 选择时触发推荐**
+- [ ] **Step 5: 在 onLoad 中加载初始推荐**
+- [ ] **Step 6: 在 Step 1 的模板中插入推荐卡片区**
+- [ ] **Step 7: 添加 goToProduct 方法和 formatPriceWithSymbol**
+- [ ] **Step 8: 添加推荐区样式**
+- [ ] **Step 9: 验证无语法错误**