Kaynağa Gözat

feat: AI任务生成+今日能量文卡片+体检AI入口

- AITaskCard组件 + accept-dynamic后端端点
- AIChatController [TASK]标记解析集成
- FamilyContextService任务上下文注入
- chat.vue任务卡片渲染+报告解读模式
- 阅读卡片改为今日能量文并重新设计(深色渐变/辉光)
- 体检/菌群报告详情页AI解读FAB入口
- 富页面模式切换+profile返回入口
Xiaogang Liao 2 ay önce
ebeveyn
işleme
149dbf73f4

+ 42 - 16
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,8 +14,11 @@ 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,
@@ -22,17 +27,38 @@ public class GrowthTaskController {
         return Result.success(growthTaskService.getTaskList(userId, type));
     }
 
-    @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);
+ }
+ }

+ 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);
     }
 
     /**

+ 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);
             }
         }

+ 17 - 2
cfc-backend/src/main/java/com/etotem/cfc/service/GrowthTaskService.java

@@ -250,7 +250,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;
         }
-    }
 }

+ 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
   },

+ 79 - 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]

+ 1 - 189
cfc-frontend/pages/body/index.vue

@@ -82,42 +82,6 @@
       </view>
     </view>
 
-    <!-- 健康报告摘要(登录后可见) -->
-    <view class="section" v-if="sectionVisible('health_report') && isLoggedIn">
-      <view class="section-header">
-        <text class="section-title">健康报告</text>
-        <text class="section-more" @click="goToHealthReport">查看详情 ›</text>
-      </view>
-      <view class="report-card" v-if="latestReport">
-        <view class="report-type-tag">{{ reportTypeLabel }}</view>
-        <view class="report-score-row">
-          <view class="score-circle">
-            <text class="score-value">{{ latestReport.overallScore || '--' }}</text>
-            <text class="score-label">综合评分</text>
-          </view>
-          <view class="report-meta">
-            <view class="meta-item" v-if="latestReport.gutHealthScore">
-              <text class="meta-label">肠道健康</text>
-              <text class="meta-value">{{ latestReport.gutHealthScore }}</text>
-            </view>
-            <view class="meta-item" v-if="latestReport.nutritionScore">
-              <text class="meta-label">营养状况</text>
-              <text class="meta-value">{{ latestReport.nutritionScore }}</text>
-            </view>
-            <view class="meta-item" v-if="latestReport.gutAge">
-              <text class="meta-label">肠道年龄</text>
-              <text class="meta-value">{{ latestReport.gutAge }}</text>
-            </view>
-          </view>
-        </view>
-        <text class="report-date">报告日期:{{ formatDate(latestReport.reportDate) }}</text>
-      </view>
-      <view class="report-empty" v-else>
-        <text class="empty-tip">暂无健康报告</text>
-        <text class="empty-sub">规划师录入后将自动显示</text>
-      </view>
-    </view>
-
     <!-- ===== 精准营养(上传报告+产品+文章) ===== -->
     <view class="section nutrition-section" v-if="isLoggedIn">
       <view class="section-header">
@@ -199,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 },
@@ -243,7 +179,7 @@ export default {
       sandboxData: null,
       dualDimension: null,
       visibleSections: [],
-      latestReport: null,
+
       dimensionTasks: [],
       dimensionActivities: [],
       dimensionProducts: [],
@@ -261,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) {
@@ -380,7 +312,6 @@ export default {
     },
     loadDashboardData: function() {
       if (!this.activeChildId) return
-      this.loadHealthReport()
       this.loadEnergyData()
       this.loadDimensionTasks()
       this.loadDimensionActivities()
@@ -416,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) {
@@ -571,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 })
     },
@@ -602,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() {
@@ -706,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

@@ -355,6 +355,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('请根据这份肠道菌群检测报告,分析我孩子的肠道健康状况并提出建议')
+      })
     }
   }
 }
@@ -451,4 +457,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 {

+ 169 - 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>
 
@@ -1490,14 +1534,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>

+ 11 - 11
cfc-frontend/pages/wealth/index.vue

@@ -17,12 +17,17 @@
         tagline="利他创富 · 价值共享"
         quote="富足源于利他与坚持" />
 
-      <!-- 退出切换按钮 -->
-      <view class="exit-switch" v-if="isSwitchedChild" @click="exitSwitch">
-        <text>🔄 退出切换</text>
-      </view>
+      <!-- 模式切换 / 退出切换入口(家长未切时显示"切换到孩子视角";已切时显示"退出切换") -->
+ <!-- 已切孩子态:点击"退出切换"回到家长模式 -->
+ <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="user-card" @click="goToProfile">
         <view class="avatar">👤</view>
         <view class="user-info">
@@ -32,12 +37,7 @@
         <text class="card-arrow">›</text>
       </view>
 
-      <!-- 切换按钮(家长模式 + 未切换状态) -->
-      <view class="switch-section" v-if="role === 'parent' && !isSwitchedChild">
-        <button class="btn-switch" @click="onSwitchMode">切换到孩子视角</button>
-      </view>
-
-      <!-- 财富子维度卡片 -->
+<!-- 财富子维度卡片 -->
       <view class="section">
         <view class="section-header">
           <text class="section-title">💎 财富子维度</text>

+ 2 - 2
cfc-web/.env.production

@@ -1,2 +1,2 @@
-VUE_APP_BASE_API=api
-VUE_APP_SKIP_CAPTCHA=true
+VUE_APP_BASE_API=
+VUE_APP_SKIP_CAPTCHA=false