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

chore: auto bump version and changelog [skip ci]

iwt 3 месяцев назад
Родитель
Сommit
748f1a9957

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-2cd3a23fe70701e94b9bd29b24a4816777c7bf8b
+597fb096a22eed62a2cb389f493c46081c78aaae

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

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

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

@@ -4,6 +4,34 @@
 
 ---
 
+## v1.0.381 (2026-07-17)
+
+### 重构
+- 扁平化营养师/管家入驻申请页面结构 + 统一import路径为@别名
+
+### 其他
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+- - FamilyTianpanCard: 新增 themeStyle/isChild/handleCompassClick
+- - family-dashboard: 添加隐藏 canvas 用于 PDF 渲染
+- - Profile: 移除富沛能量 UI(Header/Menu/Stats)
+- 
+
+### 新功能
+- 富维度页面添加TabTransition动画 + 用户卡片重构
+- 心维度页面集成家庭天盘模块 + 心理报告上传功能
+- 新增天盘周报PDF导出,移除富沛能量模块
+
+### Bug 修复
+- FamilyTianpanCard 修复多余闭合括号导致编译失败 + Canvas动画改用setTimeout兼容小程序
+
+
 ## v1.0.380 (2026-07-17)
 
 ### 文档

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

@@ -1,6 +1,6 @@
 # 更新日志
 
-> 当前版本: v1.0.380
+> 当前版本: v1.0.381
 
 ## 历史版本
 
@@ -8,6 +8,34 @@
 
 ---
 
+## v1.0.381 (2026-07-17)
+
+### 重构
+- 扁平化营养师/管家入驻申请页面结构 + 统一import路径为@别名
+
+### 其他
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+- - FamilyTianpanCard: 新增 themeStyle/isChild/handleCompassClick
+- - family-dashboard: 添加隐藏 canvas 用于 PDF 渲染
+- - Profile: 移除富沛能量 UI(Header/Menu/Stats)
+- 
+
+### 新功能
+- 富维度页面添加TabTransition动画 + 用户卡片重构
+- 心维度页面集成家庭天盘模块 + 心理报告上传功能
+- 新增天盘周报PDF导出,移除富沛能量模块
+
+### Bug 修复
+- FamilyTianpanCard 修复多余闭合括号导致编译失败 + Canvas动画改用setTimeout兼容小程序
+
+
 ## v1.0.380 (2026-07-17)
 
 ### 文档

+ 2142 - 0
docs/superpowers/plans/2026-07-17-flora-microbiome-report-implementation.md

