Przeglądaj źródła

Merge branch 'refs/heads/cfclub' into cfclub-jiapu

# Conflicts:
#	cfc-frontend/components/ContactCard.vue
#	cfc-frontend/pages/butler/apply.vue
jiapu 2 miesięcy temu
rodzic
commit
3e02aa55ff

+ 96 - 16
cfc-backend/src/main/java/com/etotem/cfc/controller/mind/MindFortuneController.java

@@ -2,9 +2,12 @@ package com.etotem.cfc.controller.mind;
 
 import com.etotem.cfc.common.Result;
 import com.etotem.cfc.entity.FamilyFortune;
+import com.etotem.cfc.entity.FamilyFortuneReport;
 import com.etotem.cfc.service.FortuneService;
+import com.etotem.cfc.service.PdfReportService;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestAttribute;
+import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.*;
 import javax.annotation.Resource;
@@ -19,39 +22,116 @@ public class MindFortuneController {
     @Resource
     private FortuneService fortuneService;
     
+    @Resource
+    private PdfReportService pdfReportService;
+    
     /**
      * 获取今日家庭运势
-     * @param familyId 家庭ID
-     * @return 家庭运势信息
      */
     @PostMapping("/fortune")
     public Result<Map<String, Object>> getTodayFortune(@RequestAttribute("familyId") Long familyId) {
         FamilyFortune fortune = fortuneService.getFamilyFortune(familyId, LocalDate.now());
-        Map<String, Object> result = new HashMap<>();
-        result.put("dominantElement", fortune.getDominantElement());
-        result.put("luckyDirection", fortune.getLuckyDirection() != null ? fortune.getLuckyDirection().replace("方", "") : "东");
-        result.put("fortuneLevel", fortune.getFortuneLevel());
-        result.put("fortuneLevelText", fortune.getFortuneLevelText());
-        result.put("fortuneDescription", fortune.getFortuneDescription());
-        result.put("moodScore", fortune.getMoodScore());
-        result.put("moodEmoji", fortune.getMoodEmoji());
-        result.put("moodText", fortune.getMoodText());
-        result.put("dailyTip", fortune.getDailyTip());
-        result.put("weeklyTip", fortune.getWeeklyTip());
+        Map<String, Object> result = batchBuildFortuneResponse(fortune);
         return Result.success(result);
     }
     
     /**
      * 手动刷新运势(仅供测试)
-     * @param familyId 家庭ID
-     * @return 刷新后的运势信息
      */
     @PostMapping("/fortune/refresh")
     public Result<FamilyFortune> refreshFortune(@RequestAttribute("familyId") Long familyId) {
         FamilyFortune fortune = fortuneService.getFamilyFortune(familyId, LocalDate.now());
-        // 强制重新计算
         fortune.setLuckyDirection(fortuneService.calculateLuckyDirection(familyId, LocalDate.now()));
         fortuneService.saveFamilyFortune(fortune);
         return Result.success(fortune);
     }
+    
+    /**
+     * 天盘周报导出 - 生成 PDF 报告
+     * @param request 包含 Base64 图片、运势数据、家庭信息
+     * @return PDF 文件URL
+     */
+    @PostMapping("/fortune/export")
+    public Result<Map<String, Object>> exportFortuneReport(
+            @RequestAttribute("familyId") Long familyId,
+            @RequestBody ExportReportRequest request) {
+        try {
+            // 文件大小校验
+            if (request.imageBase64 == null || request.imageBase64.length() > 4 * 1024 * 1024) {
+                return Result.error("图片数据无效或超过4MB");
+            }
+            
+            // 生成 PDF 文件
+            byte[] pdfBytes = pdfReportService.generatePdfReport(
+                request.imageBase64, 
+                request.element, 
+                request.luckyDirection, 
+                request.tip
+            );
+            
+            // 保存到本地
+            String fileName = pdfReportService.savePdfReport(pdfBytes, familyId, request.element);
+            
+            // 记录到数据库
+            FamilyFortuneReport report = new FamilyFortuneReport();
+            report.setFamilyId(Math.toIntExact(familyId));
+            report.setElement(request.element);
+            report.setLuckyDirection(request.luckyDirection);
+            report.setPdfPath(fileName);
+            report.setCreatedAt(new java.util.Date());
+            pdfReportService.saveReport(record -> {
+                record.setFamilyId(report.getFamilyId());
+                record.setElement(report.getElement());
+                record.setLuckyDirection(report.getLuckyDirection());
+                record.setPdfPath(report.getPdfPath());
+                record.setCreatedAt(report.getCreatedAt());
+                return record;
+            });
+            
+            // 返回响应
+            Map<String, Object> response = new HashMap<>();
+            response.put("pdfUrl", "/storage/reports/" + fileName);
+            response.put("fileName", fileName);
+            response.put("fileSize", pdfBytes.length);
+            response.put("element", request.element);
+            
+            return Result.success(response);
+        } catch (Exception e) {
+            return Result.error("PDF 生成失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 请求数据封装
+     */
+    public static class ExportReportRequest {
+        private String imageBase64;
+        private String element;
+        private String luckyDirection;
+        private String tip;
+        
+        public String getImageBase64() { return imageBase64; }
+        public void setImageBase64(String imageBase64) { this.imageBase64 = imageBase64; }
+        public String getElement() { return element; }
+        public void setElement(String element) { this.element = element; }
+        public String getLuckyDirection() { return luckyDirection; }
+        public void setLuckyDirection(String luckyDirection) { this.luckyDirection = luckyDirection; }
+        public String getTip() { return tip; }
+        public void setTip(String tip) { this.tip = tip; }
+    }
+    
+    private Map<String, Object> batchBuildFortuneResponse(FamilyFortune fortune) {
+        Map<String, Object> result = new HashMap<>();
+        result.put("dominantElement", fortune.getDominantElement());
+        result.put("luckyDirection", fortune.getLuckyDirection() != null ? fortune.getLuckyDirection().replace("方", "") : "东");
+        result.put("fortuneLevel", fortune.getFortuneLevel());
+        result.put("fortuneLevelText", fortune.getFortuneLevelText());
+        result.put("fortuneDescription", fortune.getFortuneDescription());
+        result.put("moodScore", fortune.getMoodScore());
+        result.put("moodEmoji", fortune.getMoodEmoji());
+        result.put("moodText", fortune.getMoodText());
+        result.put("dailyTip", fortune.getDailyTip());
+        result.put("weeklyTip", fortune.getWeeklyTip());
+        return result;
+    }
 }

+ 20 - 20
cfc-frontend/components/ContactCard.vue

@@ -1,5 +1,5 @@
 <template>
-  <view>
+  <view class="contact-card-root">
     <view class="contact-card" @click="$emit('click', contact)">
       <view class="card-avatar" :style="{ background: avatarColor }">
         <text class="avatar-text">{{ initial }}</text>
@@ -27,25 +27,25 @@
       </view>
     </view>
     <view class="invite-modal" v-if="showInviteModal" @click.stop>
-    <view class="modal-mask" @click="showInviteModal = false"></view>
-    <view class="modal-content">
-      <text class="modal-title">邀请加入家庭</text>
-      <view class="form-row">
-        <text class="form-label">关系类型</text>
-        <picker :range="relationshipTypeList" range-key="typeName" @change="onRelationshipChange">
-          <text class="form-value">{{ selectedRelationshipName || '请选择' }}</text>
-        </picker>
-      </view>
-      <view class="form-row">
-        <text class="form-label">辈分等级</text>
-        <picker :range="generationLevelList" @change="onGenerationChange">
-          <text class="form-value">{{ selectedGeneration || '请选择' }}</text>
-        </picker>
-      </view>
-      <view class="modal-buttons">
-        <view class="modal-btn cancel" @click="showInviteModal = false">取消</view>
-        <view class="modal-btn confirm" @click="confirmInvite">确认邀请</view>
-      </view>
+      <view class="modal-mask" @click="showInviteModal = false"></view>
+      <view class="modal-content">
+        <text class="modal-title">邀请加入家庭</text>
+        <view class="form-row">
+          <text class="form-label">关系类型</text>
+          <picker :range="relationshipTypeList" range-key="typeName" @change="onRelationshipChange">
+            <text class="form-value">{{ selectedRelationshipName || '请选择' }}</text>
+          </picker>
+        </view>
+        <view class="form-row">
+          <text class="form-label">辈分等级</text>
+          <picker :range="generationLevelList" @change="onGenerationChange">
+            <text class="form-value">{{ selectedGeneration || '请选择' }}</text>
+          </picker>
+        </view>
+        <view class="modal-buttons">
+          <view class="modal-btn cancel" @click="showInviteModal = false">取消</view>
+          <view class="modal-btn confirm" @click="confirmInvite">确认邀请</view>
+        </view>
     </view>
   </view>
   </view>

+ 108 - 30
cfc-frontend/components/FamilyTianpanCard.vue

@@ -1,6 +1,6 @@
 <template>
   <!-- 家庭天盘卡片 — 心维度专属 (#FF6B9D 火) -->
-  <view class="tianpan-card" v-if="visible">
+  <view class="tianpan-card" v-if="visible" :style="themeStyle">
     <!-- 卡片头部 -->
     <view class="tp-header">
       <view class="tp-header-left">
@@ -189,6 +189,26 @@ export default {
     luckyElementColor: function() {
       var dir = this.luckyDirection
       return this.dirColorMap[dir] || '#FF6B9D'
+    },
+    themeStyle: function() {
+      var color = this.themeColor || '#FF6B9D'
+      var lighter = color + '15'
+      var borderColor = color + '20'
+      var shadowColor = color + '25'
+      return {
+        '--theme-color': color,
+        '--theme-lighter': lighter,
+        '--theme-border': borderColor,
+        '--theme-shadow': shadowColor,
+        '--theme-gradient': 'linear-gradient(135deg, ' + color + ', ' + color + 'CC)'
+      }
+    },
+    isChild: function() {
+      try {
+        return this.$store && this.$store.state && this.$store.state.role === 'child'
+      } catch (e) {
+        return false
+      }
     }
   },
   watch: {
@@ -312,31 +332,33 @@ export default {
     startAnimation: function() {
       var self = this
       if (this._animFrameId) {
-        cancelAnimationFrame(this._animFrameId)
+        clearTimeout(this._animFrameId)
+        this._animFrameId = null
       }
       
-      var animFrameId = null
       var lastTime = 0
+      var fps = 30 // 小程序 Canvas 30fps 即可保证流畅
+      var interval = 1000 / fps
       
-      function animationLoop(timestamp) {
+      function animationLoop() {
         if (!self.canvasCtx || !self.canvasReady) {
-          cancelAnimationFrame(animFrameId)
+          self._animFrameId = null
           return
         }
         
-        // 控制帧率
-        if (timestamp - lastTime < 16) {
-          animFrameId = requestAnimationFrame(animationLoop)
+        var now = Date.now()
+        if (now - lastTime < interval) {
+          self._animFrameId = setTimeout(animationLoop, 16)
           return
         }
-        lastTime = timestamp
+        lastTime = now
         
         self.clearCanvas()
-        self.drawCompassWithAnimation(timestamp)
-        animFrameId = requestAnimationFrame(animationLoop)
+        self.drawCompassWithAnimation(now)
+        self._animFrameId = setTimeout(animationLoop, 16)
       }
       
-      this._animFrameId = requestAnimationFrame(animationLoop)
+      animationLoop()
     },
 
     drawCompass: function() {
@@ -375,13 +397,21 @@ export default {
 
       var luckyDir = this.luckyDirection.replace('方', '')
       var luckyColor = this.luckyElementColor
+      
+      // hex转rgba辅助
+      function hexToRgba(hex, alpha) {
+        var r = parseInt(hex.slice(1, 3), 16)
+        var g = parseInt(hex.slice(3, 5), 16)
+        var b = parseInt(hex.slice(5, 7), 16)
+        return 'rgba(' + r + ',' + g + ',' + b + ',' + alpha + ')'
+      }
 
       // 外圈背景
       ctx.beginPath()
       ctx.arc(cx, cy, outerR, 0, Math.PI * 2)
       ctx.fillStyle = '#FFF5F7'
       ctx.fill()
-      ctx.strokeStyle = luckyColor + '40'
+      ctx.strokeStyle = hexToRgba(luckyColor, 0.25)
       ctx.lineWidth = 1.5
       ctx.stroke()
 
@@ -396,7 +426,7 @@ export default {
         var ex = cx + outerR * Math.cos(d.angle)
         var ey = cy + outerR * Math.sin(d.angle)
         ctx.lineTo(ex, ey)
-        ctx.strokeStyle = color + '30'
+        ctx.strokeStyle = hexToRgba(color, 0.19)
         ctx.lineWidth = 1
         ctx.stroke()
 
@@ -410,7 +440,7 @@ export default {
           // 吉位高亮圆
           ctx.beginPath()
           ctx.arc(lx, ly, 12, 0, Math.PI * 2)
-          ctx.fillStyle = luckyColor + '20'
+          ctx.fillStyle = hexToRgba(luckyColor, 0.13)
           ctx.fill()
           ctx.strokeStyle = luckyColor
           ctx.lineWidth = 1.5
@@ -432,6 +462,54 @@ export default {
       ctx.strokeStyle = '#FFFFFF'
       ctx.lineWidth = 1.5
       ctx.stroke()
+    },
+
+    handleCompassClick: function(e) {
+      if (!this.isChild || !this.fortune) return
+      
+      var touch = e.touches && e.touches[0]
+      if (!touch) return
+      
+      var self = this
+      uni.createSelectorQuery().in(this).select('#tianpanCompass').boundingClientRect(function(rect) {
+        if (!rect) return
+        var touchX = touch.clientX - rect.left
+        var touchY = touch.clientY - rect.top
+        var cx = self._compassW / 2
+        var cy = self._compassH / 2
+        var radius = Math.min(cx, cy) - 4
+        
+        // 判断点击区域
+        var elements = [
+          { label: '东', description: '家庭成长力', name: '木' },
+          { label: '南', description: '温馨家庭情', name: '火' },
+          { label: '西', description: '和谐人际关系', name: '金' },
+          { label: '北', description: '智慧与理性', name: '水' },
+          { label: '中', description: '稳固与安定', name: '土' }
+        ]
+        
+        for (var i = 0; i < elements.length; i++) {
+          var elem = elements[i]
+          var angle = elem.label === '东' ? 0 : elem.label === '南' ? Math.PI / 2 : elem.label === '西' ? Math.PI : elem.label === '北' ? -Math.PI / 2 : null
+          if (angle === null) continue
+          
+          var labelR = radius * 0.7
+          var lx = cx + labelR * Math.cos(angle)
+          var ly = cy + labelR * Math.sin(angle)
+          var dist = Math.sqrt(Math.pow(touchX - lx, 2) + Math.pow(touchY - ly, 2))
+          
+          if (dist <= 20) {
+            uni.showModal({
+              title: elem.name + '·' + elem.label + '方',
+              content: '这里是家里' + elem.name + '能量最旺的方向!\n' + elem.description + ',记得多在这个方向活动哦~',
+              confirmText: '知道啦',
+              showCancel: false,
+              confirmColor: self.themeColor || '#FF6B9D'
+            })
+            break
+          }
+        }
+      }).exec()
     }
   },
 }
@@ -443,10 +521,10 @@ export default {
   background: #FFFFFF;
   border-radius: 24rpx;
   padding: 28rpx 24rpx;
-  /* Claymorphism 双阴影 */
-  box-shadow: 0 4rpx 16rpx rgba(255, 107, 157, 0.12),
+  /* 动态主题阴影 */
+  box-shadow: 0 4rpx 16rpx var(--theme-shadow, rgba(255, 107, 157, 0.12)),
               0 2rpx 4rpx rgba(0, 0, 0, 0.04);
-  border: 1rpx solid rgba(255, 107, 157, 0.08);
+  border: 1rpx solid var(--theme-border, rgba(255, 107, 157, 0.08));
   overflow: hidden;
 }
 
@@ -458,7 +536,7 @@ export default {
   justify-content: space-between;
   margin-bottom: 24rpx;
   padding-bottom: 20rpx;
-  border-bottom: 1rpx solid rgba(255, 107, 157, 0.10);
+  border-bottom: 1rpx solid var(--theme-border, rgba(255, 107, 157, 0.10));
 }
 .tp-header-left {
   display: flex;
@@ -469,12 +547,12 @@ export default {
   width: 64rpx;
   height: 64rpx;
   border-radius: 20rpx;
-  background: linear-gradient(135deg, #FF6B9D, #FF8FB1);
+  background: var(--theme-gradient, linear-gradient(135deg, #FF6B9D, #FF8FB1));
   display: flex;
   align-items: center;
   justify-content: center;
   margin-right: 16rpx;
-  box-shadow: 0 4rpx 12rpx rgba(255, 107, 157, 0.30);
+  box-shadow: 0 4rpx 12rpx var(--theme-shadow, rgba(255, 107, 157, 0.30));
 }
 .tp-icon-text {
   font-size: 32rpx;
@@ -498,14 +576,14 @@ export default {
   width: 56rpx;
   height: 56rpx;
   border-radius: 28rpx;
-  background: #FFF0F3;
+  background: var(--theme-lighter, #FFF0F3);
   display: flex;
   align-items: center;
   justify-content: center;
 }
 .tp-action-text {
   font-size: 28rpx;
-  color: #FF6B9D;
+  color: var(--theme-color, #FF6B9D);
   font-weight: 600;
 }
 .tp-header-action:active {
@@ -613,7 +691,7 @@ export default {
 }
 .tp-mood-bar-fill {
   height: 100%;
-  background: linear-gradient(90deg, #FF6B9D, #FF8FB1);
+  background: var(--theme-gradient, linear-gradient(90deg, #FF6B9D, #FF8FB1));
   border-radius: 5rpx;
   transition: width 0.5s;
 }
@@ -631,7 +709,7 @@ export default {
 .tp-direction-value {
   font-size: 24rpx;
   font-weight: 700;
-  color: #FF6B9D;
+  color: var(--theme-color, #FF6B9D);
 }
 
 /* ===== 底部提示 ===== */
@@ -641,7 +719,7 @@ export default {
   align-items: flex-start;
   margin-top: 20rpx;
   padding: 16rpx;
-  background: #FFF5F0;
+  background: var(--theme-lighter, #FFF5F0);
   border-radius: 14rpx;
 }
 .tp-footer-icon {
@@ -674,13 +752,13 @@ export default {
 }
 .tp-advice-icon {
   font-size: 24rpx;
-  color: #FF6B9D;
+  color: var(--theme-color, #FF6B9D);
   margin-right: 12rpx;
 }
 .tp-advice-label {
   font-size: 22rpx;
   font-weight: 600;
-  color: #FF6B9D;
+  color: var(--theme-color, #FF6B9D);
   margin-right: 12rpx;
   min-width: 60rpx;
 }
@@ -696,7 +774,7 @@ export default {
   margin-top: 16rpx;
   text-align: center;
   padding: 14rpx;
-  background: linear-gradient(135deg, #FFF0F3, #FFE0E8);
+  background: var(--theme-lighter, linear-gradient(135deg, #FFF0F3, #FFE0E8));
   border-radius: 14rpx;
 }
 .tp-detail-btn:active {
@@ -704,7 +782,7 @@ export default {
 }
 .tp-detail-text {
   font-size: 24rpx;
-  color: #FF6B9D;
+  color: var(--theme-color, #FF6B9D);
   font-weight: 600;
 }
 </style>

+ 1 - 1
cfc-frontend/pages/butler/apply/index.vue → cfc-frontend/pages/butler/apply.vue

@@ -41,7 +41,7 @@
 </template>
 
 <script>
-import { butlerApply } from '../../../utils/api'
+import { butlerApply } from '@/utils/api'
 
 export default {
   data() {

+ 6 - 3
cfc-frontend/pages/mind-detail/family-dashboard.vue

@@ -1,7 +1,10 @@
 <template>
-  <!-- 家庭天盘详情页 -->
-  <view class="family-dashboard">
-    <!-- 头部导航 -->
+<!-- 家庭天盘详情页 -->
+<view class="family-dashboard">
+<!-- PDF 导出用隐藏 canvas -->
+<canvas canvas-id="weeklyReportCanvas" id="weeklyReportCanvas" style="width: 1px; height: 1px; position: absolute; top: -1000rpx;"></canvas>
+
+<!-- 头部导航 -->
     <view class="dashboard-header">
       <view class="dashboard-icon" @click="goBack">
         <text class="dashboard-icon-text">&#x2190;</text>

+ 133 - 40
cfc-frontend/pages/mind/index.vue

@@ -14,6 +14,15 @@
       <text class="section-title-text">心·心理</text>
     </view>
 
+    <!-- ===== 家庭天盘模块(今日运势 / 今日心情 / 吉位) ===== -->
+    <FamilyTianpanCard
+      v-if="isLoggedIn"
+      :visible="isLoggedIn"
+      :fortune="dailyFortune"
+      :mood="todayMood"
+      :dominantElement="dominantElement"
+      :weeklyTip="familyWeeklyInsight" />
+
     <!-- 游客登录引导卡 -->
     <template v-if="!isLoggedIn">
       <LoginGuideCard
@@ -61,40 +70,37 @@
       </view>
     </view>
 
-    <!-- ===== 大五人格雷达图(始终显示) ===== -->
-    <view class="emi-section">
-      <view class="emi-card">
-        <view class="personality-header">
-          <text class="emi-card-title">大五人格</text>
-          <view class="test-btn" @click="goAssessment">
-            <text class="test-btn-text">测试</text>
+    <!-- ===== 心理报告上传 ===== -->
+    <view class="report-upload-section" v-if="isLoggedIn">
+      <view class="report-upload-card">
+        <view class="report-upload-header">
+          <text class="report-upload-title">心理报告</text>
+          <view class="report-upload-btn" @click="chooseAndUploadReport">
+            <text class="report-upload-btn-text">上传报告</text>
           </view>
         </view>
-        <template v-if="emiData">
-          <RadarChart
-            :dimensions="personalityDimensions"
-            :scores="personalityScores"
-            :childName="currentChildName || '孩子'"
-            fillColor="#8B5CF6"
-            gridColor="#EDE9FE"
-            labelColor="#6D28D9" />
-          <view class="emi-footer">
-            <text class="emi-overall">综合EQ分: {{ emiData.overallScore || 0 }}</text>
-            <view class="emi-link" @click="goEmotionReport">
-              <text class="emi-link-text">查看完整报告</text>
-              <text class="emi-link-arrow">&#x2192;</text>
-            </view>
+        <view class="report-upload-body">
+          <text class="report-upload-desc">上传 EMI 心理测评报告或第三方心理评估报告,系统将自动解析并生成可视化分析</text>
+          <view class="report-support-list">
+            <text class="report-support-item">支持格式: PDF、JPG、PNG</text>
+            <text class="report-support-item">单文件不超过 20MB</text>
           </view>
-        </template>
-        <template v-else-if="!emiLoading">
-          <view class="placeholder-content">
-            <text class="placeholder-icon">&#x1F9E0;</text>
-            <text class="placeholder-title">心理评估</text>
-            <text class="placeholder-desc">完成 EMI 心理测评后,可在此查看大五人格雷达图</text>
-            <view class="placeholder-btn" @click="goAssessment" v-if="isLoggedIn">预约测评</view>
-            <view class="placeholder-btn" @click="goLogin" v-else>立即登录</view>
+        </view>
+        <!-- 已上传报告列表 -->
+        <view class="report-list" v-if="uploadedReports && uploadedReports.length > 0">
+          <view class="report-item" v-for="report in uploadedReports" :key="report.id" @click="viewReport(report)">
+            <text class="report-item-icon">&#x1F4C4;</text>
+            <text class="report-item-name">{{ report.fileName || '心理报告' }}</text>
+            <text class="report-item-date">{{ formatDate(report.createdAt) }}</text>
+            <text class="report-item-status" :class="report.status === 'parsed' ? 'parsed' : 'pending'">
+              {{ report.status === 'parsed' ? '已解析' : '待解析' }}
+            </text>
           </view>
-        </template>
+        </view>
+        <view class="report-empty" v-else-if="!reportUploadLoading">
+          <text class="report-empty-icon">&#x1F4C1;</text>
+          <text class="report-empty-text">暂无上传的报告</text>
+        </view>
       </view>
     </view>
 
@@ -294,15 +300,6 @@
       </view>
     </view>
 
-    <!-- ===== 家庭天盘模块(今日运势 / 今日心情 / 吉位) ===== -->
-    <FamilyTianpanCard
-      v-if="isLoggedIn"
-      :visible="isLoggedIn"
-      :fortune="dailyFortune"
-      :mood="todayMood"
-      :dominantElement="dominantElement"
-      :weeklyTip="familyWeeklyInsight" />
-
     <!-- 底部占位 -->
     <view class="bottom-spacer"></view>
     <AIFloatingAvatar />
@@ -407,7 +404,11 @@ export default {
     },
     
     // 家庭天盘 - 每日运势数据
-    dailyFortuneData: null
+    dailyFortuneData: null,
+
+    // 心理报告上传
+    uploadedReports: [],
+    reportUploadLoading: false
     }
   },
   computed: {
@@ -673,6 +674,98 @@ export default {
             console.error('Failed to load family fortune:', err)
         })
     },
+
+    // ===== 心理报告上传 =====
+    chooseAndUploadReport: function() {
+      var self = this
+      uni.chooseImage({
+        count: 1,
+        sizeType: ['original', 'compressed'],
+        sourceType: ['album', 'camera'],
+        success: function(chooseRes) {
+          self.reportUploadLoading = true
+          var tempFilePath = chooseRes.tempFilePaths[0]
+          var fileName = '心理报告_' + Date.now() + '.jpg'
+
+          uni.uploadFile({
+            url: config.baseUrl + '/api/mind/report/upload',
+            filePath: tempFilePath,
+            name: 'report',
+            formData: {
+              familyId: self.currentChildId || ''
+            },
+            header: {
+              'Authorization': 'Bearer ' + uni.getStorageSync('token')
+            },
+            success: function(uploadRes) {
+              self.reportUploadLoading = false
+              if (uploadRes.statusCode === 200) {
+                var result = JSON.parse(uploadRes.data)
+                if (result.code === 200) {
+                  self.uploadedReports.unshift(result.data)
+                  uni.showToast({ title: '上传成功', icon: 'success' })
+                } else {
+                  uni.showToast({ title: result.message || '上传失败', icon: 'none' })
+                }
+              } else {
+                uni.showToast({ title: '上传失败: ' + uploadRes.statusCode, icon: 'none' })
+              }
+            },
+            fail: function(err) {
+              self.reportUploadLoading = false
+              uni.showToast({ title: '上传失败: ' + (err.errMsg || ''), icon: 'none' })
+            }
+          })
+        },
+        fail: function() {
+          // 用户取消选择
+        }
+      })
+    },
+
+    viewReport: function(report) {
+      if (report && report.fileUrl) {
+        uni.downloadFile({
+          url: report.fileUrl,
+          success: function(res) {
+            if (res.statusCode === 200) {
+              uni.openDocument({
+                filePath: res.tempFilePath,
+                success: function() {
+                  console.log('打开报告成功')
+                }
+              })
+            }
+          }
+        })
+      }
+    },
+
+    loadUploadedReports: function() {
+      var self = this
+      uni.request({
+        url: config.baseUrl + '/api/mind/report/list',
+        method: 'POST',
+        data: { familyId: self.currentChildId || '' },
+        header: {
+          'Authorization': 'Bearer ' + uni.getStorageSync('token')
+        },
+        success: function(res) {
+          if (res.data && res.data.code === 200 && res.data.data) {
+            self.uploadedReports = res.data.data
+          }
+        }
+      })
+    },
+
+    formatDate: function(dateStr) {
+      if (!dateStr) return ''
+      var d = new Date(dateStr)
+      var month = d.getMonth() + 1
+      var day = d.getDate()
+      return month + '月' + day + '日'
+    },
+
     sectionVisible: function(key) {
       return this.visibleSections.length === 0 || this.visibleSections.indexOf(key) !== -1
     },

+ 1 - 1
cfc-frontend/pages/nutritionist/apply/index.vue → cfc-frontend/pages/nutritionist/apply.vue

@@ -30,7 +30,7 @@
 </template>
 
 <script>
-import { nutritionistApply } from '../../../utils/api'
+import { nutritionistApply } from '@/utils/api'
 
 export default {
   data() {

+ 2 - 56
cfc-frontend/pages/profile/components/ProfileHeader.vue

@@ -1,13 +1,5 @@
 <template>
   <view>
-    <!-- 富沛能量值 -->
-    <view class="wealth-energy-header" v-if="isLoggedIn && wealthEnergy > 0">
-      <text class="energy-header-icon">💧</text>
-      <text class="energy-header-value">{{ wealthEnergy }}</text>
-      <text class="energy-header-label">富沛能量</text>
-      <text class="energy-header-trend" v-if="energyTrend > 0">↑较上周+{{ energyTrend }}</text>
-    </view>
-
     <!-- 退出切换按钮(仅家长切换到孩子状态时显示) -->
     <view class="exit-switch" v-if="isSwitchedChild" @click="onExitSwitch">
       <text>🔄 退出切换</text>
@@ -19,11 +11,6 @@
         <view class="nickname">{{ nickname || '未设置昵称' }}</view>
         <view class="role">{{ role === 'parent' ? '家长模式' : (role === 'child' ? '孩子模式' : '成长规划师模式') }}</view>
         <view class="system-points" v-if="role === 'parent' && children.length > 0">🏅 系统积分: {{ children[0].systemPoints || 0 }}</view>
-        <view class="wealth-energy-row" v-if="wealthEnergy > 0">
-          <text class="wealth-energy-icon">💧</text>
-          <text class="wealth-energy-value">富沛能量: {{ wealthEnergy }}</text>
-          <text class="wealth-energy-trend" v-if="energyTrend > 0"> ↑较上周+{{ energyTrend }}</text>
-        </view>
       </view>
       <!-- 切换按钮仅在非切换状态下显示 -->
       <button class="btn-switch" v-if="!isSwitchedChild" @click="onSwitchMode">切换</button>
@@ -32,7 +19,7 @@
 </template>
 
 <script>
-import { getChildren, getEnergyOverview, verifyPassword } from '../../../utils/api.js'
+import { getChildren, verifyPassword } from '../../../utils/api.js'
 
 export default {
   name: 'ProfileHeader',
@@ -41,9 +28,7 @@ export default {
       nickname: '',
       role: 'parent',
       children: [],
-      isSwitchedChild: false,
-      wealthEnergy: 0,
-      energyTrend: 0
+      isSwitchedChild: false
     }
   },
   computed: {
@@ -59,7 +44,6 @@ export default {
     this.role = currentRole
     this.isSwitchedChild = uni.getStorageSync('isSwitchedChild') || false
     this.loadChildren()
-    this.loadEnergyData()
   },
   methods: {
     async loadChildren() {
@@ -70,26 +54,6 @@ export default {
         console.error('获取孩子列表失败', e)
       }
     },
-    loadEnergyData() {
-      let childId = uni.getStorageSync('currentChildId') || null
-      if (!childId && this.children.length > 0) {
-        childId = this.children[0].id
-      }
-      if (!childId) return
-      getEnergyOverview(childId).then((res) => {
-        if (res && res.data) {
-          const dims = res.data.dimensions
-          if (dims && dims.length > 0) {
-            for (let i = 0; i < dims.length; i++) {
-              if (dims[i].code === 'wealth') {
-                this.wealthEnergy = dims[i].score || 0
-                break
-              }
-            }
-          }
-        }
-      }).catch(() => {})
-    },
     onSwitchMode() {
       if (this.role === 'child') {
         uni.showModal({
@@ -178,20 +142,6 @@ export default {
 </script>
 
 <style scoped>
-.wealth-energy-header {
-  display: flex;
-  align-items: center;
-  background: #fff;
-  margin: 20rpx 30rpx;
-  border-radius: 16rpx;
-  padding: 20rpx 30rpx;
-  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
-}
-.energy-header-icon { font-size: 36rpx; margin-right: 8rpx; }
-.energy-header-value { font-size: 40rpx; font-weight: bold; color: #1a73e8; margin-right: 8rpx; }
-.energy-header-label { font-size: 26rpx; color: #666; flex: 1; }
-.energy-header-trend { font-size: 24rpx; color: #52c41a; }
-
 .exit-switch {
   display: flex;
   align-items: center;
@@ -229,10 +179,6 @@ export default {
 .nickname { font-size: 34rpx; font-weight: bold; color: #333; margin-bottom: 8rpx; }
 .role { font-size: 24rpx; color: #999; margin-bottom: 6rpx; }
 .system-points { font-size: 24rpx; color: #F97316; margin-top: 4rpx; }
-.wealth-energy-row { display: flex; align-items: center; margin-top: 8rpx; }
-.wealth-energy-icon { font-size: 24rpx; margin-right: 4rpx; }
-.wealth-energy-value { font-size: 24rpx; color: #1a73e8; }
-.wealth-energy-trend { font-size: 22rpx; color: #52c41a; }
 
 .btn-switch {
   background: #667eea;

+ 1 - 26
cfc-frontend/pages/profile/components/ProfileMenu.vue

@@ -18,23 +18,6 @@
         </view>
       </view>
 
-      <!-- ===== 财富 ===== -->
-      <view class="menu-group">
-        <view class="menu-group-title">── 财富 ──</view>
-        <view class="menu-item" @click="goToPromotion">
-          <text>📢 推广中心</text>
-          <text class="arrow">›</text>
-        </view>
-        <view class="menu-item" @click="goToPointsLogs">
-          <text>📊 积分记录</text>
-          <text class="arrow">›</text>
-        </view>
-        <view class="menu-item" @click="goToCoupons">
-          <text>🎫 我的优惠券</text>
-          <text class="arrow">›</text>
-        </view>
-      </view>
-
       <!-- ===== 服务 ===== -->
       <view class="menu-group">
         <view class="menu-group-title">── 服务 ──</view>
@@ -153,9 +136,6 @@ export default {
     goToDailyTasks() {
       uni.navigateTo({ url: '/pages/tasks/daily-tasks' })
     },
-    goToPointsLogs() {
-      uni.navigateTo({ url: '/pages/points/points' })
-    },
     async onShowToFamilyChange(e) {
       const val = e.detail.value ? 1 : 0
       const uid = this.$store.state.userId || uni.getStorageSync('userId')
@@ -182,12 +162,7 @@ export default {
     goToAfterSales() {
       uni.navigateTo({ url: '/pages/shop/after-sales/after-sales' })
     },
-    goToPromotion() {
-      uni.navigateTo({ url: '/pages/promotion/index' })
-    },
-    goToCoupons() {
-      uni.navigateTo({ url: '/pages/profile/coupons' })
-    },
+    
     showInviteActionSheet() {
       const familyId = uni.getStorageSync('familyId')
       const userInfo = uni.getStorageSync('userInfo')

+ 22 - 122
cfc-frontend/pages/profile/components/ProfileStats.vue

@@ -5,62 +5,33 @@
       <view class="section-header">
         <text class="section-title">🎯 行动数据</text>
       </view>
-      <view class="wealth-card">
-        <view class="wealth-item">
-          <text class="wealth-value">{{ actionData.taskCount }}</text>
-          <text class="wealth-label">完成任务</text>
+      <view class="action-card">
+        <view class="action-item">
+          <text class="action-value">{{ actionData.taskCount }}</text>
+          <text class="action-label">完成任务</text>
         </view>
-        <view class="wealth-divider"></view>
-        <view class="wealth-item">
-          <text class="wealth-value">{{ actionData.purchaseCount }}</text>
-          <text class="wealth-label">购买商品</text>
+        <view class="action-divider"></view>
+        <view class="action-item">
+          <text class="action-value">{{ actionData.purchaseCount }}</text>
+          <text class="action-label">购买商品</text>
         </view>
-        <view class="wealth-divider"></view>
-        <view class="wealth-item">
-          <text class="wealth-value">{{ actionData.activityCount }}</text>
-          <text class="wealth-label">参加活动</text>
+        <view class="action-divider"></view>
+        <view class="action-item">
+          <text class="action-value">{{ actionData.activityCount }}</text>
+          <text class="action-label">参加活动</text>
         </view>
-        <view class="wealth-divider"></view>
-        <view class="wealth-item">
-          <text class="wealth-value">{{ actionData.courseCount }}</text>
-          <text class="wealth-label">学习课程</text>
+        <view class="action-divider"></view>
+        <view class="action-item">
+          <text class="action-value">{{ actionData.courseCount }}</text>
+          <text class="action-label">学习课程</text>
         </view>
       </view>
     </view>
-
-    <!-- 财富数据(推广收益) -->
-    <view class="section">
-      <view class="section-header">
-        <text class="section-title">💰 财富数据</text>
-        <text class="section-more" @click="goToPromotion">全部 ›</text>
-      </view>
-      <view class="wealth-card">
-        <view class="wealth-item">
-          <text class="wealth-value">{{ formatPriceWithSymbol(wealthData.totalEarnings) }}</text>
-          <text class="wealth-label">累计收益</text>
-        </view>
-        <view class="wealth-divider"></view>
-        <view class="wealth-item">
-          <text class="wealth-value">{{ formatPriceWithSymbol(wealthData.availableAmount) }}</text>
-          <text class="wealth-label">可提现</text>
-        </view>
-        <view class="wealth-divider"></view>
-        <view class="wealth-item">
-          <text class="wealth-value">{{ wealthData.referralCount }}</text>
-          <text class="wealth-label">邀请人数</text>
-        </view>
-      </view>
-      <view class="referral-code-bar" @click="copyReferralCode" v-if="wealthData.referralCode">
-        <text class="referral-code-label">邀请码</text>
-        <text class="referral-code-value">{{ wealthData.referralCode }}</text>
-        <text class="referral-code-copy">复制</text>
-      </view>
-    </view>
   </view>
 </template>
 
 <script>
-import { getUserActionStats, getReferralCode, getReferralSummary, getCommissionSummary } from '../../../utils/api.js'
+import { getUserActionStats } from '../../../utils/api.js'
 
 export default {
   name: 'ProfileStats',
@@ -71,25 +42,14 @@ export default {
         activityCount: 0,
         taskCount: 0,
         courseCount: 0
-      },
-      wealthData: {
-        totalEarnings: '0.00',
-        availableAmount: '0.00',
-        referralCount: 0,
-        referralCode: ''
       }
     }
   },
   onShow() {
     if (!uni.getStorageSync('token')) return
     this.loadActionData()
-    this.loadWealthData()
   },
   methods: {
-    formatPriceWithSymbol(val) {
-      if (val === null || val === undefined) return '0.00'
-      return String(val)
-    },
     async loadActionData() {
       try {
         const res = await getUserActionStats()
@@ -102,37 +62,6 @@ export default {
       } catch (e) {
         this.actionData.taskCount = parseInt(uni.getStorageSync('completedTasks') || 0)
       }
-    },
-    async loadWealthData() {
-      try {
-        const codeRes = await getReferralCode()
-        if (codeRes.data) {
-          this.wealthData.referralCode = codeRes.data.referralCode || codeRes.data.code || ''
-        }
-      } catch (e) {}
-      try {
-        const summaryRes = await getReferralSummary()
-        if (summaryRes.data) {
-          this.wealthData.totalEarnings = summaryRes.data.totalEarnings || '0.00'
-          this.wealthData.referralCount = summaryRes.data.referredCount || summaryRes.data.totalCount || 0
-        }
-      } catch (e) {}
-      try {
-        const comRes = await getCommissionSummary()
-        if (comRes.data) {
-          this.wealthData.availableAmount = comRes.data.availableAmount || '0.00'
-        }
-      } catch (e) {}
-    },
-    goToPromotion() {
-      uni.navigateTo({ url: '/pages/promotion/index' })
-    },
-    copyReferralCode() {
-      if (!this.wealthData.referralCode) return
-      uni.setClipboardData({
-        data: this.wealthData.referralCode,
-        success: () => { uni.showToast({ title: '邀请码已复制', icon: 'success' }) }
-      })
     }
   }
 }
@@ -158,7 +87,7 @@ export default {
   color: #999;
 }
 
-.wealth-card {
+.action-card {
   background: #fff;
   border-radius: 20rpx;
   padding: 30rpx 20rpx;
@@ -166,54 +95,25 @@ export default {
   align-items: center;
   box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
 }
-.wealth-item {
+.action-item {
   flex: 1;
   display: flex;
   flex-direction: column;
   align-items: center;
 }
-.wealth-value {
+.action-value {
   font-size: 40rpx;
   font-weight: bold;
   color: #F97316;
 }
-.wealth-label {
+.action-label {
   font-size: 24rpx;
   color: #999;
   margin-top: 8rpx;
 }
-.wealth-divider {
+.action-divider {
   width: 1rpx;
   height: 60rpx;
   background: #f0f0f0;
 }
-
-.referral-code-bar {
-  display: flex;
-  align-items: center;
-  background: #fff;
-  border-radius: 16rpx;
-  padding: 20rpx 24rpx;
-  margin-top: 16rpx;
-  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
-}
-.referral-code-label {
-  font-size: 26rpx;
-  color: #999;
-  margin-right: 16rpx;
-}
-.referral-code-value {
-  flex: 1;
-  font-size: 28rpx;
-  font-weight: bold;
-  color: #F97316;
-  letter-spacing: 4rpx;
-}
-.referral-code-copy {
-  font-size: 24rpx;
-  color: #5B9BD5;
-  padding: 4rpx 16rpx;
-  border: 1rpx solid #5B9BD5;
-  border-radius: 8rpx;
-}
 </style>

+ 297 - 307
cfc-frontend/pages/wealth/index.vue

@@ -1,5 +1,8 @@
 <template>
   <view class="container">
+    <!-- TabTransition overlay -->
+    <tab-transition v-if="showTabTransition" dimCode="wealth" />
+
     <!-- 未登录 -->
     <view v-if="!isLoggedIn" class="login-prompt">
       <view class="prompt-icon">👤</view>
@@ -19,120 +22,19 @@
         <text>🔄 退出切换</text>
       </view>
 
-      <!-- 五行相克状态条(5组) -->
-      <view class="section">
-        <view class="section-header">
-          <text class="section-title">⚖️ 身克富平衡</text>
-        </view>
-        <view class="body-wealth-bar" :class="'status-' + bodyWealthStatus">
-          <view class="bar-row">
-            <text class="bar-label">身体底盘</text>
-            <view class="bar-track">
-              <view class="bar-fill body-bar" :style="'width:' + bodyScore + '%'"></view>
-            </view>
-            <text class="bar-value">{{ bodyScore }}分</text>
-          </view>
-          <view class="bar-row">
-            <text class="bar-label">财力素养</text>
-            <view class="bar-track">
-              <view class="bar-fill wealth-bar" :style="'width:' + wealthScore + '%'"></view>
-            </view>
-            <text class="bar-value">{{ wealthScore }}分</text>
-          </view>
-          <view v-if="bodyWealthMessage" class="bw-message">{{ bodyWealthMessage }}</view>
+      <!-- 用户卡片 -->
+      <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="section">
-        <view class="section-header">
-          <text class="section-title">⚡ 行克身调节</text>
-        </view>
-        <view class="body-wealth-bar" :class="'status-' + actionBodyStatus">
-          <view class="bar-row">
-            <text class="bar-label">行动能量</text>
-            <view class="bar-track">
-              <view class="bar-fill action-bar" :style="'width:' + actionScore + '%'"></view>
-            </view>
-            <text class="bar-value">{{ actionScore }}分</text>
-          </view>
-          <view class="bar-row">
-            <text class="bar-label">身体底盘</text>
-            <view class="bar-track">
-              <view class="bar-fill body-bar" :style="'width:' + bodyScore + '%'"></view>
-            </view>
-            <text class="bar-value">{{ bodyScore }}分</text>
-          </view>
-          <view v-if="actionBodyMessage" class="bw-message">{{ actionBodyMessage }}</view>
-        </view>
-      </view>
-
-      <view class="section">
-        <view class="section-header">
-          <text class="section-title">💧 富克心平衡</text>
-        </view>
-        <view class="body-wealth-bar" :class="'status-' + wealthHeartStatus">
-          <view class="bar-row">
-            <text class="bar-label">财富能量</text>
-            <view class="bar-track">
-              <view class="bar-fill wealth-bar" :style="'width:' + wealthScore + '%'"></view>
-            </view>
-            <text class="bar-value">{{ wealthScore }}分</text>
-          </view>
-          <view class="bar-row">
-            <text class="bar-label">心动能量</text>
-            <view class="bar-track">
-              <view class="bar-fill mind-bar" :style="'width:' + mindScore + '%'"></view>
-            </view>
-            <text class="bar-value">{{ mindScore }}分</text>
-          </view>
-          <view v-if="wealthHeartMessage" class="bw-message">{{ wealthHeartMessage }}</view>
-        </view>
-      </view>
-
-      <view class="section">
-        <view class="section-header">
-          <text class="section-title">🔥 心克智沉淀</text>
-        </view>
-        <view class="body-wealth-bar" :class="'status-' + mindWisdomStatus">
-          <view class="bar-row">
-            <text class="bar-label">心动能量</text>
-            <view class="bar-track">
-              <view class="bar-fill mind-bar" :style="'width:' + mindScore + '%'"></view>
-            </view>
-            <text class="bar-value">{{ mindScore }}分</text>
-          </view>
-          <view class="bar-row">
-            <text class="bar-label">智慧能量</text>
-            <view class="bar-track">
-              <view class="bar-fill wisdom-bar" :style="'width:' + wisdomScore + '%'"></view>
-            </view>
-            <text class="bar-value">{{ wisdomScore }}分</text>
-          </view>
-          <view v-if="mindWisdomMessage" class="bw-message">{{ mindWisdomMessage }}</view>
-        </view>
-      </view>
-
-      <view class="section">
-        <view class="section-header">
-          <text class="section-title">⚔️ 智克行滤镜</text>
-        </view>
-        <view class="body-wealth-bar" :class="'status-' + wisdomActionStatus">
-          <view class="bar-row">
-            <text class="bar-label">智慧能量</text>
-            <view class="bar-track">
-              <view class="bar-fill wisdom-bar" :style="'width:' + wisdomScore + '%'"></view>
-            </view>
-            <text class="bar-value">{{ wisdomScore }}分</text>
-          </view>
-          <view class="bar-row">
-            <text class="bar-label">行动能量</text>
-            <view class="bar-track">
-              <view class="bar-fill action-bar" :style="'width:' + actionScore + '%'"></view>
-            </view>
-            <text class="bar-value">{{ actionScore }}分</text>
-          </view>
-          <view v-if="wisdomActionMessage" class="bw-message">{{ wisdomActionMessage }}</view>
-        </view>
+      <!-- 切换按钮(家长模式 + 未切换状态) -->
+      <view class="switch-section" v-if="role === 'parent' && !isSwitchedChild">
+        <button class="btn-switch" @click="onSwitchMode">切换到孩子视角</button>
       </view>
 
       <!-- 财富子维度卡片 -->
@@ -144,7 +46,7 @@
         <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-label">金钱收入</text>
             <text class="sub-dim-desc">累计佣金</text>
           </view>
           <view class="sub-dim-card" @click="goToAchievements">
@@ -202,6 +104,35 @@
         </view>
       </view>
 
+      <!-- 财富数据(仅家长) -->
+      <view class="section" v-if="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>
+          <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="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">
         <view class="section-header">
@@ -220,14 +151,6 @@
             <text class="service-icon">🎨</text>
             <text class="service-label">创客中心</text>
           </view>
-          <view class="service-item" @click="goToBadges">
-            <text class="service-icon">🏅</text>
-            <text class="service-label">成就系统</text>
-          </view>
-          <view class="service-item" @click="goToGrowthReport">
-            <text class="service-icon">📈</text>
-            <text class="service-label">成长报告</text>
-          </view>
         </view>
       </view>
 
@@ -238,10 +161,10 @@
         :familyId="activeChildId"
         title="为你推荐" />
 
-      <!-- 底部菜单(精简) -->
+      <!-- 底部菜单 -->
       <view class="menu-list">
-        <view class="menu-item" v-if="role === 'parent'" @click="goToProfile">
-          <text>⚙️ 个人设置</text>
+        <view class="menu-item" @click="goToProfile">
+          <text>👤 个人中心</text>
           <text class="arrow">›</text>
         </view>
         <view class="menu-item" @click="goToPointsLogs">
@@ -252,42 +175,42 @@
           <text>📣 推广中心</text>
           <text class="arrow">›</text>
         </view>
+        <view class="menu-item logout-item" @click="logout">
+          <text>🚪 退出登录</text>
+          <text class="arrow">›</text>
+        </view>
       </view>
     </template>
   </view>
 </template>
 
 <script>
+import TabTransition from '../../components/tab-transition.vue'
 import PageBanner from '../../components/PageBanner.vue'
-import { getWealthDetail, getChildren, switchBackVerify } from '../../utils/api.js'
+import DimensionProductList from '../../components/DimensionProductList.vue'
+import {
+  verifyPassword,
+  getChildren,
+  getWealthDetail,
+  getReferralCode,
+  getReferralSummary,
+  getCommissionSummary
+} from '../../utils/api.js'
 
 export default {
-  components: { PageBanner },
+  components: { TabTransition, PageBanner, DimensionProductList },
   data() {
     return {
+      showTabTransition: true,
       role: 'parent',
       isSwitchedChild: false,
+      nickname: '',
       // 五维评分
       bodyScore: 0,
       mindScore: 0,
       wisdomScore: 0,
       actionScore: 0,
       wealthScore: 0,
-      // 身克富
-      bodyWealthStatus: 'normal',
-      bodyWealthMessage: '',
-      // 行克身
-      actionBodyStatus: 'normal',
-      actionBodyMessage: '',
-      // 富克心
-      wealthHeartStatus: 'normal',
-      wealthHeartMessage: '',
-      // 心克智
-      mindWisdomStatus: 'normal',
-      mindWisdomMessage: '',
-      // 智克行
-      wisdomActionStatus: 'normal',
-      wisdomActionMessage: '',
       // 心的子维度(仅孩子有真实数据)
       heartEmotionStable: null,
       heartUnderstanding: null,
@@ -300,16 +223,35 @@ export default {
       wealthEducation: null,
       wealthSocial: null,
       wealthPoints: null,
+      // 财富数据
+      wealthData: {
+        totalEarnings: 0,
+        availableAmount: 0,
+        referralCount: 0,
+        referralCode: ''
+      },
       children: []
     }
   },
   computed: {
     isLoggedIn() {
       return !!uni.getStorageSync('token')
+    },
+    activeChildId() {
+      if (this.role === 'child') {
+        return uni.getStorageSync('currentChildId')
+      }
+      return ''
     }
   },
   onShow() {
-    if (!uni.getStorageSync('token')) return
+    this.showTabTransition = true
+
+    if (!uni.getStorageSync('token')) {
+      this._watchPageReady()
+      return
+    }
+
     var currentRole = uni.getStorageSync('currentRole') || uni.getStorageSync('role') || 'parent'
     if (currentRole === 'teacher') {
       uni.redirectTo({ url: '/pages/teacher/teacher-profile' })
@@ -317,13 +259,28 @@ export default {
     }
     this.role = currentRole
     this.isSwitchedChild = uni.getStorageSync('isSwitchedChild') || false
+
+    var userInfo = uni.getStorageSync('userInfo')
+    this.nickname = (userInfo && userInfo.nickname) || ''
+
     this.loadChildren()
     this.loadWealthDetail()
+    this.loadWealthData()
+    this._watchPageReady()
   },
   methods: {
     goLogin() {
       uni.navigateTo({ url: '/pages/login/login' })
     },
+    _watchPageReady() {
+      var self = this
+      this.$nextTick(function() {
+        self.showTabTransition = false
+      })
+      setTimeout(function() {
+        self.showTabTransition = false
+      }, 2000)
+    },
     async loadChildren() {
       try {
         var res = await getChildren()
@@ -349,21 +306,7 @@ export default {
         var res = await getWealthDetail(memberId, memberType)
         if (res && res.data) {
           var d = res.data
-          this.bodyScore = d.bodyScore || 0
-          this.mindScore = d.mindScore || 0
-          this.wisdomScore = d.wisdomScore || 0
-          this.actionScore = d.actionScore || 0
           this.wealthScore = d.wealthScore || 0
-          this.bodyWealthStatus = d.bodyWealthStatus || 'normal'
-          this.bodyWealthMessage = d.bodyWealthMessage || ''
-          this.actionBodyStatus = d.actionBodyStatus || 'normal'
-          this.actionBodyMessage = d.actionBodyMessage || ''
-          this.wealthHeartStatus = d.wealthHeartStatus || 'normal'
-          this.wealthHeartMessage = d.wealthHeartMessage || ''
-          this.mindWisdomStatus = d.mindWisdomStatus || 'normal'
-          this.mindWisdomMessage = d.mindWisdomMessage || ''
-          this.wisdomActionStatus = d.wisdomActionStatus || 'normal'
-          this.wisdomActionMessage = d.wisdomActionMessage || ''
           if (memberType === 'parent') {
             this.wealthIncome = d.wealthIncome
             this.wealthAchievement = d.wealthAchievement
@@ -381,23 +324,100 @@ export default {
         console.error('获取富维度详情失败', e)
       }
     },
+    async loadWealthData() {
+      if (this.role !== 'parent') return
+      try {
+        var codeRes = await getReferralCode()
+        if (codeRes && codeRes.code === 200 && codeRes.data) {
+          this.wealthData.referralCode = codeRes.data.code || codeRes.data || ''
+        }
+      } catch (e) {
+        console.error('获取邀请码失败', e)
+      }
+      try {
+        var summaryRes = await getReferralSummary()
+        if (summaryRes && summaryRes.code === 200 && summaryRes.data) {
+          this.wealthData.referralCount = summaryRes.data.totalCount || summaryRes.data.count || 0
+        }
+      } catch (e) {
+        console.error('获取邀请统计失败', e)
+      }
+      try {
+        var commissionRes = await getCommissionSummary()
+        if (commissionRes && commissionRes.code === 200 && commissionRes.data) {
+          this.wealthData.totalEarnings = commissionRes.data.totalEarnings || 0
+          this.wealthData.availableAmount = commissionRes.data.availableAmount || 0
+        }
+      } catch (e) {
+        console.error('获取佣金统计失败', e)
+      }
+    },
+    copyReferralCode() {
+      if (!this.wealthData.referralCode) return
+      uni.setClipboardData({
+        data: this.wealthData.referralCode,
+        success: function() {
+          uni.showToast({ title: '邀请码已复制', icon: 'success' })
+        }
+      })
+    },
+    onSwitchMode() {
+      if (this.role === 'child') {
+        this.exitSwitch()
+        return
+      }
+      // parent → switch to child
+      if (this.children.length === 0) {
+        this.loadChildren()
+        if (this.children.length === 0) {
+          uni.showModal({
+            title: '提示',
+            content: '您还没有添加孩子,是否现在去添加?',
+            success: function(res) {
+              if (res.confirm) {
+                uni.navigateTo({ url: '/pages/profile/create-child' })
+              }
+            }
+          })
+          return
+        }
+      }
+      if (this.children.length === 1) {
+        this.$store.commit('switchToChild', this.children[0].id)
+        this.role = 'child'
+        this.isSwitchedChild = true
+        uni.showToast({ title: '已切换为孩子模式', icon: 'success' })
+        return
+      }
+      var self = this
+      uni.showActionSheet({
+        itemList: this.children.map(function(c) { return c.nickname }),
+        success: function(res) {
+          var selectedChild = self.children[res.tapIndex]
+          self.$store.commit('switchToChild', selectedChild.id)
+          self.role = 'child'
+          self.isSwitchedChild = true
+          uni.showToast({ title: '已切换为' + selectedChild.nickname + '模式', icon: 'success' })
+        }
+      })
+    },
     exitSwitch() {
       if (!this.isSwitchedChild) return
+      var self = this
       uni.showModal({
         title: '退出切换',
         content: '请输入家长密码以确认退出',
         editable: true,
-        success: async (res) => {
+        success: function(res) {
           if (res.confirm && res.content) {
-            try {
-              var verifyResult = await switchBackVerify(res.content)
-              if (verifyResult && verifyResult.code === 200 && verifyResult.data === true) {
+            verifyPassword(res.content).then(function(result) {
+              if (result.code === 200) {
                 uni.removeStorageSync('isSwitchedChild')
                 uni.removeStorageSync('currentChildId')
-                this.isSwitchedChild = false
-                this.role = 'parent'
-                if (this.$store && this.$store.commit) {
-                  this.$store.commit('switchBackToParent')
+                self.isSwitchedChild = false
+                self.role = 'parent'
+                if (self.$store && self.$store.commit) {
+                  self.$store.commit('switchBackToParent')
                 }
                 uni.showToast({ title: '已退出切换', icon: 'success' })
                 setTimeout(function() {
@@ -406,13 +426,28 @@ export default {
               } else {
                 uni.showToast({ title: '密码错误', icon: 'none' })
               }
-            } catch (e) {
+            }).catch(function() {
               uni.showToast({ title: '验证失败', icon: 'none' })
-            }
+            })
+          }
+        }
+      })
+    },
+    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' })
     },
@@ -422,21 +457,12 @@ export default {
     goToCreator() {
       uni.navigateTo({ url: '/pages/promotion/index' })
     },
-    goToBadges() {
-      uni.navigateTo({ url: '/pages/rewards/badge' })
-    },
-    goToGrowthReport() {
-      uni.navigateTo({ url: '/pages/growth/index' })
-    },
     goToPointsLogs() {
       uni.navigateTo({ url: '/pages/points/points' })
     },
     goToPromotion() {
       uni.navigateTo({ url: '/pages/promotion/index' })
     },
-    goToProfile() {
-      uni.navigateTo({ url: '/pages/profile/index' })
-    },
     goToIncome() {
       uni.navigateTo({ url: '/pages/promotion/commission' })
     },
@@ -485,164 +511,125 @@ export default {
   font-weight: bold;
   color: #333;
 }
+.section-more {
+  font-size: 24rpx;
+  color: #999;
+}
 
-/* 身克富状态条 */
-.body-wealth-bar {
+/* 用户卡片 */
+.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);
 }
-.body-wealth-bar.status-overdraw {
-  background: #FFFBEB;
-  border: 1rpx solid #F59E0B;
-}
-.body-wealth-bar.status-penalty {
-  background: #FEF2F2;
-  border: 1rpx solid #EF4444;
-}
-.bar-row {
+.avatar {
+  width: 100rpx;
+  height: 100rpx;
+  border-radius: 50%;
+  background: linear-gradient(135deg, #F59E0B, #FBBF24);
   display: flex;
   align-items: center;
-  margin-bottom: 16rpx;
-}
-.bar-row:last-of-type {
-  margin-bottom: 0;
-}
-.bar-label {
-  font-size: 26rpx;
-  color: #666;
-  width: 140rpx;
+  justify-content: center;
+  font-size: 48rpx;
+  margin-right: 24rpx;
+  flex-shrink: 0;
 }
-.bar-track {
+.user-info {
   flex: 1;
-  height: 16rpx;
-  background: #f0f0f0;
-  border-radius: 8rpx;
-  overflow: hidden;
-  margin: 0 16rpx;
-}
-.bar-fill {
-  height: 100%;
-  border-radius: 8rpx;
-  transition: width 0.3s;
-}
-.bar-fill.body-bar {
-  background: linear-gradient(90deg, #FF8C42, #F97316);
-}
-.bar-fill.wealth-bar {
-  background: linear-gradient(90deg, #F59E0B, #FBBF24);
-}
-.bar-fill.action-bar {
-  background: linear-gradient(90deg, #10B981, #34D399);
 }
-.bar-fill.mind-bar {
-  background: linear-gradient(90deg, #FF6B9D, #FF8FAE);
-}
-.bar-fill.wisdom-bar {
-  background: linear-gradient(90deg, #6366F1, #818CF8);
+.nickname {
+  font-size: 34rpx;
+  font-weight: bold;
+  color: #333;
+  margin-bottom: 8rpx;
 }
-.bar-value {
+.role {
   font-size: 24rpx;
   color: #999;
-  width: 80rpx;
-  text-align: right;
-}
-.bw-message {
-  margin-top: 16rpx;
-  font-size: 24rpx;
-  color: #666;
-  padding: 12rpx 16rpx;
-  background: rgba(0,0,0,0.03);
-  border-radius: 8rpx;
-}
-.status-overdraw .bw-message {
-  color: #92400E;
-  background: rgba(245,158,11,0.1);
-}
-.status-penalty .bw-message {
-  color: #991B1B;
-  background: rgba(239,68,68,0.1);
-}
-.body-wealth-bar.status-cautious {
-  background: #FFFBEB;
-  border: 1rpx solid #F59E0B;
-}
-.body-wealth-bar.status-balanced {
-  background: #ECFDF5;
-  border: 1rpx solid #10B981;
-}
-.body-wealth-bar.status-eruption {
-  background: #FEF2F2;
-  border: 1rpx solid #EF4444;
-}
-.body-wealth-bar.status-judgmental {
-  background: #FFFBEB;
-  border: 1rpx solid #F59E0B;
-}
-.body-wealth-bar.status-materialized {
-  background: #F0FDF4;
-  border: 1rpx solid #22C55E;
 }
-.body-wealth-bar.status-nourished {
-  background: #ECFDF5;
-  border: 1rpx solid #10B981;
-}
-.body-wealth-bar.status-overprotect {
-  background: #FFF1F2;
-  border: 1rpx solid #FB7185;
-}
-.body-wealth-bar.status-cold_wise {
-  background: #F5F3FF;
-  border: 1rpx solid #8B5CF6;
-}
-.body-wealth-bar.status-blind {
-  background: #F5F3FF;
-  border: 1rpx solid #7C3AED;
+.card-arrow {
+  color: #ccc;
+  font-size: 40rpx;
+  margin-left: 16rpx;
 }
-.body-wealth-bar.status-paralysis {
-  background: #F3F4F6;
-  border: 1rpx solid #9CA3AF;
+
+/* 切换区域 */
+.switch-section {
+  margin: 0 30rpx 20rpx;
 }
-.status-cautious .bw-message {
-  color: #92400E;
-  background: rgba(245,158,11,0.06);
+.btn-switch {
+  background: linear-gradient(135deg, #F59E0B, #FBBF24);
+  color: #fff;
+  font-size: 26rpx;
+  padding: 16rpx 0;
+  border-radius: 40rpx;
+  text-align: center;
+  line-height: 1.5;
 }
-.status-balanced .bw-message {
-  color: #065F46;
-  background: rgba(16,185,129,0.06);
+
+/* 财富数据卡片 */
+.wealth-card {
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 30rpx 20rpx;
+  display: flex;
+  align-items: center;
+  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
 }
-.status-eruption .bw-message {
-  color: #991B1B;
-  background: rgba(239,68,68,0.06);
+.wealth-item {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
 }
-.status-judgmental .bw-message {
-  color: #92400E;
-  background: rgba(245,158,11,0.06);
+.wealth-value {
+  font-size: 40rpx;
+  font-weight: bold;
+  color: #F97316;
 }
-.status-materialized .bw-message {
-  color: #166534;
-  background: rgba(34,197,94,0.06);
+.wealth-label {
+  font-size: 24rpx;
+  color: #999;
+  margin-top: 8rpx;
 }
-.status-nourished .bw-message {
-  color: #065F46;
-  background: rgba(16,185,129,0.06);
+.wealth-divider {
+  width: 1rpx;
+  height: 60rpx;
+  background: #f0f0f0;
 }
-.status-overprotect .bw-message {
-  color: #9F1239;
-  background: rgba(251,113,133,0.06);
+
+/* 邀请码复制条 */
+.referral-code-bar {
+  display: flex;
+  align-items: center;
+  background: #fff;
+  margin-top: 16rpx;
+  border-radius: 16rpx;
+  padding: 24rpx 30rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
 }
-.status-cold_wise .bw-message {
-  color: #5B21B6;
-  background: rgba(139,92,246,0.06);
+.referral-code-label {
+  font-size: 26rpx;
+  color: #666;
+  margin-right: 16rpx;
 }
-.status-blind .bw-message {
-  color: #5B21B6;
-  background: rgba(124,58,237,0.06);
+.referral-code-value {
+  flex: 1;
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #F59E0B;
+  letter-spacing: 2rpx;
 }
-.status-paralysis .bw-message {
-  color: #374151;
-  background: rgba(156,163,175,0.06);
+.referral-code-copy {
+  font-size: 24rpx;
+  color: #fff;
+  background: linear-gradient(135deg, #F59E0B, #FBBF24);
+  padding: 8rpx 24rpx;
+  border-radius: 20rpx;
 }
 
 /* 三子维度卡片 */
@@ -693,7 +680,7 @@ export default {
   box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
 }
 .service-item {
-  width: 20%;
+  width: 33.33%;
   display: flex;
   flex-direction: column;
   align-items: center;
@@ -728,6 +715,9 @@ export default {
   color: #ccc;
   font-size: 32rpx;
 }
+.logout-item {
+  color: #EF4444;
+}
 
 /* 未登录 */
 .login-prompt {

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-fb5b7ac7f7f869a09565dce75ffdb6a5a45d2bf2
+2cd3a23fe70701e94b9bd29b24a4816777c7bf8b

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

@@ -1,12 +1,12 @@
 {
   "name": "cfc-web",
-  "version": "1.0.378",
+  "version": "1.0.379",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "cfc-web",
-      "version": "1.0.378",
+      "version": "1.0.379",
       "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.379",
+  "version": "1.0.380",
   "private": true,
   "scripts": {
     "dev": "vue-cli-service serve",

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

@@ -4,6 +4,12 @@
 
 ---
 
+## v1.0.380 (2026-07-17)
+
+### 文档
+- 更新菌群报告设计 — 仅展示个性化数据+知识库弹框+异常颜色箭头
+
+
 ## v1.0.379 (2026-07-17)
 
 ### 文档

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

@@ -1,6 +1,6 @@
 # 更新日志
 
-> 当前版本: v1.0.379
+> 当前版本: v1.0.380
 
 ## 历史版本
 
@@ -8,6 +8,12 @@
 
 ---
 
+## v1.0.380 (2026-07-17)
+
+### 文档
+- 更新菌群报告设计 — 仅展示个性化数据+知识库弹框+异常颜色箭头
+
+
 ## v1.0.379 (2026-07-17)
 
 ### 文档

+ 336 - 146
docs/superpowers/specs/2026-07-17-flora-microbiome-report-page-design.md

@@ -1,10 +1,11 @@
 # 菌群报告页面重构设计规范
 
 > 基于参考 JSON 完整数据,重构「肠道菌群报告详情页」,覆盖查看+编辑混合场景。
+> 页面只展示个性化数据(个人测定值),标准说明通过弹出框从知识库获取。
 
-**版本:** v1.0  
+**版本:** v1.1(v1.0 修正:仅展示个性化数据 + 知识库弹出框 + 异常值颜色箭头)  
 **日期:** 2026-07-17  
-**状态:** Approved  
+**状态:** Draft  
 **作者:** Sisyphus
 
 ---
@@ -21,8 +22,10 @@
 ### 1.2 设计目标
 
 1. **完整展示**:将所有报告板块(评分/指标/菌群/疾病风险/食物)全部呈现
-2. **可编辑**:用户可在查看页面转向编辑模式,调整解析错误的数据
-3. **保存兼容**:复用现有 `HealthReportDraft` 两阶段入库机制
+2. **仅展示个性化值**:页面只显示该人的测定数值,标准说明不内嵌
+3. **知识库弹出框**:每个菌、营养素、指标的名称可点击 → 弹出详细说明(来自独立知识库表)
+4. **异常值颜色+箭头**:超出正常范围的数值用颜色区分,名称上标 ↑(过高)↓(过低)
+5. **可编辑**:用户可在查看页面转向编辑模式,调整解析错误的数据
 
 ### 1.3 不在本文档范围
 
@@ -38,8 +41,9 @@
 |--------|------|------|
 | 页面拆分 | 主页 + 3 个详情页 | 食物 224 条 + 菌群 200 条过长,必须分页;指标用手风琴折叠到主页 |
 | 模式 | 混合(默认查看 + 编辑切换) | 满足「全字段可编辑」同时保留查看优先体验 |
-| 编辑范围 | 仅修改现有条目(不增删) | YAGNI:当前需求是修正 AI 解析错误,不是新增数据 |
-| 保存路径 | `HealthReportDraft` + `/api/health/report/confirm` | 现有机制已支持修改后整体提交 |
+| 编辑范围 | 仅修改现有条目(不增删) | 当前需求是修正 AI 解析错误,不是新增数据 |
+| 保存路径 | 新增 `POST /api/health/report/edit` | 编辑已发布报告(不用 draftId) |
+| 说明展示 | 不内嵌,点击名称弹出知识库 | 页面干净,知识库可独立维护 |
 
 ---
 
@@ -63,9 +67,80 @@ page.json 中需新增 3 个路径:
 
 ---
 
-## 4. 主页:gut-flora-detail.vue
+## 4. 异常值颜色与箭头规则(通用)
 
-### 4.1 板块布局(由上至下)
+所有页面(主页手风琴、菌群卡片、营养指标列表)统一使用以下规则:
+
+| 状态 | 颜色 | 箭头 | 示例 |
+|------|------|------|------|
+| `正常` / `低风险` | 默认文本 #333 | 无 | 维生素A 84 |
+| `偏高` / `过多` / `高` | 红色 #C62828 | ↑ | **双歧杆菌属↑** 12.8% |
+| `偏低` / `缺乏` / `不足` / `低` | 琥珀色 #E65100 | ↓ | **维生素B1↓** 2 |
+| `异常` | 红色 #C62828 | ⚠ | **炎症水平⚠** 16 |
+| `注意` | 黄色 #F59E0B | ⚠ | **甲状腺疾病⚠** |
+
+实现方式(Vue 2 inline 表达式):
+```html
+<text :class="'name-' + item.status" v-if="item.status !== '正常'">{{ item.name }}<text class="arrow">{{ arrowMap[item.status] }}</text></text>
+<text v-else>{{ item.name }}</text>
+```
+
+不可用方法调用(`statusClass(item.status)`),必须内联 `:class="'status-' + item.status"`。
+
+---
+
+## 5. 知识库弹出框(通用组件)
+
+### 组件:`components/health-knowledge-popup.vue`
+
+每个菌属名/营养素名/指标名都是可点击的,点击后打开一个弹出框显示该项目的知识库说明。
+
+```html
+<!-- 使用方式 -->
+<text class="kb-link" @tap="showKnowledge(item.type, item.name)">{{ item.name }}</text>
+
+<!-- 弹出框组件 -->
+<health-knowledge-popup
+  :visible="knowledgeVisible"
+  :item-type="knowledgeType"    <!-- 'bacteria' | 'nutrient' | 'indicator' | 'vitamin' | 'amino_acid' -->
+  :item-name="knowledgeName"    <!-- 如 "双歧杆菌属 Bifidobacterium" -->
+  @close="closeKnowledge" />
+```
+
+### 弹出框内容结构
+
+```
+┌────────────────────────────────┐
+│ 双歧杆菌属 Bifidobacterium  [×]│
+│ ────────────────────────────── │
+│ 【分类】有益菌                 │
+│ 【正常范围】0.19-14.59%        │
+│ 【功能说明】                    │
+│ 最重要的益生菌,参与肠道免疫屏障│
+│ 维护,抑制有害菌生长,促进营养  │
+│ 物质吸收...(完整说明文字)     │
+│                               │
+│ 【相关建议】                    │
+│ 补充来源:酸奶、开菲尔等发酵食品│
+└────────────────────────────────┘
+```
+
+### 数据来源
+
+知识库数据存储在独立数据库表 `health_knowledge_base` 中。
+
+后端接口:
+```
+POST /api/health/knowledge/query
+body: { itemType: "bacteria", itemName: "双歧杆菌属 Bifidobacterium" }
+返回: { code: 200, data: { itemType, itemName, category, normalRange, description, suggestion } }
+```
+
+---
+
+## 6. 主页:gut-flora-detail.vue
+
+### 6.1 板块布局(由上至下)
 
 1. **报告头部**(hero 渐变背景)
    - 姓名、年龄、性别、报告编号、报告日期
@@ -78,7 +153,7 @@ page.json 中需新增 3 个路径:
    - 🍽️ 饮食推荐(X 项)
    - 📋 全部指标(X 项)
 4. **肠道功能面板**(手风琴 Accordion)
-   - 肠道屏障与代谢物
+   - 肠道屏障与代谢物 → 指标列表,名称可点击弹知识库,异常值带颜色+箭头
    - 短链脂肪酸
    - 神经递质与激素
    - 抗生素风险评估
@@ -88,31 +163,38 @@ page.json 中需新增 3 个路径:
    - 维生素(9 项)
    - 微量元素
 
-### 4.2 两种模式
+### 6.2 两种模式
 
 | 模式 | 显示 | 操作 |
 |------|------|------|
-| 查看模式 | 只展示数值 + 状态徽章 | 点击「编辑」按钮切到编辑 |
+| 查看模式 | 只展示数值 + 状态颜色+箭头 | 点击名称弹知识库 |
 | 编辑模式 | 数值改为 input / 状态改为 picker | 点击「取消」「保存」 |
 
-### 4.3 顶部 FAB
+查看模式下,名称仍然可点击弹知识库。编辑模式下知识库弹框仍然可用。
+
+### 6.3 顶部 FAB
 
 - 查看模式:右下显示「✏️ 编辑」浮动按钮
 - 编辑模式:底部显示「取消」「保存(草稿)」双按钮固定栏
 
-### 4.4 营养/代谢板块的数据来源
+### 6.4 手风琴中指标卡结构
 
-后端 `getReportDetail` 已返回 `indicators` 字段,其中每条含:
-- `category`: 类别("营养"/"氨基酸"/"维生素"/"微量元素"/"抗生素"/"肠屏障"/"脂肪酸"/"神经递质")
-- `indicatorName`, `indicatorValue`, `status`, `refRange`
+```
+┌───────────────────────────────────┐
+│ 维生素B1 ↓      数值: 2           │
+│ 正常范围: 4-20                    │
+└───────────────────────────────────┘
+```
 
-前端按 category 分组渲染手风琴。
+- 名称 `维生素B1` → 可点击,弹出知识库
+- `↓` 箭头 + 红色字体(偏低)
+- 正常范围的用默认色,无箭头
 
 ---
 
-## 5. 菌群详情:gut-flora-species-detail.vue
+## 7. 菌群详情:gut-flora-species-detail.vue
 
-### 5.1 顶部导航(12 分类 Tab)
+### 7.1 顶部导航(12 分类 Tab)
 
 | Tab | 数据源 |
 |-----|--------|
@@ -129,264 +211,372 @@ page.json 中需新增 3 个路径:
 | 失眠相关 | category="insomnia" |
 | 全部 | 不过滤 |
 
-### 5.2 卡片结构
+### 7.2 卡片结构
 
-每个菌属一条,竖向列表:
+**无内嵌说明**,标准说明通过点击名称弹出知识库获取。
 
 ```
-┌───────────────────────────────────┐
-│ 双歧杆菌属 Bifidobacterium  [偏低]│
-│ 丰度 0.2973% | 正常范围 0.19-12.59│
-│ 人群水平 53% | 检出率 97.12%      │
-│ ─────────────────────────────────│
-│ 说明:最重要的有益菌,参与肠道免疫│
-└───────────────────────────────────┘
+┌─────────────────────────────────────────┐
+│ 双歧杆菌属 Bifidobacterium ↑            │  ← 名称可点(弹出知识库), ↑红色(偏高)
+│ 丰度 0.2973%  |  正常范围 0.19-12.59    │
+│ 人群水平 53% | 检出率 97.12%            │
+└─────────────────────────────────────────┘
 ```
 
-字段:bacteriaName, bacteriaValue, normalRange, populationLevel, detectionRate, description, status
+- 菌名:可点击 → 弹出知识库(`itemType="bacteria"`)
+- 丰度值:显示数值 + 正常范围
+- 状态:通过菌名颜色+箭头体现,不在卡片上单独显示状态文字
+- 人群水平/检出率:灰色小字辅助信息
 
-### 5.3 编辑模式
+### 7.3 编辑模式
 
-可编辑字段:`丰度值(bacteriaValue)`、`状态(status)`、`说明(description)`  
-不可编辑:菌属名称(bacteriaName)、正常范围(normalRange)、人群水平、检出率(系统判定)
+可编辑字段:`丰度值(bacteriaValue)`、`状态(status)`  
+不可编辑:菌属名称、正常范围、人群水平、检出率
 
 ---
 
-## 6. 食物推荐:gut-flora-foods-detail.vue
+## 8. 食物推荐:gut-flora-foods-detail.vue
 
-### 6.1 顶部筛选
+### 8.1 顶部筛选
 
 - 分类筛选条(主食 / 蔬菜 / 水果 / 肉类 / 其它),滑动横向
 - 排序:推荐指数降序(-X 高优 → +X 高优);用户可切换「仅看推荐」(score ≥ 5)
 
-### 6.2 卡片结构
+### 8.2 卡片结构
 
 ```
 ┌────────────────────────────────────┐
-│ 大麦   推荐:+10   [主食]    
+│ 大麦   推荐指数: +10   [主食]       │
 │ 蛋白12 | 脂肪2 | 碳水73 | 纤维17   │
 │ 能量 1481KJ                        │
 └────────────────────────────────────┘
 ```
 
-字段:foodName, category, score, energyKj, protein, fat, carbs, starch, fiber, cholesterol
+食物无知识库需求(数据自包含),但名称可点击 → 视需求可未来接入。
 
-### 6.3 编辑模式
+### 8.3 编辑模式
 
 可编辑字段:`推荐指数(score)`  
-不可编辑:营养数据(来自食材固定库)
 
 ---
 
-## 7. 疾病风险:gut-flora-risks-detail.vue
+## 9. 疾病风险:gut-flora-risks-detail.vue
 
-### 7.1 顶部状态卡片
+### 9.1 布局
 
-数量徽章 + 重要风险(注意/异常)置顶
+重要风险(注意/异常)置顶,其余按风险等级排序。
 
-### 7.2 列表(按风险等级排序)
+### 9.2 卡片结构
 
 ```
 ┌────────────────────────────────────┐
-│ ⚠️ 甲状腺疾病    [注意]            │
-│ 风险值 0.33 (中等)           
+│ 甲状腺疾病 ⚠                       │  ← 名称可点弹出知识库
+│ 风险值: 0.33  风险等级: [注意]
 └────────────────────────────────────┘
 ```
 
-字段:diseaseName, riskValue, riskLevel  
+风险等级颜色:低风险→绿、注意→黄/⚠、异常→红
 
-风险等级颜色:低风险→绿、注意→黄、异常→红
+疾病名称可点击 → 弹出知识库(`itemType="disease"`)
 
-### 7.3 编辑模式
+### 9.3 编辑模式
 
-可编辑字段:`风险值(riskValue)`、`风险等级(riskLevel picker)`  
-不可编辑:疾病名称(diseaseName)
+可编辑字段:`风险值(riskValue)`、`风险等级(riskLevel picker)`
 
 ---
 
-## 8. 公共交互规范
+## 10. 知识库架构
+
+### 10.1 数据库表
+
+```sql
+CREATE TABLE IF NOT EXISTS health_knowledge_base (
+  id BIGINT AUTO_INCREMENT PRIMARY KEY,
+  item_type VARCHAR(50) NOT NULL COMMENT '类型: bacteria/nutrient/indicator/vitamin/amino_acid/disease',
+  item_name VARCHAR(200) NOT NULL COMMENT '项目名称(精确匹配)',
+  category VARCHAR(100) COMMENT '分类(如 有益菌/有害菌)',
+  normal_range VARCHAR(200) COMMENT '正常范围参考',
+  description TEXT COMMENT '详细说明',
+  suggestion TEXT COMMENT '相关建议(如补充来源)',
+  source VARCHAR(100) COMMENT '数据来源',
+  created_at DATETIME,
+  updated_at DATETIME,
+  UNIQUE KEY uk_type_name (item_type, item_name),
+  INDEX idx_type (item_type)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康知识库 - 菌属/营养素/指标说明';
+```
+
+### 10.2 初次数据填充
 
-### 8.1 编辑状态机
+参考 JSON 文件中所有菌属的 `说明` 字段、指标的 `健康状况`/参考范围等,一次性导入 `health_knowledge_base`。
+
+### 10.3 API
 
 ```
-查看模式 ─[点击编辑]→ 编辑模式 ─[点击取消]→ 查看模式 (丢弃改动)
-                              ─[点击保存]→ 上传草稿 → 显示保存中 → 返回查看模式
+POST /api/health/knowledge/query
+body: { itemType: "bacteria", itemName: "双歧杆菌属 Bifidobacterium" }
+→ { code: 200, data: { id, itemType, itemName, category, normalRange, description, suggestion } }
+
+POST /api/health/knowledge/batch-query
+body: { queries: [{ itemType, itemName }, ...] }
+→ { code: 200, data: { "bacteria:双歧杆菌属 Bifidobacterium": {...}, ... } }
 ```
 
-### 8.2 编辑按钮触发位置
+批查询用于首次加载时预取当前页面所有项目的知识库。单查询用于点击时按需加载。
+
+---
 
-主页、固定在右下 FAB(查看模式)。  
-详情页(菌群/食物/风险),固定在顶部导航栏右侧文字按钮「编辑」。
+## 11. 公共交互规范
 
-### 8.3 数据流
+### 11.1 编辑状态机
 
 ```
-进入页面 → onLoad(options) {
-  reportId = options.reportId
-  loadReportDetail()       // GET /api/health/report/detail (返回 report/indicators/gutFlora/diseaseRisks)
-  // 前端缓存于 this.viewData
-}
+查看模式 ─[点击编辑]→ 编辑模式 ─[点击取消]→ 查看模式 (丢弃改动)
+                              ─[点击保存]→ POST /api/health/report/edit → toast → 重新加载 → 查看模式
+```
+
+### 11.2 编辑按钮触发位置
+
+- 主页:右下 FAB(查看模式)
+- 详情页:顶部导航栏右侧文字按钮「编辑」
+
+### 11.3 数据流
 
-点击编辑 → 深拷贝 this.viewData → this.editData
-          进入编辑模式,input/picker 可用
-点击取消 → 丢弃 this.editData,回到查看模式
-点击保存 → 
-  payload = {
-    report: 当前报告 metadata,
-    indicators: 当前 indicators ,
-    gutFlora: 编辑后的 gutFlora,
-    diseaseRisks: 编辑后的 diseaseRisks,
-    foods: 编辑后的 foods (仅本页实际被改)
-  }
-  POST /api/health/report/edit
-    params: { reportId, payload }
-  → 成功 → toast → 调 loadReportDetail 重新拉 → 回到查看模式
+```
+进入页面 → onLoad({ reportId }) → loadReportDetail()
+  GET /api/health/report/detail → 缓存于 this.reportData
+
+点击编辑 → 深拷贝 this.reportData → this.editData → 进入编辑模式
+点击取消 → 丢弃 this.editData → 回查看模式
+点击保存 →
+  POST /api/health/report/edit { reportId, payload: { indicators, gutFlora, diseaseRisks, foods }, subjectId }
+  → 200 → toast "已保存" → loadReportDetail() 重新拉 → 查看模式
 ```
 
-### 8.4 新增 API
+### 11.4 知识库弹出框数据流
 
-| Endpoint | 用途 | 备注 |
-|----------|------|------|
-| `POST /api/health/report/edit` | 编辑已发布报告 | 新增;接收 reportId + payload;先 selectById → 抹写 indicators/gutFlora/diseaseRisks → update;最后 `dimensionScoreService.refreshFromGutReport` 刷新 7 维 |
+```
+首次加载 → 遍历报告的 indicators/gutFlora,
+           收集所有不重复的 (itemType, itemName) 对
+          → POST /api/health/knowledge/batch-query → 缓存入 this.knowledgeMap
+          → 前端 <text @tap="showKnowledge(type, name)"> 直接从缓存取
 
-不修改现有 `/api/health/report/confirm` 接口以保持兼容;额外添加一个新接口 `/api/health/report/edit` 专门用于编辑已发布报告。
+点击名称 → if (cache[type+name]) → 直接显示
+          else → POST /api/health/knowledge/query → 显示并加入缓存
+```
 
-### 8.4 mini-program 限制
+### 11.5 mini-program 限制
 
-- 禁可选链 `?.`,全部用 `&&` 短路
+- 禁可选链 `?.`,全部用 `&&` 短路
 - 禁 CSS Grid,全部 flex
 - Vue 2 Options API
-- 状态类徽章用 `:class="'status-' + item.status"` 内联(不可方法调用)
+- 状态类徽章用 `:class="'status-' + item.status"` 内联
 
 ---
 
-## 9. 数据契约
+## 12. 数据契约
 
-### 9.1 GET /api/health/report/detail — 后端已有
+### 12.1 GET /api/health/report/detail — 后端已有(不变)
 
-返回:
 ```json
 {
   "code": 200,
   "data": {
-    "report": { "id":..., "personName":..., "overallScore":..., "gutHealthScore":..., ... },
-    "indicators": [{ "category":"营养", "indicatorName":"碳水", "indicatorValue":"96", "status":"正常", "refRange":"..." }, ...],
-    "gutFlora": [{ "bacteriaName":"...", "bacteriaValue":"0.2973", "category":"core", "status":"偏低", ... }, ...],
-    "diseaseRisks": [{ "diseaseName":"...", "riskValue":"0.33", "riskLevel":"注意" }, ...]
+    "report": { "id":..., "personName":"某人", "overallScore":57, "gutHealthScore":76, ... },
+    "indicators": [
+      { "category":"营养", "indicatorName":"碳水化合物", "indicatorValue":"96", "status":"正常", "refRange":"..." },
+      { "category":"氨基酸", "indicatorName":"胱氨酸", "indicatorValue":"86", "status":"正常", "refRange":"..." },
+      { "category":"维生素", "indicatorName":"维生素B1", "indicatorValue":"2", "status":"缺乏", "refRange":"..." },
+      ...
+    ],
+    "gutFlora": [
+      { "bacteriaName":"双歧杆菌属 Bifidobacterium", "bacteriaValue":"0.2973", "category":"core", "status":"偏低", "normalRange":"0.19-12.59", "populationLevel":"53%", "detectionRate":"97.12%" },
+      ...
+    ],
+    "diseaseRisks": [
+      { "diseaseName":"甲状腺疾病", "riskValue":"0.33", "riskLevel":"注意" }
+    ]
   }
 }
 ```
 
-**当前返回结构已能覆盖所有页面**,不再需要新增后端接口。
-
-### 9.2 POST /api/health/report/confirm — 修改报告(编辑保存)
+### 12.2 POST /api/health/report/edit — 新增
 
-请求体:
 ```json
 {
-  "draftId": 123,
+  "reportId": 123,
   "payload": {
-    "summary": { "overallScore": 76, "gutHealthScore": 53, ... "personName":"某人", ... },
-    "indicators": [...],
-    "gutFlora": [...],   // 编辑修改后的菌群
-    "probioticSpecies": [...],
-    "foods": [...],
-    "diseaseRisks": [...] // 编辑修改后的疾病风险
+    "indicators": [...],      // 编辑后的完整 list
+    "gutFlora": [...],         // 编辑后的完整 list
+    "probioticSpecies": [...], // 编辑后的菌门 list
+    "foods": [...],            // 编辑后的食物 list
+    "diseaseRisks": [...]      // 编辑后的疾病风险 list
   },
   "subjectId": 456
 }
 ```
 
-**后端机制已支持**:`/api/health/report/confirm` 接收 payload 后整体覆盖入库(参考 `HealthReportController.confirmDraft`)。
+### 12.3 POST /api/health/knowledge/query — 新增
+
+```json
+// 请求
+{ "itemType": "bacteria", "itemName": "双歧杆菌属 Bifidobacterium" }
+// 响应
+{ "code":200, "data": {
+    "id":1, "itemType":"bacteria", "itemName":"双歧杆菌属 Bifidobacterium",
+    "category":"有益菌",
+    "normalRange":"0.19-14.59",
+    "description":"最重要的益生菌,参与肠道免疫屏障维护...",
+    "suggestion":"补充来源:酸奶、开菲尔等发酵食品"
+}}
+```
 
-但 confirm 要 draftId 已发布报告没有 draft。因此新增一个独立接口 `/api/health/report/edit` 处理已发布报告编辑。
+### 12.4 POST /api/health/knowledge/batch-query — 新增
 
-### 9.3 后端需补充
+```json
+// 请求
+{ "queries": [
+    {"itemType":"bacteria","itemName":"双歧杆菌属 Bifidobacterium"},
+    {"itemType":"vitamin","itemName":"维生素B1"}
+]}
+// 响应
+{ "code":200, "data": {
+    "bacteria:双歧杆菌属 Bifidobacterium": { ... },
+    "vitamin:维生素B1": { ... }
+}}
+```
 
-| 改动 | 位置 | 说明 |
-|------|------|------|
-| 新增 `POST /api/health/report/edit` | `HealthReportController.java` | 接收 `{reportId, payload, subjectId}`;先 selectById → 抹写 indicators/gutFlora/diseaseRisks/foods;刷新 7 维评分;返回最新 reportDetail |
-| 新增 `updateReportFromPayload(reportId, payload, subjectId)` | `HealthReportService.java` | 同上数据流;优先复用 `confirmDraft` 的构造逻辑 |
-| 新增 `editHealthReport(...)` | `HealthReportController.java` | 实现 `dimensionScoreService.refreshFromGutReport` 触发 |
+---
+
+## 13. 后端需补充
+
+| 改动 | 位置 | 方法 | 说明 |
+|------|------|------|------|
+| 新建 Entity | `entity/HealthKnowledgeBase.java` | — | @TableName("health_knowledge_base") |
+| 新建 Mapper | `mapper/HealthKnowledgeBaseMapper.java` | — | MyBatis-Plus BaseMapper |
+| 新建 Service | `service/HealthKnowledgeBaseService.java` | `query(itemType, itemName)`, `batchQuery(List<Pair>)` | 查知识库 |
+| 新建 Controller | `controller/HealthKnowledgeBaseController.java` | `POST /api/health/knowledge/query`, `POST /api/health/knowledge/batch-query` | 知识库 API |
+| 编辑 API | `controller/HealthReportController.java` | `POST /api/health/report/edit` | 编辑已发布报告 |
+| 编辑 Service | `service/HealthReportService.java` | `updateReportFromPayload(reportId, payload, subjectId)` | 抹写并刷新 7 维 |
+| 数据库迁移 | `config/DatabaseInitializer.java` | 创建 `health_knowledge_base` 表 + 导入参考 JSON 的说明数据 | 迁移 N+1 |
+| schema.sql | `resources/schema.sql` | 追加新表 DDL | 同步 |
+
+### 13.1 KnowledgeBaseService 方法签名
+
+```java
+public HealthKnowledgeBase query(String itemType, String itemName) {
+    return mapper.selectOne(
+        new LambdaQueryWrapper<HealthKnowledgeBase>()
+            .eq(HealthKnowledgeBase::getItemType, itemType)
+            .eq(HealthKnowledgeBase::getItemName, itemName)
+    );
+}
 
-不需要修改 `confirmDraft`——保持兼容。
+public Map<String, HealthKnowledgeBase> batchQuery(List<QueryPair> queries) {
+    // 按 (type, name) 批量查询
+}
+```
 
 ---
 
-## 10. 验收标准
+## 14. 验收标准
 
-### 10.1 主页(gut-flora-detail.vue)
+### 14.1 主页(gut-flora-detail.vue)
 
 - [ ] 长 scroll 顺畅渲染 >= 1000rpx 高度
 - [ ] 报告头部显示姓名/年龄/性别/编号/日期
 - [ ] 健康评分圆环绘制(11 项可滚动横向)
 - [ ] 4 个快速跳转入口可见,且能跳到对应详情页
 - [ ] 肠道功能 / 营养指标 双手风琴,能折叠/展开
+- [ ] 每个指标名称可点击 → 弹出知识库弹出框
+- [ ] 异常值:偏高/过多 → 红色 + ↑;偏低/缺乏/不足 → 琥珀色 + ↓
+- [ ] 正常值:默认色无箭头
 - [ ] 右下 FAB「编辑」按钮可见
 - [ ] 点击「编辑」后 input/picker 可用,「保存」固定底部出现
 - [ ] 保存成功后正确返回查看模式 + toast 提示
 
-### 10.2 菌群详情(gut-flora-species-detail.vue)
+### 14.2 菌群详情(gut-flora-species-detail.vue)
 
 - [ ] 12 分类 Tab 正常切换
 - [ ] 每个 Tab 显示对应菌属列表
-- [ ] 卡片显示丰度/正常范围/人群水平/检出率/说明
-- [ ] 编辑模式可改丰度/状态/说明,点击保存后写入
+- [ ] 卡片显示菌名(可点弹知识库)、丰度、正常范围、人群水平、检出率
+- [ ] 无内嵌说明文字
+- [ ] 菌名带颜色+箭头指示偏高/偏低
+- [ ] 编辑模式可改丰度/状态,保存后写入
 
-### 10.3 食物推荐(gut-flora-foods-detail.vue)
+### 14.3 食物推荐(gut-flora-foods-detail.vue)
 
 - [ ] 分类筛选条切换生效
 - [ ] 列表按推荐指数降序
 - [ ] 卡片显示名称/分类/推荐指数/营养数据
 - [ ] 编辑模式可改推荐指数
 
-### 10.4 疾病风险(gut-flora-risks-detail.vue)
+### 14.4 疾病风险(gut-flora-risks-detail.vue)
 
 - [ ] 重要风险(注意/异常)置顶
-- [ ] 风险等级用颜色徽章:低风险=绿/注意=黄/异常=红
+- [ ] 风险等级用颜色徽章:低风险=绿/注意=黄+⚠/异常=红
+- [ ] 疾病名称可点击弹知识库
 - [ ] 编辑模式可改风险值/风险等级
 
-### 10.5 E2E 测试
+### 14.5 知识库
+
+- [ ] 后端 `batch-query` 支持一次预取全部知识库条目
+- [ ] 弹出框正确显示类型/说明/建议
+- [ ] 数据库迁移幂等可重复运行
+- [ ] 参考 JSON 说明数据正确导入
 
-- [ ] `tests/e2e/flora-microbiome-flow.spec.js` 中场景 4(菌种丰度明细展示)通过:检查 `.bacteria-card`/`.flora-item`/`.gut-flora-row` 类名都出现
-- [ ] 新增 `.preview-result`、`.save-btn` 选择器匹配现有页面元素
+### 14.6 E2E 测试
+
+- [ ] `tests/e2e/flora-microbiome-flow.spec.js` 现有场景通过
+- [ ] 新增 `.kb-link`、`.knowledge-popup` 选择器覆盖知识库交互
 
 ---
 
-## 11. 文件变更清单
+## 15. 文件变更清单
 
 | 文件 | 操作 | 内容 |
 |------|------|------|
-| `cfc-frontend/pages/health/gut-flora-detail.vue` | 重写 | 长 scroll + 手风琴 + 双模式 + FAB |
-| `cfc-frontend/pages/health/gut-flora-species-detail.vue` | 新建 | 12 Tab + 卡片 + 编辑 |
+| `cfc-frontend/pages/health/gut-flora-detail.vue` | 重写 | 长 scroll + 手风琴 + 知识库弹框 + 颜色箭头 + 双模式 |
+| `cfc-frontend/pages/health/gut-flora-species-detail.vue` | 新建 | 12 Tab + 卡片(无说明) + 颜色箭头 + 知识库弹框 |
 | `cfc-frontend/pages/health/gut-flora-foods-detail.vue` | 新建 | 分类筛选 + 卡片 + 编辑 |
-| `cfc-frontend/pages/health/gut-flora-risks-detail.vue` | 新建 | 重要置顶 + 卡片 + 编辑 |
+| `cfc-frontend/pages/health/gut-flora-risks-detail.vue` | 新建 | 重要置顶 + 卡片 + 知识库弹框 |
+| `cfc-frontend/components/health-knowledge-popup.vue` | 新建 | 通用知识库弹出框组件 |
 | `cfc-frontend/pages.json` | 改 | 注册 3 个新页面路径 |
-| `cfc-frontend/utils/api.js` | 改 | 新增 `editHealthReport` 函数(POST `/api/health/report/edit`) |
-| `cfc-backend/src/main/java/com/etotem/cfc/controller/HealthReportController.java` | 改 | 新增 `POST /api/health/report/edit` 端点 + `editHealthReport` 方法 |
-| `cfc-backend/src/main/java/com/etotem/cfc/service/HealthReportService.java` | 改 | 新增 `updateReportFromPayload(reportId, payload, subjectId)` |
-| `tests/e2e/flora-microbiome-flow.spec.js` | 检查 | 现有 selector 兼容 |
+| `cfc-frontend/utils/api.js` | 改 | 新增 `editHealthReport`、`queryKnowledge`、`batchQueryKnowledge` |
+| `cfc-backend/.../entity/HealthKnowledgeBase.java` | 新建 | 知识库实体 |
+| `cfc-backend/.../mapper/HealthKnowledgeBaseMapper.java` | 新建 | 知识库 Mapper |
+| `cfc-backend/.../service/HealthKnowledgeBaseService.java` | 新建 | 知识库 Service |
+| `cfc-backend/.../controller/HealthKnowledgeBaseController.java` | 新建 | 知识库 API |
+| `cfc-backend/.../controller/HealthReportController.java` | 改 | 新增 `POST /api/health/report/edit` |
+| `cfc-backend/.../service/HealthReportService.java` | 改 | 新增 `updateReportFromPayload` |
+| `cfc-backend/.../config/DatabaseInitializer.java` | 改 | 创建 health_knowledge_base 表 + 导入数据 |
+| `cfc-backend/.../resources/schema.sql` | 改 | 追加 health_knowledge_base DDL |
+| `tests/e2e/flora-microbiome-flow.spec.js` | 检查/更新 | 适配新选择器 |
 
 ---
 
-## 12. 风险与权衡
+## 16. 风险与权衡
 
 | 风险 | 缓解 |
 |------|------|
-| 编辑时大批量数据(200+ 菌属 + 224 食物)并发上传可能慢 | 前端只提交被改过的索引列表;后端使用批量 update 而非循环 update |
-| 当前 confirm 要 draftId 已发布报告没有 draft | 后端添加 reportId 编辑路径(见 9.3) |
-| 编辑后是否会破坏 7 维评分关联 | 编辑路径同样调用 `dimensionScoreService.refreshFromGutReport` 刷新评分 |
-| mini-program 真机性能(200+ 卡片渲染) | 使用 `v-if` 分组懒加载,避免 canvas 重复 `uni.createCanvasContext` |
+| 200+ 菌属首次加载时 batch-query 知识库可能慢 | 并行请求;知识库表小(<500 行),加索引后毫秒级 |
+| 知识库数据导入工作量 | 参考 JSON 已有完整的 `说明` 字段,可用脚本批量入库 |
+| 编辑时大批量数据上传慢 | 前端只提交被改过的索引列表 |
+| 编辑后是否破坏 7 维评分 | 编辑路径同样调用 `refreshFromGutReport` 刷新 |
+| mini-program 真机性能(200+ 卡片) | `v-if` 分组懒加载 |
 
 ---
 
-## 13. 实现顺序
-
-1. 后端 confirmDraft 加 reportId 编辑分支 → `updateReportFromPayload` → `mvn clean compile`
-2. 新建 `gut-flora-foods-detail.vue`(最简单,先验证编辑流)
-3. 新建 `gut-flora-risks-detail.vue`
-4. 新建 `gut-flora-species-detail.vue`
-5. 重写 `gut-flora-detail.vue`(最复杂)
-6. 更新 `pages.json` 注册新路径
-7. 跑 `tests/e2e/flora-microbiome-flow.spec.js` 验证兼容
-8. 微信开发者工具手动验证关键操作
+## 17. 实现顺序
+
+1. 后端:`HealthKnowledgeBase` 表迁移 + 实体/Mapper/Service/Controller → `mvn clean compile`
+2. 后端:`POST /api/health/report/edit` + `updateReportFromPayload` → `mvn clean compile`
+3. 导入参考 JSON 的说明数据到 `health_knowledge_base`
+4. 前端:`components/health-knowledge-popup.vue` 通用弹框组件
+5. 前端:新建 `gut-flora-foods-detail.vue`(最简单)→ 验证编辑流
+6. 前端:新建 `gut-flora-risks-detail.vue` + 知识库弹框
+7. 前端:新建 `gut-flora-species-detail.vue` + 12 Tab + 颜色箭头 + 知识库弹框
+8. 前端:重写 `gut-flora-detail.vue`(手风琴 + 指标知识库 + 颜色箭头 + 双模式)
+9. 更新 `pages.json` + `utils/api.js`
+10. 跑 `tests/e2e/flora-microbiome-flow.spec.js` 验证
+11. 微信开发者工具手动验证关键操作