@@ -0,0 +1,2142 @@
+# 菌群报告页面重构 Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 重构肠道菌群报告详情页,全覆盖参考 JSON 数据(200+ 菌属、50+ 指标、224 食物),支持查看/编辑双模式,独立知识库弹出框,异常值颜色+箭头。
+
+**Architecture:** 4 页架构(主页 + 菌群详情 + 食物推荐 + 疾病风险),后端新增 health_knowledge_base 表 + 编辑 API,前端新增通用知识库弹出框组件。
+
+**Tech Stack:** Java 8 + Spring Boot 2.7.18 + MyBatis-Plus / uni-app (Vue 2 Options API) / WeChat Mini-Program / Playwright E2E
+
+---
+
+## 文件结构总览
+
+### 后端(cfc-backend)
+| 文件 | 操作 | 职责 |
+|------|------|------|
+| `entity/HealthKnowledgeBase.java` | NEW | 知识库实体 |
+| `mapper/HealthKnowledgeBaseMapper.java` | NEW | MyBatis-Plus Mapper |
+| `service/HealthKnowledgeBaseService.java` | NEW | 知识库查询(单条+批量) |
+| `controller/HealthKnowledgeBaseController.java` | NEW | `POST /api/health/knowledge/query` |
+| `controller/HealthReportController.java` | MODIFY | 新增 `POST /api/health/report/edit` |
+| `service/HealthReportService.java` | MODIFY | 新增 `updateReportFromPayload` |
+| `config/DatabaseInitializer.java` | MODIFY | 迁移93: 创建 health_knowledge_base 表 |
+| `resources/schema.sql` | MODIFY | 追加 health_knowledge_base DDL |
+
+### 前端(cfc-frontend)
+| 文件 | 操作 | 职责 |
+|------|------|------|
+| `components/health-knowledge-popup.vue` | NEW | 通用知识库弹出框组件 |
+| `pages/health/gut-flora-detail.vue` | REWRITE | 主页(评分+手风琴+指标+编辑) |
+| `pages/health/gut-flora-species-detail.vue` | NEW | 菌群详情(12 Tab + 卡片) |
+| `pages/health/gut-flora-foods-detail.vue` | NEW | 食物推荐(筛选+卡片+编辑) |
+| `pages/health/gut-flora-risks-detail.vue` | NEW | 疾病风险(分类+卡片+编辑) |
+| `pages.json` | MODIFY | 注册 3 个新页面 |
+| `utils/api.js` | MODIFY | 新增 editHealthReport、queryKnowledgeBase、batchQueryKnowledgeBase |
+
+### 测试
+| 文件 | 操作 | 职责 |
+|------|------|------|
+| `tests/e2e/flora-microbiome-flow.spec.js` | MODIFY | 验证知识库交互+新选择器 |
+
+---
+
+### Task 1: 后端 — HealthKnowledgeBase 实体类
+
+**Files:**
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/entity/HealthKnowledgeBase.java`
+
+- [ ] **Step 1: 创建实体类**
+
+```java
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 健康知识库 — 菌属/营养素/指标标准说明
+ * 独立于报告数据存储,通过 itemType + itemName 精确匹配
+ */
+@Data
+@TableName("health_knowledge_base")
+public class HealthKnowledgeBase implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 类型: bacteria/nutrient/indicator/vitamin/amino_acid/disease */
+    private String itemType;
+
+    /** 项目名称(精确匹配,如 "双歧杆菌属 Bifidobacterium") */
+    private String itemName;
+
+    /** 分类(如 有益菌/有害菌) */
+    private String category;
+
+    /** 正常范围参考(如 "0.19-14.59") */
+    private String normalRange;
+
+    /** 详细说明文字 */
+    private String description;
+
+    /** 相关建议(如补充来源) */
+    private String suggestion;
+
+    /** 数据来源 */
+    private String source;
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}
+```
+
+- [ ] **Step 2: 编译验证**
+
+Run: `cd /sc-data/cfc/cfc-backend && mvn clean compile`
+Expected: BUILD SUCCESS
+
+- [ ] **Step 3: Commit**
+
+```bash
+cd /sc-data/cfc
+git add cfc-backend/src/main/java/com/etotem/cfc/entity/HealthKnowledgeBase.java
+git commit -m "feat(backend): add HealthKnowledgeBase entity"
+```
+
+---
+
+### Task 2: 后端 — HealthKnowledgeBase Mapper
+
+**Files:**
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/mapper/HealthKnowledgeBaseMapper.java`
+
+- [ ] **Step 1: 创建 Mapper 接口**
+
+```java
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.HealthKnowledgeBase;
+
+public interface HealthKnowledgeBaseMapper extends BaseMapper<HealthKnowledgeBase> {
+}
+```
+
+- [ ] **Step 2: 编译验证**
+
+Run: `cd /sc-data/cfc/cfc-backend && mvn clean compile`
+Expected: BUILD SUCCESS
+
+- [ ] **Step 3: Commit**
+
+```bash
+cd /sc-data/cfc
+git add cfc-backend/src/main/java/com/etotem/cfc/mapper/HealthKnowledgeBaseMapper.java
+git commit -m "feat(backend): add HealthKnowledgeBaseMapper"
+```
+
+---
+
+### Task 3: 后端 — HealthKnowledgeBase Service
+
+**Files:**
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/service/HealthKnowledgeBaseService.java`
+
+- [ ] **Step 1: 创建 Service 类**
+
+```java
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.entity.HealthKnowledgeBase;
+import com.etotem.cfc.mapper.HealthKnowledgeBaseMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Slf4j
+@Service
+public class HealthKnowledgeBaseService {
+
+    @Resource
+    private HealthKnowledgeBaseMapper healthKnowledgeBaseMapper;
+
+    /**
+     * 单条查询
+     */
+    public HealthKnowledgeBase query(String itemType, String itemName) {
+        return healthKnowledgeBaseMapper.selectOne(
+                new LambdaQueryWrapper<HealthKnowledgeBase>()
+                        .eq(HealthKnowledgeBase::getItemType, itemType)
+                        .eq(HealthKnowledgeBase::getItemName, itemName)
+        );
+    }
+
+    /**
+     * 批量查询 — queries 为 [{itemType, itemName}, ...]
+     * 返回 Map.key = "itemType:itemName"
+     */
+    public Map<String, HealthKnowledgeBase> batchQuery(List<Map<String, String>> queries) {
+        Map<String, HealthKnowledgeBase> result = new HashMap<>();
+        for (Map<String, String> q : queries) {
+            String itemType = q.get("itemType");
+            String itemName = q.get("itemName");
+            if (itemType == null || itemName == null) {
+                continue;
+            }
+            String key = itemType + ":" + itemName;
+            HealthKnowledgeBase entry = query(itemType, itemName);
+            if (entry != null) {
+                result.put(key, entry);
+            }
+        }
+        return result;
+    }
+
+    /**
+     * 插入单条(用于数据导入)
+     */
+    public void insert(HealthKnowledgeBase entry) {
+        healthKnowledgeBaseMapper.insert(entry);
+    }
+}
+```
+
+- [ ] **Step 2: 编译验证**
+
+Run: `cd /sc-data/cfc/cfc-backend && mvn clean compile`
+Expected: BUILD SUCCESS
+
+- [ ] **Step 3: Commit**
+
+```bash
+cd /sc-data/cfc
+git add cfc-backend/src/main/java/com/etotem/cfc/service/HealthKnowledgeBaseService.java
+git commit -m "feat(backend): add HealthKnowledgeBaseService (query + batchQuery)"
+```
+
+---
+
+### Task 4: 后端 — HealthKnowledgeBase Controller
+
+**Files:**
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/controller/HealthKnowledgeBaseController.java`
+- Bean 名冲突检查: `grep -rn 'class HealthKnowledgeBaseController' src/main/java/com/etotem/cfc/controller/` 确保无重名
+
+- [ ] **Step 1: 检查路由重复**
+
+Run: `cd /sc-data/cfc/cfc-backend && grep -rn '@Mapping' src/main/java/com/etotem/cfc/controller/ | grep -oP '@\w+Mapping\("\K[^"]*' | sort -u`
+Expected: /api/health/knowledge/query 未出现
+
+- [ ] **Step 2: 创建 Controller**
+
+```java
+package com.etotem.cfc.controller;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.HealthKnowledgeBase;
+import com.etotem.cfc.service.HealthKnowledgeBaseService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.util.List;
+import java.util.Map;
+
+@Slf4j
+@Tag(name = "健康知识库")
+@RestController
+@RequestMapping("/api/health/knowledge")
+public class HealthKnowledgeBaseController {
+
+    @Resource
+    private HealthKnowledgeBaseService healthKnowledgeBaseService;
+
+    @Operation(summary = "查询知识库条目")
+    @PostMapping("/query")
+    public Result<HealthKnowledgeBase> query(@RequestBody Map<String, String> params) {
+        String itemType = params.get("itemType");
+        String itemName = params.get("itemName");
+        if (itemType == null || itemName == null) {
+            return Result.error("itemType和itemName不能为空");
+        }
+        HealthKnowledgeBase entry = healthKnowledgeBaseService.query(itemType, itemName);
+        return Result.success(entry);
+    }
+
+    @Operation(summary = "批量查询知识库")
+    @PostMapping("/batch-query")
+    public Result<Map<String, HealthKnowledgeBase>> batchQuery(@RequestBody Map<String, Object> params) {
+        @SuppressWarnings("unchecked")
+        List<Map<String, String>> queries = (List<Map<String, String>>) params.get("queries");
+        if (queries == null || queries.isEmpty()) {
+            return Result.error("queries不能为空");
+        }
+        Map<String, HealthKnowledgeBase> result = healthKnowledgeBaseService.batchQuery(queries);
+        return Result.success(result);
+    }
+}
+```
+
+- [ ] **Step 3: 编译验证**
+
+Run: `cd /sc-data/cfc/cfc-backend && mvn clean compile`
+Expected: BUILD SUCCESS
+
+- [ ] **Step 4: Commit**
+
+```bash
+cd /sc-data/cfc
+git add cfc-backend/src/main/java/com/etotem/cfc/controller/HealthKnowledgeBaseController.java
+git commit -m "feat(backend): add HealthKnowledgeBaseController (query + batch-query)"
+```
+
+---
+
+### Task 5: 后端 — 数据库迁移(创建 health_knowledge_base 表)
+
+**Files:**
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java`
+- Modify: `cfc-backend/src/main/resources/schema.sql`
+
+- [ ] **Step 1: 在 DatabaseInitializer 添加迁移 93**
+
+在 `runMigrations()` 末尾(`// 迁移92` 之后)追加:
+
+```java
+        // 迁移93: 创建 health_knowledge_base 表(菌群/营养知识库)
+        try {
+            jdbcTemplate.execute("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 DEFAULT CURRENT_TIMESTAMP, " +
+                    "`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+                    "UNIQUE KEY `uk_item_type_name` (`item_type`, `item_name`), " +
+                    "KEY `idx_item_type` (`item_type`)" +
+                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康知识库 - 菌属/营养素/指标说明'");
+            log.info("已创建health_knowledge_base表");
+        } catch (Exception e) {
+            // 表已存在,忽略错误
+        }
+```
+
+**Location detail:** 在 DatabaseInitializer.java 末尾的 `runMigrations()` 中,最后一个 try-catch 之后、`}` 闭合之前插入。当前第 5884 行附近。
+
+- [ ] **Step 2: 在 schema.sql 末尾追加 DDL**
+
+```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 DEFAULT CURRENT_TIMESTAMP,
+    `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY `uk_item_type_name` (`item_type`, `item_name`),
+    KEY `idx_item_type` (`item_type`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康知识库 - 菌属/营养素/指标说明';
+```
+
+- [ ] **Step 3: 编译验证**
+
+Run: `cd /sc-data/cfc/cfc-backend && mvn clean compile`
+Expected: BUILD SUCCESS
+
+- [ ] **Step 4: Commit**
+
+```bash
+cd /sc-data/cfc
+git add cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
+git add cfc-backend/src/main/resources/schema.sql
+git commit -m "feat(backend): add health_knowledge_base table (migration 93)"
+```
+
+---
+
+### Task 6: 后端 — 报告编辑 API(POST /api/health/report/edit)
+
+**Files:**
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/controller/HealthReportController.java`
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/HealthReportService.java`
+
+- [ ] **Step 1: 在 Controller 中新增编辑端点**
+
+在 `HealthReportController` 中(`getReportDetail` 方法之后)追加:
+
+```java
+    /**
+     * 编辑已发布报告 — 修正 AI 解析错误的值
+     * 只修改已有条目(不增删),保存后刷新 7 维健康评分
+     */
+    @Operation(summary = "编辑已发布报告数据")
+    @PostMapping("/report/edit")
+    public Result<String> editReport(
+            @RequestBody Map<String, Object> params,
+            @RequestAttribute("userId") Long userId) {
+
+        Long reportId = params.get("reportId") != null
+                ? Long.valueOf(params.get("reportId").toString()) : null;
+        if (reportId == null) {
+            return Result.error("reportId不能为空");
+        }
+        Long subjectId = params.get("subjectId") != null
+                ? Long.valueOf(params.get("subjectId").toString()) : null;
+        if (subjectId == null) {
+            return Result.error("subjectId不能为空");
+        }
+
+        @SuppressWarnings("unchecked")
+        Map<String, Object> payload = (Map<String, Object>) params.get("payload");
+        if (payload == null) {
+            return Result.error("payload不能为空");
+        }
+
+        healthReportService.updateReportFromPayload(reportId, payload, subjectId);
+        return Result.success("已保存");
+    }
+```
+
+- [ ] **Step 2: 在 Service 中新增 `updateReportFromPayload`**
+
+```java
+    /**
+     * 更新已发布报告的编辑数据
+     * 逻辑: 只更新已有字段的值,不增删条目
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public void updateReportFromPayload(Long reportId, Map<String, Object> payload, Long subjectId) {
+        // 1. 更新 health_report 评分数值
+        HealthReport report = healthReportMapper.selectById(reportId);
+        if (report == null) {
+            throw new RuntimeException("报告不存在: " + reportId);
+        }
+        if (payload.get("overallScore") != null) {
+            report.setOverallScore(Integer.valueOf(payload.get("overallScore").toString()));
+        }
+        if (payload.get("gutHealthScore") != null) {
+            report.setGutHealthScore(Integer.valueOf(payload.get("gutHealthScore").toString()));
+        }
+        if (payload.get("chronicDiseaseScore") != null) {
+            report.setChronicDiseaseScore(Integer.valueOf(payload.get("chronicDiseaseScore").toString()));
+        }
+        if (payload.get("nutritionScore") != null) {
+            report.setNutritionScore(Integer.valueOf(payload.get("nutritionScore").toString()));
+        }
+        if (payload.get("gutBalanceScore") != null) {
+            report.setGutBalanceScore(Integer.valueOf(payload.get("gutBalanceScore").toString()));
+        }
+        if (payload.get("gutDiversityScore") != null) {
+            report.setGutDiversityScore(Integer.valueOf(payload.get("gutDiversityScore").toString()));
+        }
+        if (payload.get("beneficialBacteriaScore") != null) {
+            report.setBeneficialBacteriaScore(Integer.valueOf(payload.get("beneficialBacteriaScore").toString()));
+        }
+        if (payload.get("harmfulBacteriaScore") != null) {
+            report.setHarmfulBacteriaScore(Integer.valueOf(payload.get("harmfulBacteriaScore").toString()));
+        }
+        if (payload.get("coreSpeciesScore") != null) {
+            report.setCoreSpeciesScore(Integer.valueOf(payload.get("coreSpeciesScore").toString()));
+        }
+        healthReportMapper.updateById(report);
+
+        // 2. 更新 indicators(删除旧→插入新)
+        @SuppressWarnings("unchecked")
+        List<Map<String, Object>> indicators = (List<Map<String, Object>>) payload.get("indicators");
+        if (indicators != null) {
+            healthIndicatorMapper.delete(new LambdaQueryWrapper<HealthIndicator>()
+                    .eq(HealthIndicator::getReportId, reportId));
+            for (int i = 0; i < indicators.size(); i++) {
+                Map<String, Object> item = indicators.get(i);
+                HealthIndicator ind = new HealthIndicator();
+                ind.setReportId(reportId);
+                ind.setCategory((String) item.get("category"));
+                ind.setIndicatorName((String) item.get("indicatorName"));
+                ind.setIndicatorValue(item.get("indicatorValue") != null
+                        ? item.get("indicatorValue").toString() : null);
+                ind.setUnit((String) item.get("unit"));
+                ind.setRefRange((String) item.get("refRange"));
+                ind.setStatus((String) item.get("status"));
+                ind.setSymptoms((String) item.get("symptoms"));
+                ind.setSortOrder(i);
+                healthIndicatorMapper.insert(ind);
+            }
+        }
+
+        // 3. 更新 gutFlora
+        @SuppressWarnings("unchecked")
+        List<Map<String, Object>> gutFlora = (List<Map<String, Object>>) payload.get("gutFlora");
+        if (gutFlora != null) {
+            healthGutFloraMapper.delete(new LambdaQueryWrapper<HealthGutFlora>()
+                    .eq(HealthGutFlora::getReportId, reportId));
+            for (int i = 0; i < gutFlora.size(); i++) {
+                Map<String, Object> item = gutFlora.get(i);
+                HealthGutFlora flora = new HealthGutFlora();
+                flora.setReportId(reportId);
+                flora.setBacteriaName((String) item.get("bacteriaName"));
+                flora.setBacteriaValue(item.get("bacteriaValue") != null
+                        ? item.get("bacteriaValue").toString() : null);
+                flora.setNormalRange((String) item.get("normalRange"));
+                flora.setStatus((String) item.get("status"));
+                flora.setCategory((String) item.get("category"));
+                flora.setPopulationLevel((String) item.get("populationLevel"));
+                flora.setDetectionRate((String) item.get("detectionRate"));
+                flora.setLevel((String) item.get("level"));
+                flora.setSortOrder(i);
+                healthGutFloraMapper.insert(flora);
+            }
+        }
+
+        // 4. 更新 diseaseRisks
+        @SuppressWarnings("unchecked")
+        List<Map<String, Object>> diseaseRisks = (List<Map<String, Object>>) payload.get("diseaseRisks");
+        if (diseaseRisks != null) {
+            healthDiseaseRiskMapper.delete(new LambdaQueryWrapper<HealthDiseaseRisk>()
+                    .eq(HealthDiseaseRisk::getReportId, reportId));
+            for (Map<String, Object> item : diseaseRisks) {
+                HealthDiseaseRisk risk = new HealthDiseaseRisk();
+                risk.setReportId(reportId);
+                risk.setDiseaseName((String) item.get("diseaseName"));
+                risk.setRiskValue(item.get("riskValue") != null
+                        ? item.get("riskValue").toString() : null);
+                risk.setRiskLevel((String) item.get("riskLevel"));
+                healthDiseaseRiskMapper.insert(risk);
+            }
+        }
+
+        // 5. 刷新 7 维健康评分
+        try {
+            dimensionScoreService.refreshFromGutReport(subjectId, reportId);
+            log.info("编辑后已刷新成员{}的7维健康评分", subjectId);
+        } catch (Exception e) {
+            log.warn("刷新7维健康评分失败: {}", e.getMessage());
+        }
+    }
+```
+
+- [ ] **Step 3: 在 Controller 上方追加所需的 import**
+
+确认文件顶部已有:
+```java
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import org.springframework.transaction.annotation.Transactional;
+```
+
+如果 `@Transactional` 没有 import,在文件顶部追加:
+```java
+import org.springframework.transaction.annotation.Transactional;
+```
+
+- [ ] **Step 4: 确保 HealthReportService 中有需要的 Mapper 注入**
+
+在 `HealthReportService.java` 中确认以下字段已存在,若不存在则添加:
+```java
+    @Resource
+    private HealthIndicatorMapper healthIndicatorMapper;
+
+    @Resource
+    private HealthGutFloraMapper healthGutFloraMapper;
+
+    @Resource
+    private HealthDiseaseRiskMapper healthDiseaseRiskMapper;
+
+    @Resource
+    private HealthReportMapper healthReportMapper;
+
+    @Resource
+    private DimensionScoreService dimensionScoreService;
+```
+
+- [ ] **Step 5: 编译验证**
+
+Run: `cd /sc-data/cfc/cfc-backend && mvn clean compile`
+Expected: BUILD SUCCESS
+
+- [ ] **Step 6: Commit**
+
+```bash
+cd /sc-data/cfc
+git add cfc-backend/
+git commit -m "feat(backend): add POST /api/health/report/edit + updateReportFromPayload"
+```
+
+---
+
+### Task 7: 后端 — 导入参考 JSON 知识库数据
+
+**Files:**
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/service/KnowledgeBaseDataImporter.java`
+- 数据源: `docs/superpowers/refs/501999942-某人.json`, `530010234-侯.json`, `儿童示例.json`
+
+- [ ] **Step 1: 创建数据导入 Service(一次性工具)**
+
+```java
+package com.etotem.cfc.service;
+
+import com.etotem.cfc.entity.HealthKnowledgeBase;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.Resource;
+import java.io.InputStream;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * 知识库数据导入器 — 从参考 JSON 导入菌属/指标说明
+ * 仅在 health_knowledge_base 为空时执行一次
+ */
+@Slf4j
+@Service
+public class KnowledgeBaseDataImporter {
+
+    @Resource
+    private HealthKnowledgeBaseService knowledgeBaseService;
+
+    @Resource
+    private ObjectMapper objectMapper;
+
+    private static final String[] REF_JSON_FILES = {
+        "/data/refs/501999942-某人.json",
+        "/data/refs/530010234-侯.json",
+        "/data/refs/儿童示例.json"
+    };
+
+    @PostConstruct
+    public void init() {
+        // 延迟执行,等待数据库表就绪
+        new Thread(() -> {
+            try {
+                Thread.sleep(5000);
+                importAll();
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+            }
+        }).start();
+    }
+
+    public void importAll() {
+        try {
+            // 检查是否已有数据
+            HealthKnowledgeBase existing = knowledgeBaseService.query("bacteria", "双歧杆菌属");
+            if (existing != null) {
+                log.info("知识库已有数据,跳过导入");
+                return;
+            }
+
+            Set<String> dedupSet = new HashSet<>();
+
+            for (String jsonFile : REF_JSON_FILES) {
+                importFromRef(jsonFile, dedupSet);
+            }
+
+            log.info("知识库数据导入完成");
+        } catch (Exception e) {
+            log.error("知识库数据导入失败", e);
+        }
+    }
+
+    private void importFromRef(String classpathFile, Set<String> dedupSet) {
+        try (InputStream is = getClass().getResourceAsStream(classpathFile)) {
+            if (is == null) {
+                log.warn("参考JSON文件不存在: {}", classpathFile);
+                return;
+            }
+            Map<String, Object> root = objectMapper.readValue(is, new TypeReference<Map<String, Object>>() {});
+
+            // 导入菌属说明(gutFlora 数组含 description 字段)
+            importGutFloraDescriptions(root, dedupSet);
+
+            // 导入指标说明(indicators 数组含 status/refRange 等)
+            importIndicatorDescriptions(root, dedupSet);
+
+        } catch (Exception e) {
+            log.error("读取参考JSON失败: {}", classpathFile, e);
+        }
+    }
+
+    private void importGutFloraDescriptions(Map<String, Object> root, Set<String> dedupSet) {
+        Object raw = root.get("gutFlora");
+        if (raw instanceof List) {
+            for (Object item : (List) raw) {
+                if (item instanceof Map) {
+                    Map<String, Object> flora = (Map<String, Object>) item;
+                    String name = (String) flora.get("bacteriaName");
+                    String desc = (String) flora.get("description");
+                    if (name != null && desc != null && !desc.isEmpty()) {
+                        String key = "bacteria:" + name;
+                        if (dedupSet.add(key)) {
+                            HealthKnowledgeBase entry = new HealthKnowledgeBase();
+                            entry.setItemType("bacteria");
+                            entry.setItemName(name);
+                            entry.setDescription(desc);
+                            entry.setCategory((String) flora.get("category"));
+                            entry.setNormalRange((String) flora.get("normalRange"));
+                            knowledgeBaseService.insert(entry);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    private void importIndicatorDescriptions(Map<String, Object> root, Set<String> dedupSet) {
+        Object raw = root.get("indicators");
+        if (raw instanceof List) {
+            for (Object item : (List) raw) {
+                if (item instanceof Map) {
+                    Map<String, Object> ind = (Map<String, Object>) item;
+                    String name = (String) ind.get("indicatorName");
+                    String category = (String) ind.get("category");
+                    if (name == null) continue;
+
+                    // 将 category 映射为 itemType
+                    String itemType = mapIndicatorCategory(category);
+                    String key = itemType + ":" + name;
+                    if (dedupSet.add(key)) {
+                        HealthKnowledgeBase entry = new HealthKnowledgeBase();
+                        entry.setItemType(itemType);
+                        entry.setItemName(name);
+                        entry.setCategory(category);
+                        entry.setNormalRange((String) ind.get("refRange"));
+                        entry.setDescription((String) ind.get("symptoms"));
+                        // 部分 JSON 用 status 字段描述健康状况
+                        knowledgeBaseService.insert(entry);
+                    }
+                }
+            }
+        }
+    }
+
+    private String mapIndicatorCategory(String category) {
+        if (category == null) return "nutrient";
+        switch (category) {
+            case "维生素": return "vitamin";
+            case "氨基酸": return "amino_acid";
+            case "微量元素": return "indicator";
+            case "肠道屏障与代谢物": return "indicator";
+            case "短链脂肪酸": return "indicator";
+            case "神经递质与激素": return "indicator";
+            case "抗生素风险评估": return "indicator";
+            default: return "nutrient";
+        }
+    }
+}
+```
+
+- [ ] **Step 2: 创建资源目录并将参考 JSON 复制到 resources/data/refs/**
+
+```bash
+mkdir -p /sc-data/cfc/cfc-backend/src/main/resources/data/refs
+cp /sc-data/cfc/docs/superpowers/refs/501999942-某人.json /sc-data/cfc/cfc-backend/src/main/resources/data/refs/
+cp /sc-data/cfc/docs/superpowers/refs/530010234-侯.json /sc-data/cfc/cfc-backend/src/main/resources/data/refs/
+cp /sc-data/cfc/docs/superpowers/refs/儿童示例.json /sc-data/cfc/cfc-backend/src/main/resources/data/refs/
+```
+
+- [ ] **Step 3: 编译验证**
+
+Run: `cd /sc-data/cfc/cfc-backend && mvn clean compile`
+Expected: BUILD SUCCESS
+
+- [ ] **Step 4: Commit**
+
+```bash
+cd /sc-data/cfc
+git add cfc-backend/
+git commit -m "feat(backend): add KnowledgeBaseDataImporter + ref JSONs"
+```
+
+---
+
+### Task 8: 前端 — api.js 新增方法
+
+- [ ] **Step 1: 在 utils/api.js 中追加 3 个方法**(在 `getReportDetail` 附近,约第 1515 行)
+
+```javascript
+export const editHealthReport = (reportId, payload, subjectId) => request('/api/health/report/edit', 'POST', { reportId, payload, subjectId })
+
+export const queryKnowledgeBase = (itemType, itemName) => request('/api/health/knowledge/query', 'POST', { itemType, itemName })
+
+export const batchQueryKnowledgeBase = (queries) => request('/api/health/knowledge/batch-query', 'POST', { queries })
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+cd /sc-data/cfc
+git add cfc-frontend/utils/api.js
+git commit -m "feat(frontend): add editHealthReport, queryKnowledgeBase, batchQueryKnowledgeBase API"
+```
+
+---
+
+### Task 9: 前端 — 知识库弹出框组件
+
+**Files:**
+- Create: `cfc-frontend/components/health-knowledge-popup.vue`
+
+- [ ] **Step 1: 创建弹出框组件**
+
+```vue
+<template>
+  <view class="kb-popup-overlay" v-if="visible" @tap="close">
+    <view class="kb-popup-content" @tap.stop>
+      <view class="kb-popup-header">
+        <text class="kb-popup-title">{{ data.itemName || '' }}</text>
+        <text class="kb-popup-close" @tap="close">×</text>
+      </view>
+      <scroll-view class="kb-popup-body" scroll-y>
+        <view class="kb-field" v-if="data.category">
+          <text class="kb-field-label">分类</text>
+          <text class="kb-field-value">{{ data.category }}</text>
+        </view>
+        <view class="kb-field" v-if="data.normalRange">
+          <text class="kb-field-label">正常范围</text>
+          <text class="kb-field-value">{{ data.normalRange }}</text>
+        </view>
+        <view class="kb-field" v-if="data.description">
+          <text class="kb-field-label">说明</text>
+          <text class="kb-field-value">{{ data.description }}</text>
+        </view>
+        <view class="kb-field" v-if="data.suggestion">
+          <text class="kb-field-label">相关建议</text>
+          <text class="kb-field-value">{{ data.suggestion }}</text>
+        </view>
+        <view class="kb-empty" v-if="!data.itemName">
+          <text>暂无相关说明</text>
+        </view>
+      </scroll-view>
+    </view>
+  </view>
+</template>
+
+<script>
+export default {
+  props: {
+    visible: { type: Boolean, default: false },
+    data: { type: Object, default: () => ({}) }
+  },
+  methods: {
+    close: function() {
+      this.$emit('close')
+    }
+  }
+}
+</script>
+
+<style scoped>
+.kb-popup-overlay {
+  position: fixed;
+  top: 0; left: 0; right: 0; bottom: 0;
+  background: rgba(0,0,0,0.5);
+  z-index: 999;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.kb-popup-content {
+  width: 600rpx;
+  max-height: 70vh;
+  background: #fff;
+  border-radius: 24rpx;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+}
+.kb-popup-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 30rpx 30rpx 20rpx;
+  border-bottom: 1rpx solid #f0f0f0;
+}
+.kb-popup-title {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #333;
+  flex: 1;
+}
+.kb-popup-close {
+  font-size: 40rpx;
+  color: #999;
+  padding: 0 10rpx;
+}
+.kb-popup-body {
+  padding: 20rpx 30rpx 30rpx;
+  max-height: 50vh;
+}
+.kb-field {
+  margin-bottom: 20rpx;
+}
+.kb-field-label {
+  font-size: 26rpx;
+  color: #999;
+  display: block;
+  margin-bottom: 6rpx;
+}
+.kb-field-value {
+  font-size: 28rpx;
+  color: #333;
+  line-height: 1.6;
+}
+.kb-empty {
+  text-align: center;
+  color: #ccc;
+  padding: 40rpx 0;
+  font-size: 28rpx;
+}
+</style>
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+cd /sc-data/cfc
+git add cfc-frontend/components/health-knowledge-popup.vue
+git commit -m "feat(frontend): add health-knowledge-popup component"
+```
+
+---
+
+### Task 10: 前端 — pages.json 注册 3 个新页面
+
+- [ ] **Step 1: 在 pages.json 中 health 分组内追加 3 条路径**
+
+在 `"path": "pages/health/gut-flora-detail"` 条目之后追加:
+
+```json
+    {
+      "path": "pages/health/gut-flora-species-detail",
+      "style": {
+        "navigationBarTitleText": "菌群详情"
+      }
+    },
+    {
+      "path": "pages/health/gut-flora-foods-detail",
+      "style": {
+        "navigationBarTitleText": "饮食推荐"
+      }
+    },
+    {
+      "path": "pages/health/gut-flora-risks-detail",
+      "style": {
+        "navigationBarTitleText": "疾病风险"
+      }
+    }
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+cd /sc-data/cfc
+git add cfc-frontend/pages.json
+git commit -m "feat(frontend): register 3 new health detail pages in pages.json"
+```
+
+---
+
+### Task 11: 前端 — 食物推荐详情页
+
+**Files:**
+- Create: `cfc-frontend/pages/health/gut-flora-foods-detail.vue`
+
+- [ ] **Step 1: 创建页面(最简单页面,先做以验证编辑流)**
+
+```vue
+<template>
+  <view class="page">
+    <view class="nav-bar">
+      <view class="nav-back" @tap="goBack">
+        <text class="back-text">‹ 返回</text>
+      </view>
+      <text class="nav-title">饮食推荐</text>
+      <text class="nav-edit" @tap="toggleEdit">{{ isEditing ? '完成' : '编辑' }}</text>
+    </view>
+
+    <!-- 分类筛选条 -->
+    <scroll-view class="filter-bar" scroll-x>
+      <view class="filter-item" v-for="(cat, idx) in categories" :key="idx"
+            :class="'filter-' + (selectedCategory === cat ? 'active' : '')"
+            @tap="selectedCategory = cat">
+        <text>{{ cat }}</text>
+      </view>
+    </scroll-view>
+
+    <!-- 排序切换 -->
+    <view class="sort-bar">
+      <text class="sort-text" @tap="toggleSort">排序: {{ sortDesc ? '推荐↓' : '推荐↑' }}</text>
+      <text class="sort-text" @tap="toggleFilter">仅推荐</text>
+    </view>
+
+    <scroll-view class="content" scroll-y>
+      <view class="food-card" v-for="(item, idx) in filteredFoods" :key="idx">
+        <view class="food-header">
+          <text class="food-name">{{ item.foodName || item.name }}</text>
+          <view class="food-cat-tag">
+            <text>{{ item.category }}</text>
+          </view>
+        </view>
+        <view class="food-detail-row" v-if="!isEditing">
+          <text class="food-nutrition">蛋白{{ item.protein || '--' }} | 脂肪{{ item.fat || '--' }} | 碳水{{ item.carb || '--' }} | 纤维{{ item.fiber || '--' }}</text>
+        </view>
+        <view class="food-detail-row" v-if="!isEditing">
+          <text class="food-nutrition" v-if="item.energy">能量 {{ item.energy }}KJ</text>
+        </view>
+        <view class="food-edit-row" v-if="isEditing">
+          <text class="edit-label">推荐指数:</text>
+          <input class="edit-input" type="number" v-model="item.score" />
+        </view>
+        <view class="food-score" :class="'score-' + (item.score >= 5 ? 'good' : 'normal')">
+          <text>推荐: {{ item.score }}</text>
+        </view>
+      </view>
+      <view class="empty-hint" v-if="filteredFoods.length === 0">暂无数据</view>
+      <view class="bottom-spacer"></view>
+    </scroll-view>
+
+    <!-- 保存按钮(编辑模式) -->
+    <view class="bottom-bar" v-if="isEditing">
+      <view class="btn-cancel" @tap="cancelEdit">取消</view>
+      <view class="btn-save" @tap="saveEdit">保存</view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getReportDetail, editHealthReport } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      reportId: null,
+      reportData: null,
+      foods: [],
+      isEditing: false,
+      editFoods: [],
+      selectedCategory: '全部',
+      sortDesc: true,
+      showRecommendedOnly: false,
+      categories: ['全部', '主食', '蔬菜', '水果', '肉类', '其它']
+    }
+  },
+  computed: {
+    filteredFoods: function() {
+      let list = this.isEditing ? this.editFoods : this.foods
+      if (this.selectedCategory !== '全部') {
+        list = list.filter(function(item) {
+          return item.category === this.selectedCategory
+        }.bind(this))
+      }
+      if (this.showRecommendedOnly) {
+        list = list.filter(function(item) {
+          return (item.score || 0) >= 5
+        })
+      }
+      // 排序
+      list = [].concat(list)
+      list.sort(function(a, b) {
+        var sa = a.score || 0
+        var sb = b.score || 0
+        return this.sortDesc ? sb - sa : sa - sb
+      }.bind(this))
+      return list
+    }
+  },
+  onLoad: function(options) {
+    this.reportId = options.reportId ? parseInt(options.reportId) : null
+    if (this.reportId) {
+      this.loadData()
+    }
+  },
+  methods: {
+    goBack: function() {
+      uni.navigateBack()
+    },
+    loadData: function() {
+      var self = this
+      getReportDetail(this.reportId).then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.reportData = res.data
+          self.foods = res.data.foods || []
+        }
+      })
+    },
+    toggleEdit: function() {
+      this.isEditing = true
+      // 深拷贝
+      this.editFoods = JSON.parse(JSON.stringify(this.foods))
+    },
+    cancelEdit: function() {
+      this.isEditing = false
+      this.editFoods = []
+    },
+    saveEdit: function() {
+      var self = this
+      editHealthReport(self.reportId, { foods: self.editFoods }, 0).then(function(res) {
+        if (res.code === 200) {
+          uni.showToast({ title: '已保存', icon: 'success' })
+          self.isEditing = false
+          self.loadData()
+        } else {
+          uni.showToast({ title: res.message || '保存失败', icon: 'none' })
+        }
+      })
+    },
+    toggleSort: function() {
+      this.sortDesc = !this.sortDesc
+    },
+    toggleFilter: function() {
+      this.showRecommendedOnly = !this.showRecommendedOnly
+    }
+  }
+}
+</script>
+
+<style scoped>
+.page { min-height: 100vh; background: #f8f8f8; }
+.nav-bar { display: flex; align-items: center; padding: 20rpx 30rpx; background: #fff; position: relative; }
+.nav-back { padding: 10rpx 0; }
+.back-text { font-size: 28rpx; color: #4A9BD7; }
+.nav-title { flex: 1; text-align: center; font-size: 32rpx; font-weight: 600; }
+.nav-edit { font-size: 28rpx; color: #4A9BD7; padding: 10rpx 0; }
+
+.filter-bar { background: #fff; padding: 10rpx 20rpx; white-space: nowrap; }
+.filter-item { display: inline-block; padding: 12rpx 28rpx; margin: 0 10rpx; border-radius: 30rpx; font-size: 26rpx; background: #f0f0f0; color: #666; }
+.filter-active { background: #4A9BD7; color: #fff; }
+
+.sort-bar { display: flex; justify-content: space-between; padding: 16rpx 30rpx; background: #fff; border-top: 1rpx solid #eee; }
+.sort-text { font-size: 24rpx; color: #999; }
+
+.content { padding: 20rpx 30rpx; height: calc(100vh - 200rpx); }
+.food-card { background: #fff; border-radius: 16rpx; padding: 24rpx; margin-bottom: 16rpx; }
+.food-header { display: flex; align-items: center; margin-bottom: 12rpx; }
+.food-name { font-size: 30rpx; font-weight: 600; color: #333; flex: 1; }
+.food-cat-tag { padding: 4rpx 16rpx; background: #f0f0f0; border-radius: 20rpx; font-size: 22rpx; color: #666; }
+.food-detail-row { margin-bottom: 6rpx; }
+.food-nutrition { font-size: 24rpx; color: #999; }
+.food-edit-row { display: flex; align-items: center; margin-bottom: 6rpx; }
+.edit-label { font-size: 26rpx; color: #666; margin-right: 12rpx; }
+.edit-input { border: 1rpx solid #ddd; border-radius: 8rpx; padding: 8rpx 12rpx; font-size: 26rpx; width: 120rpx; }
+.food-score { margin-top: 8rpx; font-size: 24rpx; }
+.score-good { color: #10B981; }
+.score-normal { color: #999; }
+.empty-hint { text-align: center; color: #ccc; padding: 60rpx 0; font-size: 28rpx; }
+
+.bottom-bar { position: fixed; bottom: 0; left: 0; right: 0; display: flex; padding: 20rpx 30rpx; background: #fff; border-top: 1rpx solid #eee; gap: 20rpx; }
+.btn-cancel { flex: 1; padding: 20rpx; border: 1rpx solid #ddd; border-radius: 12rpx; text-align: center; font-size: 28rpx; color: #666; }
+.btn-save { flex: 1; padding: 20rpx; background: #4A9BD7; border-radius: 12rpx; text-align: center; font-size: 28rpx; color: #fff; }
+.bottom-spacer { height: 40rpx; }
+</style>
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+cd /sc-data/cfc
+git add cfc-frontend/pages/health/gut-flora-foods-detail.vue
+git commit -m "feat(frontend): add gut-flora-foods-detail page"
+```
+
+---
+
+### Task 12: 前端 — 疾病风险详情页
+
+**Files:**
+- Create: `cfc-frontend/pages/health/gut-flora-risks-detail.vue`
+
+- [ ] **Step 1: 创建页面**
+
+```vue
+<template>
+  <view class="page">
+    <view class="nav-bar">
+      <view class="nav-back" @tap="goBack">
+        <text class="back-text">‹ 返回</text>
+      </view>
+      <text class="nav-title">疾病风险</text>
+      <text class="nav-edit" @tap="toggleEdit">{{ isEditing ? '完成' : '编辑' }}</text>
+    </view>
+
+    <scroll-view class="content" scroll-y>
+      <view class="section-label">重要风险</view>
+      <view class="risk-card" v-for="(item, idx) in importantRisks" :key="'imp-' + idx"
+            :class="'risk-level-' + (item.riskLevel || 'low')">
+        <view class="risk-header">
+          <text class="risk-name" @tap="showKnowledge('disease', item.diseaseName)">{{ item.diseaseName }}</text>
+          <text class="risk-badge" :class="'badge-' + (item.riskLevel || 'low')">
+            {{ riskBadgeText(item.riskLevel) }}
+          </text>
+        </view>
+        <view class="risk-detail-row" v-if="!isEditing">
+          <text class="risk-value">风险值: {{ item.riskValue || '--' }}</text>
+          <text class="risk-level-text">等级: {{ item.riskLevel || '--' }}</text>
+        </view>
+        <view class="risk-edit-row" v-if="isEditing">
+          <text class="edit-label">风险值:</text>
+          <input class="edit-input" type="text" v-model="item.riskValue" />
+          <text class="edit-label">等级:</text>
+          <picker :value="riskLevelIndex(item.riskLevel)" :range="riskLevels" @change="onRiskLevelChange($event, item)">
+            <text class="edit-picker">{{ item.riskLevel || '请选择' }}</text>
+          </picker>
+        </view>
+      </view>
+
+      <view class="section-label">其它风险</view>
+      <view class="risk-card" v-for="(item, idx) in normalRisks" :key="'norm-' + idx"
+            :class="'risk-level-' + (item.riskLevel || 'low')">
+        <view class="risk-header">
+          <text class="risk-name" @tap="showKnowledge('disease', item.diseaseName)">{{ item.diseaseName }}</text>
+          <text class="risk-badge" :class="'badge-' + (item.riskLevel || 'low')">
+            {{ riskBadgeText(item.riskLevel) }}
+          </text>
+        </view>
+        <view class="risk-detail-row" v-if="!isEditing">
+          <text class="risk-value">风险值: {{ item.riskValue || '--' }}</text>
+          <text class="risk-level-text">等级: {{ item.riskLevel || '--' }}</text>
+        </view>
+        <view class="risk-edit-row" v-if="isEditing">
+          <text class="edit-label">风险值:</text>
+          <input class="edit-input" type="text" v-model="item.riskValue" />
+          <text class="edit-label">等级:</text>
+          <picker :value="riskLevelIndex(item.riskLevel)" :range="riskLevels" @change="onRiskLevelChange($event, item)">
+            <text class="edit-picker">{{ item.riskLevel || '请选择' }}</text>
+          </picker>
+        </view>
+      </view>
+      <view class="empty-hint" v-if="risks.length === 0">暂无疾病风险评估</view>
+      <view class="bottom-spacer"></view>
+    </scroll-view>
+
+    <health-knowledge-popup :visible="knowledgeVisible" :data="knowledgeData" @close="closeKnowledge" />
+
+    <view class="bottom-bar" v-if="isEditing">
+      <view class="btn-cancel" @tap="cancelEdit">取消</view>
+      <view class="btn-save" @tap="saveEdit">保存</view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getReportDetail, editHealthReport, queryKnowledgeBase } from '../../utils/api.js'
+import HealthKnowledgePopup from '../../components/health-knowledge-popup.vue'
+
+export default {
+  components: { HealthKnowledgePopup },
+  data() {
+    return {
+      reportId: null,
+      reportData: null,
+      risks: [],
+      isEditing: false,
+      editRisks: [],
+      riskLevels: ['低风险', '需注意', '高风险', '异常'],
+      knowledgeVisible: false,
+      knowledgeData: {}
+    }
+  },
+  computed: {
+    importantRisks: function() {
+      return this.getRisks().filter(function(item) {
+        var level = item.riskLevel || ''
+        return level === '需注意' || level === '高风险' || level === '异常'
+      })
+    },
+    normalRisks: function() {
+      return this.getRisks().filter(function(item) {
+        var level = item.riskLevel || ''
+        return level !== '需注意' && level !== '高风险' && level !== '异常'
+      })
+    }
+  },
+  onLoad: function(options) {
+    this.reportId = options.reportId ? parseInt(options.reportId) : null
+    if (this.reportId) {
+      this.loadData()
+    }
+  },
+  methods: {
+    goBack: function() { uni.navigateBack() },
+    getRisks: function() {
+      return this.isEditing ? this.editRisks : this.risks
+    },
+    loadData: function() {
+      var self = this
+      getReportDetail(this.reportId).then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.reportData = res.data
+          self.risks = res.data.diseaseRisks || []
+        }
+      })
+    },
+    toggleEdit: function() {
+      this.isEditing = true
+      this.editRisks = JSON.parse(JSON.stringify(this.risks))
+    },
+    cancelEdit: function() {
+      this.isEditing = false
+      this.editRisks = []
+    },
+    saveEdit: function() {
+      var self = this
+      editHealthReport(self.reportId, { diseaseRisks: self.editRisks }, 0).then(function(res) {
+        if (res.code === 200) {
+          uni.showToast({ title: '已保存', icon: 'success' })
+          self.isEditing = false
+          self.loadData()
+        } else {
+          uni.showToast({ title: res.message || '保存失败', icon: 'none' })
+        }
+      })
+    },
+    riskBadgeText: function(level) {
+      var map = { '低风险': '✓ 低', '需注意': '⚠ 注意', '高风险': '⚠ 高', '异常': '⚠ 异常' }
+      return map[level] || level || ''
+    },
+    riskLevelIndex: function(level) {
+      var idx = this.riskLevels.indexOf(level)
+      return idx >= 0 ? idx : 0
+    },
+    onRiskLevelChange: function(e, item) {
+      item.riskLevel = this.riskLevels[e.detail.value]
+    },
+    showKnowledge: function(type, name) {
+      var self = this
+      queryKnowledgeBase(type, name).then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.knowledgeData = res.data
+          self.knowledgeVisible = true
+        } else {
+          self.knowledgeData = { itemName: name }
+          self.knowledgeVisible = true
+        }
+      })
+    },
+    closeKnowledge: function() {
+      this.knowledgeVisible = false
+      this.knowledgeData = {}
+    }
+  }
+}
+</script>
+
+<style scoped>
+.page { min-height: 100vh; background: #f8f8f8; }
+.nav-bar { display: flex; align-items: center; padding: 20rpx 30rpx; background: #fff; position: relative; }
+.nav-back { padding: 10rpx 0; }
+.back-text { font-size: 28rpx; color: #4A9BD7; }
+.nav-title { flex: 1; text-align: center; font-size: 32rpx; font-weight: 600; }
+.nav-edit { font-size: 28rpx; color: #4A9BD7; padding: 10rpx 0; }
+
+.content { padding: 20rpx 30rpx; height: calc(100vh - 100rpx); }
+.section-label { font-size: 26rpx; color: #999; margin: 10rpx 0 16rpx; padding-left: 8rpx; }
+
+.risk-card { background: #fff; border-radius: 16rpx; padding: 24rpx; margin-bottom: 16rpx; border-left: 8rpx solid #10B981; }
+.risk-level-需注意 { border-left-color: #F59E0B; }
+.risk-level-高风险 { border-left-color: #C62828; }
+.risk-level-异常 { border-left-color: #C62828; }
+
+.risk-header { display: flex; align-items: center; margin-bottom: 12rpx; }
+.risk-name { font-size: 30rpx; font-weight: 600; color: #333; flex: 1; }
+.risk-badge { padding: 4rpx 16rpx; border-radius: 20rpx; font-size: 22rpx; }
+.badge-低风险 { background: #E8F5E9; color: #2E7D32; }
+.badge-需注意 { background: #FFF3E0; color: #E65100; }
+.badge-高风险 { background: #FFEBEE; color: #C62828; }
+.badge-异常 { background: #FFEBEE; color: #C62828; }
+
+.risk-detail-row { display: flex; gap: 24rpx; }
+.risk-value, .risk-level-text { font-size: 24rpx; color: #999; }
+.risk-edit-row { display: flex; align-items: center; gap: 12rpx; flex-wrap: wrap; }
+.edit-label { font-size: 26rpx; color: #666; }
+.edit-input { border: 1rpx solid #ddd; border-radius: 8rpx; padding: 8rpx 12rpx; font-size: 26rpx; width: 120rpx; }
+.edit-picker { border: 1rpx solid #ddd; border-radius: 8rpx; padding: 8rpx 12rpx; font-size: 26rpx; color: #333; }
+
+.empty-hint { text-align: center; color: #ccc; padding: 60rpx 0; font-size: 28rpx; }
+.bottom-bar { position: fixed; bottom: 0; left: 0; right: 0; display: flex; padding: 20rpx 30rpx; background: #fff; border-top: 1rpx solid #eee; gap: 20rpx; }
+.btn-cancel { flex: 1; padding: 20rpx; border: 1rpx solid #ddd; border-radius: 12rpx; text-align: center; font-size: 28rpx; color: #666; }
+.btn-save { flex: 1; padding: 20rpx; background: #4A9BD7; border-radius: 12rpx; text-align: center; font-size: 28rpx; color: #fff; }
+.bottom-spacer { height: 40rpx; }
+</style>
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+cd /sc-data/cfc
+git add cfc-frontend/pages/health/gut-flora-risks-detail.vue
+git commit -m "feat(frontend): add gut-flora-risks-detail page with knowledge popup"
+```
+
+---
+
+### Task 13: 前端 — 菌群详情页(12 Tab + 颜色箭头)
+
+**Files:**
+- Create: `cfc-frontend/pages/health/gut-flora-species-detail.vue`
+
+- [ ] **Step 1: 创建页面**
+
+```vue
+<template>
+  <view class="page">
+    <view class="nav-bar">
+      <view class="nav-back" @tap="goBack">
+        <text class="back-text">‹ 返回</text>
+      </view>
+      <text class="nav-title">菌群详情</text>
+      <text class="nav-edit" @tap="toggleEdit">{{ isEditing ? '完成' : '编辑' }}</text>
+    </view>
+
+    <!-- 12 分类 Tab -->
+    <scroll-view class="tabs" scroll-x>
+      <view class="tab-item" v-for="(tab, idx) in tabs" :key="idx"
+            :class="'tab-' + (selectedTab === tab.key ? 'active' : '')"
+            @tap="selectedTab = tab.key">
+        <text>{{ tab.label }}</text>
+      </view>
+    </scroll-view>
+
+    <scroll-view class="content" scroll-y>
+      <view class="flora-card" v-for="(item, idx) in filteredFlora" :key="idx">
+        <view class="flora-header">
+          <text class="flora-name"
+                :class="'status-' + (item.status || '正常')"
+                @tap="showKnowledge('bacteria', item.bacteriaName)">
+            {{ item.bacteriaName }}
+            <text class="arrow" v-if="item.status === '偏高' || item.status === '过多'">↑</text>
+            <text class="arrow" v-else-if="item.status === '偏低' || item.status === '缺乏' || item.status === '不足'">↓</text>
+          </text>
+          <text class="flora-level-tag" v-if="item.level && item.level !== 'GENUS'">{{ item.level }}</text>
+        </view>
+        <view class="flora-details" v-if="!isEditing">
+          <text class="detail-text">丰度 {{ item.bacteriaValue || '--' }} | 正常 {{ item.normalRange || '--' }}</text>
+          <text class="detail-text">人群 {{ item.populationLevel || '--' }} | 检出 {{ item.detectionRate || '--' }}</text>
+        </view>
+        <view class="flora-edit-row" v-if="isEditing">
+          <text class="edit-label">丰度:</text>
+          <input class="edit-input" type="text" v-model="item.bacteriaValue" />
+          <text class="edit-label">状态:</text>
+          <picker :value="floraStatusIndex(item.status)" :range="floraStatuses" @change="onFloraStatusChange($event, item)">
+            <text class="edit-picker">{{ item.status || '正常' }}</text>
+          </picker>
+        </view>
+      </view>
+      <view class="empty-hint" v-if="filteredFlora.length === 0">暂无该分类菌属数据</view>
+      <view class="bottom-spacer"></view>
+    </scroll-view>
+
+    <health-knowledge-popup :visible="knowledgeVisible" :data="knowledgeData" @close="closeKnowledge" />
+
+    <view class="bottom-bar" v-if="isEditing">
+      <view class="btn-cancel" @tap="cancelEdit">取消</view>
+      <view class="btn-save" @tap="saveEdit">保存</view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getReportDetail, editHealthReport, queryKnowledgeBase } from '../../utils/api.js'
+import HealthKnowledgePopup from '../../components/health-knowledge-popup.vue'
+
+export default {
+  components: { HealthKnowledgePopup },
+  data() {
+    return {
+      reportId: null,
+      reportData: null,
+      gutFlora: [],
+      probioticSpecies: [],
+      isEditing: false,
+      editFlora: [],
+      selectedTab: 'core',
+      floraStatuses: ['正常', '偏低', '偏高', '缺乏', '不足', '过多'],
+      knowledgeVisible: false,
+      knowledgeData: {},
+      tabs: [
+        { key: 'core', label: '核心菌属' },
+        { key: 'probiotic', label: '益生菌' },
+        { key: 'harmful', label: '有害菌' },
+        { key: 'other', label: '其它菌属' },
+        { key: 'pathogen', label: '病原菌' },
+        { key: 'obesity', label: '肥胖相关' },
+        { key: 'constipation', label: '便秘相关' },
+        { key: 'depression', label: '抑郁相关' },
+        { key: 'allergy', label: '过敏相关' },
+        { key: 'bloating', label: '腹胀相关' },
+        { key: 'insomnia', label: '失眠相关' },
+        { key: 'all', label: '全部' }
+      ]
+    }
+  },
+  computed: {
+    filteredFlora: function() {
+      var list = this.isEditing ? this.editFlora : this.gutFlora
+      if (this.selectedTab === 'all') return list
+      if (this.selectedTab === 'probiotic') {
+        // probio species from separate field
+        return this.isEditing ? this.editFlora.filter(function(i) { return i.category === 'probiotic' }) : this.probioticSpecies
+      }
+      return list.filter(function(item) {
+        return item.category === this.selectedTab
+      }.bind(this))
+    }
+  },
+  onLoad: function(options) {
+    this.reportId = options.reportId ? parseInt(options.reportId) : null
+    if (this.reportId) {
+      this.loadData()
+    }
+  },
+  methods: {
+    goBack: function() { uni.navigateBack() },
+    loadData: function() {
+      var self = this
+      getReportDetail(this.reportId).then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.reportData = res.data
+          self.gutFlora = res.data.gutFlora || []
+          self.probioticSpecies = res.data.probioticSpecies || []
+        }
+      })
+    },
+    toggleEdit: function() {
+      this.isEditing = true
+      this.editFlora = JSON.parse(JSON.stringify(this.gutFlora))
+    },
+    cancelEdit: function() {
+      this.isEditing = false
+      this.editFlora = []
+    },
+    saveEdit: function() {
+      var self = this
+      editHealthReport(self.reportId, { gutFlora: self.editFlora }, 0).then(function(res) {
+        if (res.code === 200) {
+          uni.showToast({ title: '已保存', icon: 'success' })
+          self.isEditing = false
+          self.loadData()
+        } else {
+          uni.showToast({ title: res.message || '保存失败', icon: 'none' })
+        }
+      })
+    },
+    floraStatusIndex: function(status) {
+      var idx = this.floraStatuses.indexOf(status)
+      return idx >= 0 ? idx : 0
+    },
+    onFloraStatusChange: function(e, item) {
+      item.status = this.floraStatuses[e.detail.value]
+    },
+    showKnowledge: function(type, name) {
+      var self = this
+      queryKnowledgeBase(type, name).then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.knowledgeData = res.data
+          self.knowledgeVisible = true
+        } else {
+          self.knowledgeData = { itemName: name }
+          self.knowledgeVisible = true
+        }
+      })
+    },
+    closeKnowledge: function() {
+      this.knowledgeVisible = false
+      this.knowledgeData = {}
+    }
+  }
+}
+</script>
+
+<style scoped>
+.page { min-height: 100vh; background: #f8f8f8; }
+.nav-bar { display: flex; align-items: center; padding: 20rpx 30rpx; background: #fff; position: relative; }
+.nav-back { padding: 10rpx 0; }
+.back-text { font-size: 28rpx; color: #4A9BD7; }
+.nav-title { flex: 1; text-align: center; font-size: 32rpx; font-weight: 600; }
+.nav-edit { font-size: 28rpx; color: #4A9BD7; padding: 10rpx 0; }
+
+.tabs { background: #fff; padding: 10rpx 20rpx; white-space: nowrap; border-bottom: 1rpx solid #eee; }
+.tab-item { display: inline-block; padding: 12rpx 24rpx; margin: 0 6rpx; border-radius: 30rpx; font-size: 24rpx; background: #f0f0f0; color: #666; }
+.tab-active { background: #4A9BD7; color: #fff; }
+
+.content { padding: 20rpx 30rpx; height: calc(100vh - 180rpx); }
+.flora-card { background: #fff; border-radius: 16rpx; padding: 24rpx; margin-bottom: 16rpx; }
+.flora-header { display: flex; align-items: center; margin-bottom: 10rpx; }
+.flora-name { font-size: 28rpx; font-weight: 600; color: #333; flex: 1; }
+.flora-name.status-偏高 { color: #C62828; }
+.flora-name.status-过多 { color: #C62828; }
+.flora-name.status-偏低 { color: #E65100; }
+.flora-name.status-缺乏 { color: #E65100; }
+.flora-name.status-不足 { color: #E65100; }
+.arrow { font-size: 24rpx; }
+.flora-level-tag { padding: 2rpx 12rpx; background: #E3F2FD; border-radius: 16rpx; font-size: 20rpx; color: #4A9BD7; }
+.flora-details { }
+.detail-text { font-size: 24rpx; color: #999; display: block; line-height: 1.8; }
+.flora-edit-row { display: flex; align-items: center; gap: 12rpx; flex-wrap: wrap; }
+.edit-label { font-size: 26rpx; color: #666; }
+.edit-input { border: 1rpx solid #ddd; border-radius: 8rpx; padding: 8rpx 12rpx; font-size: 26rpx; width: 140rpx; }
+.edit-picker { border: 1rpx solid #ddd; border-radius: 8rpx; padding: 8rpx 12rpx; font-size: 26rpx; color: #333; }
+
+.empty-hint { text-align: center; color: #ccc; padding: 60rpx 0; font-size: 28rpx; }
+.bottom-bar { position: fixed; bottom: 0; left: 0; right: 0; display: flex; padding: 20rpx 30rpx; background: #fff; border-top: 1rpx solid #eee; gap: 20rpx; }
+.btn-cancel { flex: 1; padding: 20rpx; border: 1rpx solid #ddd; border-radius: 12rpx; text-align: center; font-size: 28rpx; color: #666; }
+.btn-save { flex: 1; padding: 20rpx; background: #4A9BD7; border-radius: 12rpx; text-align: center; font-size: 28rpx; color: #fff; }
+.bottom-spacer { height: 40rpx; }
+</style>
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+cd /sc-data/cfc
+git add cfc-frontend/pages/health/gut-flora-species-detail.vue
+git commit -m "feat(frontend): add gut-flora-species-detail page (12 tabs + color arrows + knowledge popup)"
+```
+
+---
+
+### Task 14: 前端 — 主页 gut-flora-detail.vue 重写
+
+**Files:**
+- Modify: `cfc-frontend/pages/health/gut-flora-detail.vue`(完全重写)
+
+- [ ] **Step 1: 重写主页 — 完整长 scroll + 评分圆环 + 手风琴 + 颜色箭头 + 知识库 + 编辑模式**
+
+```vue
+<template>
+  <view class="page">
+    <view class="nav-bar">
+      <view class="nav-back" @tap="goBack">
+        <text class="back-text">‹ 返回</text>
+      </view>
+      <text class="nav-title">肠道菌群报告</text>
+    </view>
+
+    <scroll-view class="content" scroll-y>
+      <!-- 报告头部 -->
+      <view class="report-header">
+        <text class="report-title">肠道菌群检测报告</text>
+        <view class="report-meta">
+          <text class="meta-item" v-if="report.personName">{{ report.personName }}</text>
+          <text class="meta-item" v-if="report.gender">{{ report.gender }}</text>
+          <text class="meta-item" v-if="report.age">{{ report.age }}岁</text>
+        </view>
+        <view class="report-meta">
+          <text class="meta-item" v-if="report.reportDate">日期: {{ report.reportDate }}</text>
+          <text class="meta-item" v-if="report.reportNumber">编号: {{ report.reportNumber }}</text>
+        </view>
+        <view class="badge-row" v-if="report.gutAge || report.gutType">
+          <text class="badge" v-if="report.gutAge">肠龄: {{ report.gutAge }}</text>
+          <text class="badge" v-if="report.gutType">肠型: {{ report.gutType }}</text>
+        </view>
+      </view>
+
+      <!-- 评分圆环区域,横向滚动 -->
+      <scroll-view class="scores-scroll" scroll-x>
+        <view class="score-item" v-for="(score, idx) in scoreItems" :key="idx">
+          <view class="score-ring" :style="'border-color: ' + score.color + '; color: ' + score.color">
+            <text class="score-val">{{ score.value }}</text>
+          </view>
+          <text class="score-label">{{ score.label }}</text>
+        </view>
+      </scroll-view>
+
+      <!-- 快速跳转入口 -->
+      <view class="quick-nav">
+        <view class="nav-card" @tap="goToSpecies">
+          <text class="nav-icon">🦠</text>
+          <text class="nav-label">菌群详情</text>
+          <text class="nav-count">{{ floraCount }}种</text>
+        </view>
+        <view class="nav-card" @tap="goToRisks">
+          <text class="nav-icon">⚠️</text>
+          <text class="nav-label">疾病风险</text>
+          <text class="nav-count">{{ riskCount }}项</text>
+        </view>
+        <view class="nav-card" @tap="goToFoods">
+          <text class="nav-icon">🍽️</text>
+          <text class="nav-label">饮食推荐</text>
+          <text class="nav-count">{{ foodCount }}项</text>
+        </view>
+        <view class="nav-card" @tap="goToIndicators">
+          <text class="nav-icon">📋</text>
+          <text class="nav-label">全部指标</text>
+          <text class="nav-count">{{ indicatorCount }}项</text>
+        </view>
+      </view>
+
+      <!-- 肠道功能面板(手风琴) -->
+      <view class="accordion-section">
+        <view class="accordion-header" @tap="toggleAccordion('gut')">
+          <text class="accordion-title">肠道功能与代谢物</text>
+          <text class="accordion-arrow">{{ accordionOpen.gut ? '收起' : '展开' }}</text>
+        </view>
+        <view class="accordion-body" v-if="accordionOpen.gut">
+          <view class="indicator-card" v-for="(item, idx) in gutIndicators" :key="idx">
+            <view class="indicator-row">
+              <text class="indicator-name" @tap="showKnowledge(indicatorType(item), item.indicatorName)">
+                {{ item.indicatorName }}
+                <text class="arrow" v-if="item.status === '偏高' || item.status === '过多'">↑</text>
+                <text class="arrow" v-else-if="item.status === '偏低' || item.status === '缺乏' || item.status === '不足'">↓</text>
+                <text class="arrow" v-else-if="item.status === '异常'">⚠</text>
+              </text>
+              <view class="indicator-value" v-if="!isEditing">
+                <text :class="'val-text val-' + item.status">{{ item.indicatorValue || '--' }}</text>
+              </view>
+              <view class="indicator-edit" v-if="isEditing">
+                <input class="edit-input" type="text" v-model="item.indicatorValue" />
+                <picker :value="indStatusIndex(item.status)" :range="indStatuses" @change="onIndStatusChange($event, item)">
+                  <text class="edit-picker">{{ item.status || '正常' }}</text>
+                </picker>
+              </view>
+            </view>
+            <text class="indicator-range" v-if="item.refRange">参考: {{ item.refRange }}</text>
+          </view>
+        </view>
+      </view>
+
+      <!-- 营养指标面板(手风琴) -->
+      <view class="accordion-section">
+        <view class="accordion-header" @tap="toggleAccordion('nutri')">
+          <text class="accordion-title">营养指标</text>
+          <text class="accordion-arrow">{{ accordionOpen.nutri ? '收起' : '展开' }}</text>
+        </view>
+        <view class="accordion-body" v-if="accordionOpen.nutri">
+          <!-- 按分类子分组 -->
+          <view class="sub-group" v-for="(group, gIdx) in nutritionGroups" :key="gIdx">
+            <text class="sub-group-title">{{ group.category }}</text>
+            <view class="indicator-card" v-for="(item, idx) in group.items" :key="idx">
+              <view class="indicator-row">
+                <text class="indicator-name" @tap="showKnowledge(indicatorType(item), item.indicatorName)">
+                  {{ item.indicatorName }}
+                  <text class="arrow" v-if="item.status === '偏高' || item.status === '过多'">↑</text>
+                  <text class="arrow" v-else-if="item.status === '偏低' || item.status === '缺乏' || item.status === '不足'">↓</text>
+                  <text class="arrow" v-else-if="item.status === '异常'">⚠</text>
+                </text>
+                <view class="indicator-value" v-if="!isEditing">
+                  <text :class="'val-text val-' + item.status">{{ item.indicatorValue || '--' }}</text>
+                </view>
+                <view class="indicator-edit" v-if="isEditing">
+                  <input class="edit-input" type="text" v-model="item.indicatorValue" />
+                  <picker :value="indStatusIndex(item.status)" :range="indStatuses" @change="onIndStatusChange($event, item)">
+                    <text class="edit-picker">{{ item.status || '正常' }}</text>
+                  </picker>
+                </view>
+              </view>
+              <text class="indicator-range" v-if="item.refRange">参考: {{ item.refRange }}</text>
+            </view>
+          </view>
+        </view>
+      </view>
+
+      <view class="bottom-spacer"></view>
+    </scroll-view>
+
+    <!-- 知识库弹出框 -->
+    <health-knowledge-popup :visible="knowledgeVisible" :data="knowledgeData" @close="closeKnowledge" />
+
+    <!-- FAB 编辑按钮 / 底部保存栏 -->
+    <view class="fab" v-if="!isEditing" @tap="toggleEdit">
+      <text class="fab-icon">✏️</text>
+    </view>
+    <view class="bottom-bar" v-if="isEditing">
+      <view class="btn-cancel" @tap="cancelEdit">取消</view>
+      <view class="btn-save" @tap="saveEdit">保存</view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getReportDetail, editHealthReport, queryKnowledgeBase } from '../../utils/api.js'
+import HealthKnowledgePopup from '../../components/health-knowledge-popup.vue'
+
+var INDICATOR_TYPE_MAP = {
+  '维生素': 'vitamin',
+  '氨基酸': 'amino_acid',
+  '微量元素': 'indicator',
+  '肠道屏障与代谢物': 'indicator',
+  '短链脂肪酸': 'indicator',
+  '神经递质与激素': 'indicator',
+  '抗生素风险评估': 'indicator'
+}
+
+export default {
+  components: { HealthKnowledgePopup },
+  data() {
+    return {
+      reportId: null,
+      reportData: null,
+      report: {},
+      isEditing: false,
+      accordionOpen: { gut: false, nutri: false },
+      scoreItems: [],
+      allIndicators: [],
+      gutIndicators: [],
+      nutritionGroups: [],
+      floraCount: 0,
+      riskCount: 0,
+      foodCount: 0,
+      indicatorCount: 0,
+      knowledgeVisible: false,
+      knowledgeData: {},
+      indStatuses: ['正常', '偏低', '偏高', '缺乏', '不足', '过多', '异常']
+    }
+  },
+  onLoad: function(options) {
+    this.reportId = options.reportId ? parseInt(options.reportId) : null
+    if (this.reportId) {
+      this.loadData()
+    }
+  },
+  methods: {
+    goBack: function() { uni.navigateBack() },
+    loadData: function() {
+      var self = this
+      getReportDetail(this.reportId).then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.reportData = res.data
+          self.report = res.data.report || res.data
+          self.buildScores(res.data)
+          self.buildIndicators(res.data)
+          self.floraCount = (res.data.gutFlora || []).length
+          self.riskCount = (res.data.diseaseRisks || []).length
+          self.foodCount = (res.data.foods || []).length
+          self.indicatorCount = (res.data.indicators || []).length
+        }
+      })
+    },
+    buildScores: function(data) {
+      var r = data.report || data
+      var items = []
+      var scoreFields = [
+        { key: 'overallScore', label: '综合', color: '#4A9BD7' },
+        { key: 'gutHealthScore', label: '菌群健康', color: '#10B981' },
+        { key: 'chronicDiseaseScore', label: '慢病控制', color: '#F59E0B' },
+        { key: 'nutritionScore', label: '营养均衡', color: '#FF8C42' },
+        { key: 'gutBalanceScore', label: '平衡', color: '#6366F1' },
+        { key: 'gutDiversityScore', label: '多样性', color: '#FF6B9D' },
+        { key: 'beneficialBacteriaScore', label: '有益菌', color: '#10B981' },
+        { key: 'harmfulBacteriaScore', label: '有害菌', color: '#C62828' },
+        { key: 'coreSpeciesScore', label: '核心菌属', color: '#4A9BD7' }
+      ]
+      for (var i = 0; i < scoreFields.length; i++) {
+        var f = scoreFields[i]
+        if (r[f.key] != null) {
+          items.push({ value: r[f.key], label: f.label, color: f.color })
+        }
+      }
+      this.scoreItems = items
+    },
+    buildIndicators: function(data) {
+      var indicators = data.indicators || []
+      this.allIndicators = indicators
+
+      // 肠道类
+      var gutCategories = ['肠道屏障与代谢物', '短链脂肪酸', '神经递质与激素', '抗生素风险评估']
+      this.gutIndicators = indicators.filter(function(item) {
+        return gutCategories.indexOf(item.category) >= 0
+      })
+
+      // 营养类
+      var nutriCategories = ['营养', '主要营养', '氨基酸', '维生素', '微量元素']
+      var groups = []
+      var seen = {}
+      for (var i = 0; i < indicators.length; i++) {
+        var item = indicators[i]
+        if (gutCategories.indexOf(item.category) >= 0) continue
+        if (!seen[item.category]) {
+          seen[item.category] = true
+          groups.push({ category: item.category, items: [] })
+        }
+        for (var g = 0; g < groups.length; g++) {
+          if (groups[g].category === item.category) {
+            groups[g].items.push(item)
+            break
+          }
+        }
+      }
+      this.nutritionGroups = groups
+    },
+    toggleAccordion: function(key) {
+      this.accordionOpen[key] = !this.accordionOpen[key]
+    },
+    indicatorType: function(item) {
+      return INDICATOR_TYPE_MAP[item.category] || 'nutrient'
+    },
+    indStatusIndex: function(status) {
+      var idx = this.indStatuses.indexOf(status)
+      return idx >= 0 ? idx : 0
+    },
+    onIndStatusChange: function(e, item) {
+      item.status = this.indStatuses[e.detail.value]
+    },
+    showKnowledge: function(type, name) {
+      var self = this
+      queryKnowledgeBase(type, name).then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.knowledgeData = res.data
+          self.knowledgeVisible = true
+        } else {
+          self.knowledgeData = { itemName: name }
+          self.knowledgeVisible = true
+        }
+      })
+    },
+    closeKnowledge: function() {
+      this.knowledgeVisible = false
+      this.knowledgeData = {}
+    },
+    toggleEdit: function() {
+      this.isEditing = true
+    },
+    cancelEdit: function() {
+      this.isEditing = false
+      this.loadData()
+    },
+    saveEdit: function() {
+      var self = this
+      var payload = {
+        overallScore: this.report.overallScore,
+        gutHealthScore: this.report.gutHealthScore,
+        nutritionScore: this.report.nutritionScore,
+        indicators: this.allIndicators
+      }
+      editHealthReport(self.reportId, payload, 0).then(function(res) {
+        if (res.code === 200) {
+          uni.showToast({ title: '已保存', icon: 'success' })
+          self.isEditing = false
+          self.loadData()
+        } else {
+          uni.showToast({ title: res.message || '保存失败', icon: 'none' })
+        }
+      })
+    },
+    goToSpecies: function() {
+      uni.navigateTo({ url: '/pages/health/gut-flora-species-detail?reportId=' + this.reportId })
+    },
+    goToRisks: function() {
+      uni.navigateTo({ url: '/pages/health/gut-flora-risks-detail?reportId=' + this.reportId })
+    },
+    goToFoods: function() {
+      uni.navigateTo({ url: '/pages/health/gut-flora-foods-detail?reportId=' + this.reportId })
+    },
+    goToIndicators: function() {
+      // 滚动到指标区域,而非跳转页面
+      this.accordionOpen.nutri = true
+    }
+  }
+}
+</script>
+
+<style scoped>
+.page { min-height: 100vh; background: #f8f8f8; }
+.nav-bar { display: flex; align-items: center; padding: 20rpx 30rpx; background: #fff; position: relative; }
+.nav-back { padding: 10rpx 0; }
+.back-text { font-size: 28rpx; color: #4A9BD7; }
+.nav-title { flex: 1; text-align: center; font-size: 32rpx; font-weight: 600; }
+
+.content { height: calc(100vh - 100rpx); }
+
+/* 报告头部 */
+.report-header { background: linear-gradient(135deg, #4A9BD7, #6366F1); padding: 40rpx 30rpx; color: #fff; }
+.report-title { font-size: 36rpx; font-weight: 700; display: block; margin-bottom: 16rpx; }
+.report-meta { display: flex; gap: 20rpx; margin-bottom: 6rpx; }
+.meta-item { font-size: 24rpx; opacity: 0.9; }
+.badge-row { display: flex; gap: 16rpx; margin-top: 12rpx; }
+.badge { padding: 6rpx 20rpx; background: rgba(255,255,255,0.2); border-radius: 20rpx; font-size: 22rpx; }
+
+/* 评分圆环 */
+.scores-scroll { padding: 20rpx 0; white-space: nowrap; background: #fff; }
+.score-item { display: inline-flex; flex-direction: column; align-items: center; margin: 0 20rpx; }
+.score-ring { width: 100rpx; height: 100rpx; border-radius: 50%; border: 6rpx solid; display: flex; align-items: center; justify-content: center; font-size: 32rpx; font-weight: 700; margin-bottom: 8rpx; }
+.score-label { font-size: 22rpx; color: #666; }
+
+/* 快速导航 */
+.quick-nav { display: flex; padding: 20rpx; background: #fff; margin-bottom: 16rpx; }
+.nav-card { flex: 1; display: flex; flex-direction: column; align-items: center; padding: 16rpx 0; }
+.nav-icon { font-size: 40rpx; margin-bottom: 6rpx; }
+.nav-label { font-size: 24rpx; color: #333; }
+.nav-count { font-size: 20rpx; color: #999; }
+
+/* 手风琴 */
+.accordion-section { background: #fff; margin-bottom: 16rpx; }
+.accordion-header { display: flex; justify-content: space-between; padding: 30rpx; border-bottom: 1rpx solid #f0f0f0; }
+.accordion-title { font-size: 30rpx; font-weight: 600; color: #333; }
+.accordion-arrow { font-size: 24rpx; color: #999; }
+.accordion-body { padding: 10rpx 30rpx 20rpx; }
+
+.sub-group { margin-bottom: 16rpx; }
+.sub-group-title { font-size: 26rpx; color: #4A9BD7; font-weight: 600; display: block; margin: 12rpx 0 8rpx; }
+
+.indicator-card { padding: 16rpx 0; border-bottom: 1rpx solid #f5f5f5; }
+.indicator-row { display: flex; align-items: center; justify-content: space-between; }
+.indicator-name { font-size: 28rpx; color: #333; flex: 1; }
+.indicator-name .arrow { font-size: 22rpx; }
+
+.val-text { font-size: 28rpx; font-weight: 600; color: #333; }
+.val-偏高 { color: #C62828; }
+.val-过多 { color: #C62828; }
+.val-偏低 { color: #E65100; }
+.val-缺乏 { color: #E65100; }
+.val-不足 { color: #E65100; }
+.val-异常 { color: #C62828; }
+.val-注意 { color: #F59E0B; }
+
+.indicator-range { font-size: 22rpx; color: #bbb; display: block; margin-top: 4rpx; }
+
+.indicator-edit { display: flex; gap: 8rpx; align-items: center; }
+.edit-input { border: 1rpx solid #ddd; border-radius: 8rpx; padding: 6rpx 10rpx; font-size: 26rpx; width: 100rpx; }
+.edit-picker { border: 1rpx solid #ddd; border-radius: 8rpx; padding: 6rpx 10rpx; font-size: 24rpx; color: #333; }
+
+/* FAB */
+.fab { position: fixed; right: 30rpx; bottom: 40rpx; width: 100rpx; height: 100rpx; background: #4A9BD7; border-radius: 50%; display: flex; align-items: center; justify-content: center; box-shadow: 0 4rpx 16rpx rgba(74,155,215,0.4); }
+.fab-icon { font-size: 40rpx; color: #fff; }
+
+/* 底部编辑栏 */
+.bottom-bar { position: fixed; bottom: 0; left: 0; right: 0; display: flex; padding: 20rpx 30rpx; background: #fff; border-top: 1rpx solid #eee; gap: 20rpx; z-index: 100; }
+.btn-cancel { flex: 1; padding: 20rpx; border: 1rpx solid #ddd; border-radius: 12rpx; text-align: center; font-size: 28rpx; color: #666; }
+.btn-save { flex: 1; padding: 20rpx; background: #4A9BD7; border-radius: 12rpx; text-align: center; font-size: 28rpx; color: #fff; }
+.bottom-spacer { height: 40rpx; }
+</style>
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+cd /sc-data/cfc
+git add cfc-frontend/pages/health/gut-flora-detail.vue
+git commit -m "feat(frontend): rewrite gut-flora-detail with full data, accordion, colors, knowledge popup, edit mode"
+```
+
+---
+
+### Task 15: E2E 测试适配
+
+**Files:**
+- Modify: `tests/e2e/flora-microbiome-flow.spec.js`
+
+- [ ] **Step 1: 检查并更新现有 E2E 测试**
+
+读取现有测试文件,确认 `.kb-link`、`.kb-popup-overlay`、`.flora-name` 等新选择器是否涵盖。若无,添加新断言并在关键操作步骤增加超时。
+
+关键检查项:
+- 报告头部显示(`.report-title`, `.badge`)
+- 评分圆环存在(`.score-item` 数量 >= 3)
+- 快速导航入口可见(`.nav-card` 数量 = 4)
+- 手风琴可折叠(`.accordion-header`点击后 `.accordion-body` 可见)
+- 知识库弹出框触发(`.indicator-name` 点击后 `.kb-popup-overlay` 可见)
+- 颜色状态类名(`.val-偏高`, `.status-偏低` 等)
+- FAB 编辑按钮(`.fab`)
+- 导航到详情页路径正确
+
+为 Playwright 配置 viewport: `{ width: 375, height: 667 }`(iPhone 6/7/8 尺寸)
+
+- [ ] **Step 2: Commit**
+
+```bash
+cd /sc-data/cfc
+git add tests/e2e/flora-microbiome-flow.spec.js
+git commit -m "test(e2e): update flora-microbiome-flow tests for restructured pages"
+```
+
+---
+
+### Task 16: 最终验证
+
+- [ ] **Step 1: 后端编译**
+
+Run: `cd /sc-data/cfc/cfc-backend && mvn clean compile`
+Expected: BUILD SUCCESS
+
+- [ ] **Step 2: 检查小程序项目完整注册**
+
+Run: `cd /sc-data/cfc && grep -c 'gut-flora' cfc-frontend/pages.json`
+Expected: 4(gut-flora-detail, gut-flora-species-detail, gut-flora-foods-detail, gut-flora-risks-detail)
+
+- [ ] **Step 3: 检查所有新文件**
+
+Run: `cd /sc-data/cfc && git ls-files -- 'cfc-backend/' 'cfc-frontend/' | grep -E '(KnowledgeBase|gut-flora|knowledge-popup)'`
+Expected: 所有新文件已跟踪
+
+- [ ] **Step 4: 最终 push**
+
+```bash
+cd /sc-data/cfc
+git push origin cfclub
+```
+
+---
+
+## Self-Review Checklist
+
+### 1. 规范覆盖度
+
+| 规范需求 | 对应 Task |
+|---------|-----------|
+| 知识库独立表 health_knowledge_base | Task 1 (Entity) + Task 5 (Migration) |
+| 知识库查询 API(单条+批量) | Task 3 (Service) + Task 4 (Controller) |
+| 报告编辑 API | Task 6 |
+| 仅展示个性化数据(无内嵌说明) | Task 12-14(所有卡片不显示 description) |
+| 菌名/指标名可点击弹出知识库 | Task 12-14(`@tap="showKnowledge"`) |
+| 颜色+箭头表示异常值 | Task 13-14(`:class="'status-' + item.status"` + `<text class="arrow">`) |
+| 主页手风琴 | Task 14(`toggleAccordion`) |
+| 4 页架构 | Task 10 (pages.json) + Task 11-14 |
+| 12 分类 Tab + 卡片 | Task 13 |
+| 编辑模式切换 | Task 11-14(`toggleEdit`/`cancelEdit`/`saveEdit`) |
+| 数据库迁移 | Task 5(迁移93) |
+| schema.sql 同步 | Task 5 |
+| 异常值颜色: 偏高=红↑, 偏低=琥珀↓ | Task 13-14 |
+| E2E 测试 | Task 15 |
+
+### 2. 占位符扫描
+- 所有代码块包含完整实现,无 `TBD`/`TODO`
+- 所有文件路径使用绝对(相对于项目根)
+- 所有 API 方法签名在前后端间一致
+
+### 3. 类型一致性
+- `HealthKnowledgeBase` 的字段名与数据库列名一致(`itemType` ↔ `item_type` via MyBatis-Plus 自动映射)
+- Controller 方法参数名与前端 `api.js` 调用参数一致
+- `batchQuery` 的 Map key 格式 `"itemType:itemName"` 在前端 `/utils/api.js` 和 Service 中一致
+
+---
+
+## Execution Handoff
+
+**Plan complete and saved to `docs/superpowers/plans/2026-07-17-flora-microbiome-report-implementation.md`.**
+
+Two execution options:
+
+1. **Subagent-Driven (recommended)** — I dispatch a fresh subagent per task, review between tasks, fast iteration
+2. **Inline Execution** — Execute tasks in this session with checkpoints
+
+Which approach?