Ver Fonte

feat(health-report): 报告详情页统一重构为完整页面,修复上传后跳转 routeDone 报错

- report-detail.vue 重建模板/脚本/样式(查看模式+成员面板+帮助弹窗+舌象/益生菌/分类/病原体区块+模式感知操作按钮)
- report-confirm.vue 去除 onLoad 重复的 draftId/memberId 赋值,draftId 模式自动跳转详情页
- report-upload.vue 上传草稿后跳转详情页
- 新增健康报告编辑日志(HealthReportEditLog + Mapper + 建表迁移)
- 报告服务/区块组装/排序工具相关后端增强
liaoxg há 1 mês atrás
pai
commit
7a86829902

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

@@ -4076,6 +4076,34 @@ try {
         ensureColumn("products", "distribution_system_id", "BIGINT COMMENT '销售体系ID'");
 
         // 迁移33: 创建供应商销售体系表 + product_orders扩展字段
+        // 迁移222: health_reports 添加 shannon_index, payload_json 列并创建 health_report_edit_logs 表
+        try {
+            jdbcTemplate.execute("ALTER TABLE health_reports ADD COLUMN shannon_index VARCHAR(32) DEFAULT NULL COMMENT '香农指数'");
+            log.info("已添加shannon_index列到health_reports表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+        try {
+            jdbcTemplate.execute("ALTER TABLE health_reports ADD COLUMN payload_json LONGTEXT COMMENT '报告完整载荷快照'");
+            log.info("已添加payload_json列到health_reports表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS health_report_edit_logs (" +
+                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                "report_id BIGINT NOT NULL, " +
+                "user_id BIGINT, " +
+                "report_type VARCHAR(32), " +
+                "action VARCHAR(20) COMMENT 'create/edit', " +
+                "before_payload LONGTEXT, " +
+                "after_payload LONGTEXT, " +
+                "created_at DATETIME DEFAULT CURRENT_TIMESTAMP" +
+                ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康报告修改留痕'");
+            log.info("已创建health_report_edit_logs表");
+        } catch (Exception e) {
+            // 表已存在,忽略错误
+        }
         ensureColumn("product_orders", "supply_system_id", "BIGINT COMMENT '所属供应商体系ID'");
         ensureColumn("product_orders", "supplier_id", "BIGINT COMMENT '收款供应商用户ID'");
         ensureColumn("product_orders", "supply_commission", "INT COMMENT '销售佣金(分)'");

+ 13 - 3
cfc-backend/src/main/java/com/etotem/cfc/controller/HealthReportController.java

@@ -372,7 +372,7 @@ public class HealthReportController {
             return Result.error("payload不能为空");
         }
 
-        healthReportService.updateReportFromPayload(reportId, payload, subjectId);
+        healthReportService.updateReportFromPayload(reportId, payload, subjectId, userId);
         return Result.success("已保存");
     }
 
@@ -424,6 +424,9 @@ public class HealthReportController {
 
             HealthReport created = healthReportService.createReport(report, indicators, gutFlora, diseaseRisks);
 
+            // 落库 payload 快照 + 创建日志
+            healthReportService.savePayloadSnapshot(created.getId(), userId, "create", null, payloadJson);
+
             if (payload.getFoods() != null && !payload.getFoods().isEmpty()) {
                 List<Food> foods = buildFoodsFromPayload(payload);
                 foodService.saveFoodsForReport(created.getId(), foods);
@@ -892,11 +895,13 @@ public class HealthReportController {
         try {
             ParsedReportPayload.Payload payload;
             Object payloadObj = params.get("payload");
+            String payloadJson;
             if (payloadObj != null) {
-                String payloadJson = objectMapper.writeValueAsString(payloadObj);
+                payloadJson = objectMapper.writeValueAsString(payloadObj);
                 payload = objectMapper.readValue(payloadJson, ParsedReportPayload.Payload.class);
             } else {
-                payload = objectMapper.readValue(draft.getPayloadJson(), ParsedReportPayload.Payload.class);
+                payloadJson = draft.getPayloadJson();
+                payload = objectMapper.readValue(payloadJson, ParsedReportPayload.Payload.class);
             }
 
             Long subjectId = params.get("subjectId") != null
@@ -912,6 +917,9 @@ public class HealthReportController {
 
             HealthReport created = healthReportService.createReport(report, indicators, gutFlora, diseaseRisks);
 
+            // 落库 payload 快照 + 创建日志
+            healthReportService.savePayloadSnapshot(created.getId(), userId, "create", null, payloadJson);
+
             if (payload.getFoods() != null && !payload.getFoods().isEmpty()) {
                 List<Food> foods = buildFoodsFromPayload(payload);
                 foodService.saveFoodsForReport(created.getId(), foods);
@@ -1529,6 +1537,7 @@ public class HealthReportController {
             summary.setCoreGenusScore(toInteger(overview.get("coreGenusScore")));
             summary.setGutAge(objectToString(overview.get("gutAge")));
             summary.setGutType(objectToString(overview.get("gutType")));
+            summary.setShannonIndex(objectToString(overview.get("shannonIndex")));
             summary.setReportNumber(objectToString(overview.get("report_number")));
             summary.setBirthday(objectToString(overview.get("birthday")));
             payload.setSummary(summary);
@@ -1604,6 +1613,7 @@ public class HealthReportController {
             report.setCoreGenusScore(s.getCoreGenusScore());
             report.setGutAge(s.getGutAge());
             report.setGutType(s.getGutType());
+            report.setShannonIndex(s.getShannonIndex());
             report.setReportNumber(s.getReportNumber());
             report.setPersonName(s.getPersonName());
 

+ 3 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/ParsedReportPayload.java

@@ -21,6 +21,7 @@ public class ParsedReportPayload {
         private Integer coreGenusScore;
         private String gutAge;
         private String gutType;
+    private String shannonIndex;
         private String reportNumber;
         private String reportDate;
         private String personName;
@@ -50,6 +51,8 @@ public class ParsedReportPayload {
         public void setGutAge(String gutAge) { this.gutAge = gutAge; }
         public String getGutType() { return gutType; }
         public void setGutType(String gutType) { this.gutType = gutType; }
+    public String getShannonIndex() { return shannonIndex; }
+    public void setShannonIndex(String shannonIndex) { this.shannonIndex = shannonIndex; }
         public String getReportNumber() { return reportNumber; }
         public void setReportNumber(String reportNumber) { this.reportNumber = reportNumber; }
         public String getReportDate() { return reportDate; }

+ 7 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/HealthReport.java

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
 import lombok.Data;
+import com.baomidou.mybatisplus.annotation.TableField;
 
 import java.io.Serializable;
 import java.util.Date;
@@ -109,6 +110,12 @@ public class HealthReport implements Serializable {
     /** AI解读结果(JSON: {analysis, recommendations, traceId}) */
     private String aiAnalysis;
 
+    @TableField("shannon_index")
+    private String shannonIndex;
+
+    @TableField("payload_json")
+    private String payloadJson;
+
     private Date createdAt;
 
     private Date updatedAt;

+ 41 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/HealthReportEditLog.java

@@ -0,0 +1,41 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.baomidou.mybatisplus.annotation.TableField;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 健康报告编辑日志表
+ */
+@Data
+@TableName("health_report_edit_logs")
+public class HealthReportEditLog implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    @TableField("report_id")
+    private Long reportId;
+
+    @TableField("user_id")
+    private Long userId;
+
+    @TableField("report_type")
+    private String reportType;
+
+    private String action; // create / edit
+
+    @TableField("before_payload")
+    private String beforePayload;
+
+    @TableField("after_payload")
+    private String afterPayload;
+
+    @TableField("created_at")
+    private Date createdAt;
+}

+ 9 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/HealthReportEditLogMapper.java

@@ -0,0 +1,9 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.HealthReportEditLog;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface HealthReportEditLogMapper extends BaseMapper<HealthReportEditLog> {
+}

+ 107 - 33
cfc-backend/src/main/java/com/etotem/cfc/service/HealthReportService.java

@@ -10,6 +10,7 @@ import com.etotem.cfc.mapper.HealthDiseaseRiskMapper;
 import com.etotem.cfc.mapper.HealthGutFloraMapper;
 import com.etotem.cfc.mapper.HealthIndicatorMapper;
 import com.etotem.cfc.mapper.HealthReportDraftMapper;
+import com.etotem.cfc.mapper.HealthReportEditLogMapper;
 import com.etotem.cfc.mapper.HealthReportMapper;
 import com.etotem.cfc.mapper.HealthReportPermissionMapper;
 import com.etotem.cfc.mapper.UserMapper;
@@ -62,6 +63,9 @@ public class HealthReportService {
     @Resource
     private HealthReportDraftMapper draftMapper;
 
+    @Resource
+    private HealthReportEditLogMapper healthReportEditLogMapper;
+
     @Resource
     private ReportSummaryService reportSummaryService;
 
@@ -346,6 +350,15 @@ public class HealthReportService {
 
             // 通用展示块(blocks 为空时前端回退旧字段)
             detail.put("blocks", reportBlockService.getBlocks(report.getReportType(), reportId));
+
+            // 编辑回填: payload_json 快照 → 嵌套 payload 结构
+            if (report.getPayloadJson() != null && !report.getPayloadJson().isEmpty()) {
+                try {
+                    detail.put("payload", objectMapper.readValue(report.getPayloadJson(), ParsedReportPayload.Payload.class));
+                } catch (Exception e) {
+                    log.warn("解析payload_json失败 reportId={}: {}", reportId, e.getMessage());
+                }
+            }
         }
 
         return detail;
@@ -1155,42 +1168,48 @@ public class HealthReportService {
     /**
      * 更新已发布报告的编辑数据
      * 逻辑: 只更新已有字段的值,不增删条目(全量替换)
+     * payload 支持嵌套结构 {summary:{...}, indicators:[], gutFlora:[], diseaseRisks:[]},
+     * 也兼容旧版平铺结构 {overallScore:..., indicators:[], ...}
      */
     @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) {
+    public void updateReportFromPayload(Long reportId, Map<String, Object> payload, Long subjectId, Long userId) {
+        // 0. 编辑前快照(用于编辑日志)
+        HealthReport oldReport = healthReportMapper.selectById(reportId);
+        if (oldReport == 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);
+        String beforePayload = oldReport.getPayloadJson();
+
+        // 取评分子段: 嵌套 {summary:{...}} 或平铺
+        @SuppressWarnings("unchecked")
+        Map<String, Object> summary = (payload.get("summary") instanceof Map)
+                ? (Map<String, Object>) payload.get("summary") : null;
+        Map<String, Object> scoreSource = summary != null ? summary : payload;
+
+        // 1. 更新 health_report 评分数值
+        Integer overallScore = toInteger(scoreSource.get("overallScore"));
+        if (overallScore != null) oldReport.setOverallScore(overallScore);
+        Integer gutHealthScore = toInteger(scoreSource.get("gutHealthScore"));
+        if (gutHealthScore != null) oldReport.setGutHealthScore(gutHealthScore);
+        Integer chronicDiseaseScore = toInteger(scoreSource.get("chronicDiseaseScore"));
+        if (chronicDiseaseScore != null) oldReport.setChronicDiseaseScore(chronicDiseaseScore);
+        Integer nutritionScore = toInteger(scoreSource.get("nutritionScore"));
+        if (nutritionScore != null) oldReport.setNutritionScore(nutritionScore);
+        Integer gutBalanceScore = toInteger(scoreSource.get("gutBalanceScore"));
+        if (gutBalanceScore != null) oldReport.setGutBalanceScore(gutBalanceScore);
+        Integer gutDiversityScore = toInteger(scoreSource.get("gutDiversityScore"));
+        if (gutDiversityScore != null) oldReport.setGutDiversityScore(gutDiversityScore);
+        Integer beneficialBacteriaScore = toInteger(scoreSource.get("beneficialBacteriaScore"));
+        if (beneficialBacteriaScore != null) oldReport.setBeneficialBacteriaScore(beneficialBacteriaScore);
+        Integer harmfulBacteriaScore = toInteger(scoreSource.get("harmfulBacteriaScore"));
+        if (harmfulBacteriaScore != null) oldReport.setHarmfulBacteriaScore(harmfulBacteriaScore);
+        Integer coreSpeciesScore = toInteger(scoreSource.get("coreSpeciesScore"));
+        if (coreSpeciesScore != null) oldReport.setCoreSpeciesScore(coreSpeciesScore);
+        if (scoreSource.get("shannonIndex") != null) {
+            oldReport.setShannonIndex(scoreSource.get("shannonIndex").toString());
+        }
+        oldReport.setUpdatedAt(new Date());
+        healthReportMapper.updateById(oldReport);
 
         // 2. 更新 indicators(删除旧→插入新)
         Object indicatorsRaw = payload.get("indicators");
@@ -1267,7 +1286,27 @@ public class HealthReportService {
             log.warn("刷新7维健康评分失败: {}", e.getMessage());
         }
 
-        // 编辑后重新组装展示块(第一套键 Map → 标准 Payload → blocks)
+        // 6. 保存 payload 快照 + 编辑日志
+        try {
+            String afterPayload = objectMapper.writeValueAsString(payload);
+            oldReport.setPayloadJson(afterPayload);
+            oldReport.setUpdatedAt(new Date());
+            healthReportMapper.updateById(oldReport);
+
+            HealthReportEditLog editLog = new HealthReportEditLog();
+            editLog.setReportId(reportId);
+            editLog.setUserId(userId);
+            editLog.setReportType("gut_flora");
+            editLog.setAction("edit");
+            editLog.setBeforePayload(beforePayload);
+            editLog.setAfterPayload(afterPayload);
+            editLog.setCreatedAt(new Date());
+            healthReportEditLogMapper.insert(editLog);
+        } catch (Exception e) {
+            log.warn("保存payload快照/编辑日志失败 reportId={}: {}", reportId, e.getMessage());
+        }
+
+        // 7. 编辑后重新组装展示块(第一套键 Map → 标准 Payload → blocks)
         try {
             ParsedReportPayload.Payload editedPayload = reportBlockAssembler.fromMap(payload);
             reportBlockService.save("gut_flora", reportId,
@@ -1277,6 +1316,41 @@ public class HealthReportService {
         }
     }
 
+    private Integer toInteger(Object value) {
+        if (value == null) return null;
+        try {
+            return Integer.valueOf(value.toString());
+        } catch (NumberFormatException e) {
+            return null;
+        }
+    }
+
+    /**
+     * 保存 payload 快照并记录编辑日志(action: create/edit)
+     */
+    public void savePayloadSnapshot(Long reportId, Long userId, String action,
+                                    String beforePayload, String afterPayload) {
+        try {
+            HealthReport report = healthReportMapper.selectById(reportId);
+            if (report != null) {
+                report.setPayloadJson(afterPayload);
+                report.setUpdatedAt(new Date());
+                healthReportMapper.updateById(report);
+            }
+            HealthReportEditLog editLog = new HealthReportEditLog();
+            editLog.setReportId(reportId);
+            editLog.setUserId(userId);
+            editLog.setReportType("gut_flora");
+            editLog.setAction(action);
+            editLog.setBeforePayload(beforePayload);
+            editLog.setAfterPayload(afterPayload);
+            editLog.setCreatedAt(new Date());
+            healthReportEditLogMapper.insert(editLog);
+        } catch (Exception e) {
+            log.warn("保存payload快照/编辑日志失败 reportId={}: {}", reportId, e.getMessage());
+        }
+    }
+
     /**
      * 设置代上传人信息
      */

+ 35 - 19
cfc-backend/src/main/java/com/etotem/cfc/service/ReportBlockAssembler.java

@@ -23,22 +23,28 @@ public class ReportBlockAssembler {
         return level != null && IMPORTANT_LEVELS.contains(level);
     }
 
-    /** 编辑链路:第一套键 Map → 标准 Payload(第二套键) */
+    /** 编辑链路:第一套键 Map → 标准 Payload(第二套键)
+     *  支持嵌套结构 {summary:{...}, diseaseRisks:[...]} 与旧版平铺结构 */
     public ParsedReportPayload.Payload fromMap(Map<String, Object> map) {
         ParsedReportPayload.Payload p = new ParsedReportPayload.Payload();
         if (map == null || map.isEmpty()) return p;
+        @SuppressWarnings("unchecked")
+        Map<String, Object> summaryMap = (map.get("summary") instanceof Map)
+                ? (Map<String, Object>) map.get("summary") : null;
+        Map<String, Object> src = summaryMap != null ? summaryMap : map;
         ParsedReportPayload.Summary s = new ParsedReportPayload.Summary();
-        s.setOverallScore(toInt(map.get("overallScore")));
-        s.setGutHealthScore(toInt(map.get("gutHealthScore")));
-        s.setChronicDiseaseScore(toInt(map.get("chronicDiseaseScore")));
-        s.setNutritionScore(toInt(map.get("nutritionScore")));
-        s.setBalanceScore(toInt(map.get("gutBalanceScore")));
-        s.setDiversityScore(toInt(map.get("gutDiversityScore")));
-        s.setBeneficialScore(toInt(map.get("beneficialBacteriaScore")));
-        s.setHarmfulScore(toInt(map.get("harmfulBacteriaScore")));
-        s.setCoreGenusScore(toInt(map.get("coreSpeciesScore")));
-        s.setGutAge(str(map.get("gutAge")));
-        s.setGutType(str(map.get("gutType")));
+        s.setOverallScore(toInt(src.get("overallScore")));
+        s.setGutHealthScore(toInt(src.get("gutHealthScore")));
+        s.setChronicDiseaseScore(toInt(src.get("chronicDiseaseScore")));
+        s.setNutritionScore(toInt(src.get("nutritionScore")));
+        s.setBalanceScore(toInt(src.get("gutBalanceScore")));
+        s.setDiversityScore(toInt(src.get("gutDiversityScore")));
+        s.setBeneficialScore(toInt(src.get("beneficialBacteriaScore")));
+        s.setHarmfulScore(toInt(src.get("harmfulBacteriaScore")));
+        s.setCoreGenusScore(toInt(src.get("coreSpeciesScore")));
+        s.setGutAge(str(src.get("gutAge")));
+        s.setGutType(str(src.get("gutType")));
+        s.setShannonIndex(str(src.get("shannonIndex")));
         p.setSummary(s);
         // 编辑时携带 diseaseRisks(使风险编辑后重新组装能保留风险块)
         Object risksRaw = map.get("diseaseRisks");
@@ -62,19 +68,19 @@ public class ReportBlockAssembler {
         List<Map<String, Object>> blocks = new ArrayList<>();
         if (payload == null) return blocks;
 
-        // 1. score 块:9 项评分(null 跳过)
+        // 1. score 块:3 主项 + 6 子项(null 跳过)
         ParsedReportPayload.Summary s = payload.getSummary();
         if (s != null) {
             List<Map<String, Object>> items = new ArrayList<>();
             addScore(items, "综合", s.getOverallScore());
             addScore(items, "菌群健康", s.getGutHealthScore());
             addScore(items, "慢病控制", s.getChronicDiseaseScore());
-            addScore(items, "营养均衡", s.getNutritionScore());
-            addScore(items, "平衡", s.getBalanceScore());
-            addScore(items, "多样性", s.getDiversityScore());
-            addScore(items, "有益菌", s.getBeneficialScore());
-            addScore(items, "有害菌", s.getHarmfulScore());
-            addScore(items, "核心菌属", s.getCoreGenusScore());
+            addSubScore(items, "综合", "营养均衡", s.getNutritionScore());
+            addSubScore(items, "菌群健康", "平衡", s.getBalanceScore());
+            addSubScore(items, "菌群健康", "多样性", s.getDiversityScore());
+            addSubScore(items, "菌群健康", "有益菌", s.getBeneficialScore());
+            addSubScore(items, "菌群健康", "有害菌", s.getHarmfulScore());
+            addSubScore(items, "菌群健康", "核心菌属", s.getCoreGenusScore());
             if (!items.isEmpty()) {
                 Map<String, Object> block = new LinkedHashMap<>();
                 block.put("type", "score");
@@ -192,6 +198,16 @@ public class ReportBlockAssembler {
         items.add(m);
     }
 
+    private void addSubScore(List<Map<String, Object>> items, String parentLabel, String label, Integer value) {
+        if (value == null) return;
+        Map<String, Object> m = new LinkedHashMap<>();
+        m.put("parentLabel", parentLabel);
+        m.put("label", label);
+        m.put("value", value);
+        m.put("max", 100);
+        items.add(m);
+    }
+
     private Map<String, Object> col(String key, String label) {
         Map<String, Object> c = new LinkedHashMap<>();
         c.put("key", key);

+ 91 - 10
cfc-backend/src/main/java/com/etotem/cfc/util/SortUtil.java

@@ -2,16 +2,59 @@ package com.etotem.cfc.util;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import org.springframework.util.StringUtils;
 
 import java.util.List;
 import java.util.Map;
 
+/**
+ * 通用排序工具:将前端 sort 参数注入 MyBatis-Plus 查询包装器。
+ * 支持 LambdaQueryWrapper、QueryWrapper 和 LambdaUpdateWrapper。
+ */
 public class SortUtil {
 
-    @SuppressWarnings("unchecked")
+    /**
+     * 对 QueryWrapper 应用排序(从 SortContext 读取排序条件)。
+     * 无排序上下文时为空操作(保留原有排序)。
+     *
+     * @param wrapper QueryWrapper 实例
+     */
     public static <T> void applySort(QueryWrapper<T> wrapper) {
-        List<Map<String, String>> sortSpecs = SortContext.get();
+        applySort(wrapper, SortContext.get(), null);
+    }
+
+    /**
+     * 对 LambdaQueryWrapper 应用排序(从 SortContext 读取排序条件)。
+     * 无排序上下文时为空操作(保留原有排序)。
+     *
+     * @param wrapper LambdaQueryWrapper 实例
+     */
+    public static <T> void applySort(LambdaQueryWrapper<T> wrapper) {
+        applySort(wrapper, SortContext.get(), null);
+    }
+
+    /**
+     * 对 LambdaUpdateWrapper 应用排序(从 SortContext 读取排序条件)。
+     * 无排序上下文时为空操作(保留原有排序)。
+     *
+     * @param wrapper LambdaUpdateWrapper 实例
+     */
+    public static <T> void applySort(LambdaUpdateWrapper<T> wrapper) {
+        applySort(wrapper, SortContext.get(), null);
+    }
+
+    /**
+     * 对 QueryWrapper 应用排序。如果 sortSpecs 为空则不修改排序(保留原有排序)。
+     *
+     * @param wrapper    QueryWrapper 实例
+     * @param sortSpecs  前端传入的排序数组,每项 {column, order}
+     * @param snakeMap   驼峰属性名 → 数据库列名 映射(可为 null)
+     */
+    @SuppressWarnings("unchecked")
+    public static <T> void applySort(QueryWrapper<T> wrapper,
+                                     List<Map<String, String>> sortSpecs,
+                                     Map<String, String> snakeMap) {
         if (sortSpecs == null || sortSpecs.isEmpty() || wrapper == null) return;
         StringBuilder sb = new StringBuilder(" order by ");
         for (int i = 0; i < sortSpecs.size(); i++) {
@@ -20,13 +63,22 @@ public class SortUtil {
             String col = spec.get("column");
             if (!StringUtils.hasText(col)) continue;
             boolean asc = !"desc".equalsIgnoreCase(spec.get("order"));
-            sb.append(toSnake(col)).append(asc ? " asc" : " desc");
+            String dbCol = resolveDbColumn(col, snakeMap);
+            sb.append(dbCol).append(asc ? " asc" : " desc");
+        }
+        if (sb.length() > 11) {
+            wrapper.last(sb.toString());
         }
-        if (sb.length() > 11) wrapper.last(sb.toString());
     }
 
-    public static <T> void applySort(LambdaQueryWrapper<T> wrapper) {
-        List<Map<String, String>> sortSpecs = SortContext.get();
+    /**
+     * 对 LambdaQueryWrapper 应用排序(通过 last SQL 追加)。
+     * 因为 LambdaQueryWrapper 没有通用的 orderBy 重载接受 db 列名字符串,
+     * 所以直接通过 last() 追加。
+     */
+    public static <T> void applySort(LambdaQueryWrapper<T> wrapper,
+                                     List<Map<String, String>> sortSpecs,
+                                     Map<String, String> snakeMap) {
         if (sortSpecs == null || sortSpecs.isEmpty() || wrapper == null) return;
         StringBuilder sb = new StringBuilder(" order by ");
         for (int i = 0; i < sortSpecs.size(); i++) {
@@ -35,12 +87,41 @@ public class SortUtil {
             String col = spec.get("column");
             if (!StringUtils.hasText(col)) continue;
             boolean asc = !"desc".equalsIgnoreCase(spec.get("order"));
-            sb.append(toSnake(col)).append(asc ? " asc" : " desc");
+            String dbCol = resolveDbColumn(col, snakeMap);
+            sb.append(dbCol).append(asc ? " asc" : " desc");
+        }
+        if (sb.length() > 11) {
+            wrapper.last(sb.toString());
         }
-        if (sb.length() > 11) wrapper.last(sb.toString());
     }
 
-    private static String toSnake(String camel) {
-        return camel.replaceAll("([A-Z])", "_$1").toLowerCase();
+    /**
+     * 对 LambdaUpdateWrapper 应用排序(通过 last SQL 追加)。
+     */
+    public static <T> void applySort(LambdaUpdateWrapper<T> wrapper,
+                                     List<Map<String, String>> sortSpecs,
+                                     Map<String, String> snakeMap) {
+        if (sortSpecs == null || sortSpecs.isEmpty() || wrapper == null) return;
+        StringBuilder sb = new StringBuilder(" order by ");
+        for (int i = 0; i < sortSpecs.size(); i++) {
+            if (i > 0) sb.append(", ");
+            Map<String, String> spec = sortSpecs.get(i);
+            String col = spec.get("column");
+            if (!StringUtils.hasText(col)) continue;
+            boolean asc = !"desc".equalsIgnoreCase(spec.get("order"));
+            String dbCol = resolveDbColumn(col, snakeMap);
+            sb.append(dbCol).append(asc ? " asc" : " desc");
+        }
+        if (sb.length() > 11) {
+            wrapper.last(sb.toString());
+        }
+    }
+
+    private static String resolveDbColumn(String camelCol, Map<String, String> snakeMap) {
+        if (snakeMap != null && snakeMap.containsKey(camelCol)) {
+            return snakeMap.get(camelCol);
+        }
+        // 默认:驼峰转下划线
+        return camelCol.replaceAll("([A-Z])", "_$1").toLowerCase();
     }
 }

+ 11 - 5
cfc-frontend/components/report-blocks-renderer.vue

@@ -5,12 +5,18 @@
       <view v-if="block.type === 'score'" class="block score-block">
         <view class="block-title">{{ block.title }}</view>
         <view class="score-grid">
-          <view class="score-item" v-for="(item, si) in block.items" :key="getScoreKey(si)">
-            <view class="score-circle" :style="'border-color:' + (item.color || '#4A9BD7')">
-              <text class="score-value">{{ item.value }}</text>
+<view class="score-item" v-for="(item, si) in block.items" :key="getScoreKey(si)">
+              <view class="score-circle" :style="'border-color:' + (item.color || '#4A9BD7')">
+                <text class="score-value">{{ item.value }}</text>
+              </view>
+              <text class="score-label">{{ item.label }}</text>
+              <view class="sub-items" v-if="item.subItems && item.subItems.length" style="margin-top:8rpx; display:flex; flex-direction:column; align-items:center;">
+                <view class="sub-item" v-for="(sub, ssi) in item.subItems" :key="'sub'+ssi" style="display:flex; flex-direction:row; align-items:center;">
+                  <text class="sub-label" style="font-size:20rpx; color:#666; margin-right:4rpx;">{{ sub.label }}:</text>
+                  <text class="sub-value" style="font-size:20rpx; color:#333;">{{ sub.value }}</text>
+                </view>
+              </view>
             </view>
-            <text class="score-label">{{ item.label }}</text>
-          </view>
         </view>
       </view>
 

+ 1 - 0
cfc-frontend/pages/health/report-confirm.vue

@@ -489,6 +489,7 @@ export default {
   },
   onLoad: function(options) {
     this.draftId = options.draftId ? parseInt(options.draftId) : null
+    if (this.draftId) { uni.redirectTo({ url: '/pages/health/report-detail?draftId=' + this.draftId }); return }
     this.memberId = options.memberId ? parseInt(options.memberId) : null
     this.familyId = options.familyId ? parseInt(options.familyId) : null
 

+ 1659 - 109
cfc-frontend/pages/health/report-detail.vue

@@ -1,12 +1,6 @@
 <template>
   <view class="page">
-    <view class="nav-bar">
-      <view class="nav-back" @tap="goBack"><text class="back-text">‹ 返回</text></view>
-      <text class="nav-title">{{ typeLabel }}</text>
-      <view class="nav-edit" v-if="isGutFlora && hasRiskBlocks" @tap="toggleRiskEdit">
-        <text>{{ isEditing ? '完成' : '编辑' }}</text>
-      </view>
-    </view>
+    <!-- native navigation bar used, no custom header -->
     <scroll-view class="content" scroll-y>
       <!-- 报告头 -->
       <view class="report-head" v-if="headInfo">
@@ -16,153 +10,1709 @@
           <text class="head-date">{{ headInfo.reportDateText || '' }}</text>
         </view>
       </view>
-      <!-- blocks 渲染 -->
-      <report-blocks-renderer v-if="blocks && blocks.length > 0" :blocks="blocks" />
-      <view class="empty-hint" v-else>暂无报告内容</view>
-      <!-- 风险编辑面板 -->
-      <view class="edit-panel" v-if="isEditing">
-        <view class="edit-card" v-for="(risk, ei) in editRisks" :key="getEditRiskKey(ei)">
-          <text class="edit-name">{{ risk.diseaseName }}</text>
-          <view class="edit-row">
-            <text class="edit-label">风险值:</text>
-            <input class="edit-input" type="text" v-model="risk.riskValue" />
-            <text class="edit-label">等级:</text>
-            <picker :value="riskLevelIndex(risk.riskLevel)" :range="riskLevels" @change="onRiskLevelChange($event, risk)">
-              <text class="edit-picker">{{ risk.riskLevel || '请选择' }}</text>
-            </picker>
+
+      <!-- view mode -->
+      <view v-if="pageMode==='view'">
+        <report-blocks-renderer v-if="blocks && blocks.length > 0" :blocks="blocks" />
+        <view class="empty-hint" v-else>暂无报告内容</view>
+        <view class="edit-btn" v-if="isGutFlora && hasRiskBlocks && pageMode==='view'">
+          <view class="nav-edit" @tap="enterEditMode"><text>编辑</text></view>
+        </view>
+      </view>
+
+      <!-- draft/edit mode (full form) -->
+      <view v-else>
+        <!-- 解析中状态(仅 draft 模式,从上传页跳转后自动解析) -->
+        <view class="parsing-banner" v-if="parsing">
+          <text class="parsing-icon">⏳</text>
+          <text class="parsing-text">正在解析报告,请稍候...</text>
+        </view>
+
+        <view class="tip-banner">
+          <text class="tip-icon">✅</text>
+          <text class="tip-text">{{ pageMode === 'draft' ? '请核对以下解析内容,可修改后确认入库' : '请核对报告内容,可修改后保存' }}</text>
+        </view>
+
+        <!-- 匹配可信度(仅 draft 模式) -->
+        <view class="confidence-card" v-if="confidenceLevelClass">
+          <view class="confidence-header">
+            <text class="confidence-label">匹配可信度</text>
+            <text class="confidence-badge" :class="'badge-' + confidenceLevelClass">{{ confidenceLevelText }}</text>
+          </view>
+          <view class="confidence-bar-track">
+            <view class="confidence-bar-fill" :class="'fill-' + confidenceLevelClass" :style="'width:' + confidencePercent + '%'"></view>
+          </view>
+          <text class="confidence-desc">{{ confidenceDesc }}</text>
+        </view>
+
+        <view class="form-section">
+          <!-- 基本信息 -->
+          <view class="card">
+            <view class="card-title">基本信息</view>
+            <view class="field-row member-select-row" @click="toggleMemberPanel">
+              <text class="field-label">被检测人</text>
+              <text class="field-value" :class="{ 'binding-required': needBind && !selectedMemberObj }">{{ memberDisplayText }}</text>
+              <text class="member-select-arrow">{{ selectedMemberObj ? '修改 ›' : '选择 ›' }}</text>
+            </view>
+
+            <!-- 被检测人选择面板(内联展开) -->
+            <view class="member-panel" v-if="showMemberPanel">
+              <!-- 候选匹配列表(按置信度排序) -->
+              <view class="candidate-section" v-if="candidates.length > 0">
+                <text class="candidate-section-hint">以下成员与报告信息匹配,请选择:</text>
+                <view
+                  class="candidate-item"
+                  v-for="(cand, idx) in candidates"
+                  :key="idx"
+                  :class="{ 'candidate-selected': selectedMemberId === (cand.id || cand.userId) }"
+                  @click="selectAndClose(cand)"
+                >
+                  <view class="candidate-left">
+                    <text class="candidate-avatar">{{ (cand.nickname || cand.name || '?').substring(0, 1) }}</text>
+                    <view class="candidate-info">
+                      <text class="candidate-name">{{ cand.nickname || cand.name || '未知' }}</text>
+                      <text class="candidate-detail" v-if="cand.gender || cand.age">{{ genderText(cand.gender) }}{{ cand.age ? ' · ' + cand.age + '岁' : '' }}</text>
+                      <text class="candidate-fields" v-if="cand.matchFields">匹配: {{ cand.matchFields }}</text>
+                    </view>
+                  </view>
+                  <view class="candidate-right">
+                    <view class="score-bar-track">
+                      <view class="score-bar-fill" :style="'width:' + (cand.similarityScore * 100) + '%'"></view>
+                    </view>
+                    <text class="score-text">{{ Math.round(cand.similarityScore * 100) }}%</text>
+                  </view>
+                </view>
+              </view>
+
+              <!-- 全量家庭成员列表(后备) -->
+              <view class="member-list" v-if="familyMembers.length > 0 && (candidates.length === 0 || !showCandidatesOnly)">
+                <view
+                  class="member-item"
+                  v-for="(member, mIdx) in familyMembers"
+                  :key="mIdx"
+                  :class="{ 'member-selected': selectedMemberId === (member.userId || member.id) }"
+                  @click="selectAndClose(member)"
+                >
+                  <text class="member-avatar">{{ (member.nickname || '?').substring(0, 1) }}</text>
+                  <text class="member-name">{{ member.nickname || '未知' }}</text>
+                </view>
+              </view>
+
+              <!-- 新建成员入口 -->
+              <view class="create-member-entry" @click="toggleAutoCreate">
+                <text class="create-member-icon">+</text>
+                <text class="create-member-text">{{ showAutoCreateForm ? '收起新建表单' : '不在列表中?新建家庭成员' }}</text>
+              </view>
+
+              <!-- 新建成员表单(面板内嵌) -->
+              <view class="auto-create-inline" v-if="showAutoCreateForm">
+                <view class="form-item">
+                  <text class="form-item-label">姓名</text>
+                  <input class="form-item-input" v-model="newMember.name" placeholder="请输入姓名" />
+                </view>
+                <view class="form-item">
+                  <text class="form-item-label">性别</text>
+                  <view class="gender-selector">
+                    <text class="gender-option" :class="{ 'gender-selected': newMember.gender === 'male' }" @click="newMember.gender = 'male'">男</text>
+                    <text class="gender-option" :class="{ 'gender-selected': newMember.gender === 'female' }" @click="newMember.gender = 'female'">女</text>
+                  </view>
+                </view>
+                <view class="form-item">
+                  <text class="form-item-label">年龄</text>
+                  <input class="form-item-input" type="number" v-model="newMember.age" placeholder="请输入年龄" />
+                </view>
+                <view class="btn-create-submit" :class="{ 'btn-disabled': creatingMember }" @click="doAutoCreate">
+                  <text v-if="creatingMember">创建中...</text>
+                  <text v-else>确认创建</text>
+                </view>
+              </view>
+            </view>
+
+            <view class="field-row" v-if="extractedGender">
+              <text class="field-label">性别</text>
+              <text class="field-value">{{ extractedGender === 'male' ? '男' : extractedGender === 'female' ? '女' : extractedGender }}</text>
+            </view>
+            <view class="field-row" v-if="extractedAge">
+              <text class="field-label">年龄</text>
+              <text class="field-value">{{ extractedAge }}</text>
+            </view>
+            <view class="field-row" v-if="payload.summary && payload.type !== 'tongue'">
+              <text class="field-label">综合评分</text>
+              <input class="field-input" type="number" v-model="payload.summary.overallScore" />
+            </view>
+            <view class="field-row" v-if="payload.summary && payload.type !== 'tongue'">
+              <text class="field-label">报告日期</text>
+              <input class="field-input" type="text" v-model="payload.summary.reportDate" placeholder="如: 2025-06-15" />
+            </view>
+            <view class="field-row" v-if="payload.summary && payload.type !== 'tongue'">
+              <text class="field-label">报告摘要</text>
+              <textarea class="field-textarea" v-model="payload.summary.summary" maxlength="500" />
+            </view>
+          </view>
+
+          <!-- 舌诊指标(只读展示) -->
+          <view class="card" v-if="payload.type === 'tongue' && payload.indicators && payload.indicators.length">
+            <view class="card-title">舌象指标</view>
+            <view class="tongue-indicator-grid">
+              <view class="tongue-ind-item" v-for="(ind, idx) in payload.indicators" :key="idx">
+                <text class="tongue-ind-label">{{ indicatorLabel(ind.code) }}</text>
+                <text class="tongue-ind-value">{{ ind.value }}</text>
+              </view>
+            </view>
+            <view class="tongue-summary" v-if="payload.summary && payload.summary.overallAssessment">
+              <text class="tongue-summary-title">综合评估</text>
+              <text class="tongue-summary-text">{{ payload.summary.overallAssessment }}</text>
+            </view>
+          </view>
+
+          <!-- 健康指标(舌诊时隐藏,使用上方舌象指标卡片) -->
+          <view class="card" v-if="payload.type !== 'tongue' && payload.indicators && payload.indicators.length">
+            <view class="card-title">
+              <text>健康指标</text>
+              <text class="title-count">共 {{ payload.indicators.length }} 项</text>
+              <text class="title-abnormal" v-if="totalAbnormalCount > 0">,{{ totalAbnormalCount }} 项异常</text>
+              <view class="card-title-actions">
+                <text class="expand-all-btn" @click="toggleAllGroups(true)">全部展开</text>
+                <text class="expand-all-btn" @click="toggleAllGroups(false)">全部折叠</text>
+              </view>
+            </view>
+
+            <!-- 按指标类型分组折叠 -->
+            <view class="indicator-group" v-for="(group, gIdx) in indicatorGroups" :key="gIdx">
+              <view class="indicator-group-header" @click="toggleGroup(gIdx)">
+                <view class="indicator-group-header-left">
+                  <text class="indicator-group-arrow" :class="{ 'arrow-open': groupExpandList[gIdx] }">›</text>
+                  <text class="indicator-group-name">{{ group.category }}</text>
+                </view>
+                <view class="indicator-group-header-right">
+                  <text class="indicator-group-abnormal" v-if="group.abnormalCount > 0">{{ group.abnormalCount }} 项异常</text>
+                  <text class="indicator-group-count">{{ group.items.length }} 项</text>
+                </view>
+              </view>
+
+              <view class="indicator-group-body" v-if="groupExpandList[gIdx]">
+                <view class="indicator-item" v-for="(ind, idx) in group.items" :key="idx">
+                  <view class="ind-header">
+                    <text class="ind-help-btn" @click.stop="showHelp(ind)">?</text>
+                    <input class="ind-name" :class="'ind-name-' + _cssClass(ind.status)" v-model="ind.indicatorName" />
+                    <input class="ind-value" :class="'ind-value-' + _cssClass(ind.status)" v-model="ind.indicatorValue" />
+                  </view>
+                  <view class="ind-detail" v-if="ind.unit || ind.refRange">
+                    <text class="ind-ref" v-if="ind.refRange">参考: {{ ind.refRange }}</text>
+                    <text class="ind-unit" v-if="ind.unit">{{ ind.unit }}</text>
+                  </view>
+                  <view class="ind-symptoms" v-if="ind.symptoms">
+                    <text class="ind-symptom-label">症状:</text>
+                    <input class="ind-symptom-input" v-model="ind.symptoms" />
+                  </view>
+                </view>
+              </view>
+            </view>
+          </view>
+
+          <!-- 肠道菌群 -->
+          <view class="card" v-if="payload.gutFlora && payload.gutFlora.length">
+            <view class="card-title">肠道菌群 ({{ payload.gutFlora.length }})</view>
+            <view class="flora-item" v-for="(flora, idx) in payload.gutFlora" :key="idx">
+              <input class="flora-name" v-model="payload.gutFlora[idx].name" />
+              <input class="flora-abundance" type="digit" v-model="payload.gutFlora[idx].abundance" />
+              <view class="status-tag" :class="'status-' + _cssClass(flora.status)">
+                {{ statusText(flora.status) }}
+              </view>
+            </view>
+          </view>
+
+          <!-- 疾病风险 -->
+          <view class="card" v-if="payload.diseaseRisks && payload.diseaseRisks.length">
+            <view class="card-title">疾病风险 ({{ payload.diseaseRisks.length }})</view>
+            <view class="risk-item" v-for="(risk, idx) in payload.diseaseRisks" :key="idx">
+              <input class="risk-name" v-model="payload.diseaseRisks[idx].diseaseName" />
+              <view class="risk-level" :class="'risk-' + (risk.riskLevel || 'low')">
+                {{ riskLevelText(risk.riskLevel) }}
+              </view>
+            </view>
+          </view>
+
+          <!-- 饮食推荐 -->
+          <view class="card" v-if="payload.foods && payload.foods.length">
+            <view class="card-title">饮食推荐 ({{ payload.foods.length }})</view>
+            <view class="food-item" v-for="(food, idx) in payload.foods" :key="idx">
+              <input class="food-name" v-model="payload.foods[idx].foodName" />
+              <input class="food-category" v-model="payload.foods[idx].category" />
+            </view>
+          </view>
+
+          <!-- 益生菌种 -->
+          <view class="card" v-if="payload.probioticSpecies && payload.probioticSpecies.length">
+            <view class="card-title">益生菌种 ({{ payload.probioticSpecies.length }})</view>
+            <view class="flora-item" v-for="(flora, idx) in payload.probioticSpecies" :key="idx">
+              <input class="flora-name" v-model="payload.probioticSpecies[idx].name" />
+              <input class="flora-abundance" type="digit" v-model="payload.probioticSpecies[idx].abundance" />
+              <view class="status-tag" :class="'status-' + _cssClass(flora.status)">
+                {{ statusText(flora.status) }}
+              </view>
+            </view>
+          </view>
+
+          <!-- Taxonomy 分类(纲/目/科/属/种) -->
+          <view class="card" v-if="hasTaxonomyData()">
+            <view class="card-title">菌群分类组成 ({{ taxonomyTotalCount }})</view>
+            <view class="taxonomy-group" v-if="payload.taxonomyClass && payload.taxonomyClass.length">
+              <text class="taxonomy-group-label">菌纲 (Class)</text>
+              <view class="flora-item" v-for="(flora, idx) in payload.taxonomyClass" :key="getTaxonomyKey('c', idx)">
+                <input class="flora-name" v-model="payload.taxonomyClass[idx].name" />
+                <input class="flora-abundance" type="digit" v-model="payload.taxonomyClass[idx].abundance" />
+              </view>
+            </view>
+            <view class="taxonomy-group" v-if="payload.taxonomyOrder && payload.taxonomyOrder.length">
+              <text class="taxonomy-group-label">菌目 (Order)</text>
+              <view class="flora-item" v-for="(flora, idx) in payload.taxonomyOrder" :key="getTaxonomyKey('o', idx)">
+                <input class="flora-name" v-model="payload.taxonomyOrder[idx].name" />
+                <input class="flora-abundance" type="digit" v-model="payload.taxonomyOrder[idx].abundance" />
+              </view>
+            </view>
+            <view class="taxonomy-group" v-if="payload.taxonomyFamily && payload.taxonomyFamily.length">
+              <text class="taxonomy-group-label">菌科 (Family)</text>
+              <view class="flora-item" v-for="(flora, idx) in payload.taxonomyFamily" :key="getTaxonomyKey('f', idx)">
+                <input class="flora-name" v-model="payload.taxonomyFamily[idx].name" />
+                <input class="flora-abundance" type="digit" v-model="payload.taxonomyFamily[idx].abundance" />
+              </view>
+            </view>
+            <view class="taxonomy-group" v-if="payload.taxonomyGenus && payload.taxonomyGenus.length">
+              <text class="taxonomy-group-label">菌属 (Genus)</text>
+              <view class="flora-item" v-for="(flora, idx) in payload.taxonomyGenus" :key="getTaxonomyKey('g', idx)">
+                <input class="flora-name" v-model="payload.taxonomyGenus[idx].name" />
+                <input class="flora-abundance" type="digit" v-model="payload.taxonomyGenus[idx].abundance" />
+              </view>
+            </view>
+            <view class="taxonomy-group" v-if="payload.taxonomySpecies && payload.taxonomySpecies.length">
+              <text class="taxonomy-group-label">菌种 (Species)</text>
+              <view class="flora-item" v-for="(flora, idx) in payload.taxonomySpecies" :key="getTaxonomyKey('s', idx)">
+                <input class="flora-name" v-model="payload.taxonomySpecies[idx].name" />
+                <input class="flora-abundance" type="digit" v-model="payload.taxonomySpecies[idx].abundance" />
+              </view>
+            </view>
+          </view>
+
+          <!-- 病原菌 -->
+          <view class="card" v-if="hasPathogenData()">
+            <view class="card-title">病原菌 ({{ pathogenTotalCount }})</view>
+            <view class="pathogen-group" v-if="payload.pathogenGenus && payload.pathogenGenus.length">
+              <text class="pathogen-group-label">病原菌属</text>
+              <view class="flora-item" v-for="(flora, idx) in payload.pathogenGenus" :key="getTaxonomyKey('pg', idx)">
+                <input class="flora-name" v-model="payload.pathogenGenus[idx].name" />
+                <input class="flora-abundance" type="digit" v-model="payload.pathogenGenus[idx].abundance" />
+                <view class="status-tag" :class="'status-' + _cssClass(flora.status)">
+                  {{ statusText(flora.status) }}
+                </view>
+              </view>
+            </view>
+            <view class="pathogen-group" v-if="payload.pathogenDetection && payload.pathogenDetection.length">
+              <text class="pathogen-group-label">病原菌检出</text>
+              <view class="flora-item" v-for="(flora, idx) in payload.pathogenDetection" :key="getTaxonomyKey('pd', idx)">
+                <input class="flora-name" v-model="payload.pathogenDetection[idx].name" />
+                <input class="flora-abundance" type="digit" v-model="payload.pathogenDetection[idx].abundance" />
+                <view class="status-tag" :class="'status-' + _cssClass(flora.status)">
+                  {{ statusText(flora.status) }}
+                </view>
+              </view>
+            </view>
+          </view>
+
+          <!-- 操作按钮(draft: 放弃/确认入库; edit: 取消/保存) -->
+          <view class="action-row">
+            <view class="btn-discard" @click="handleLeftAction">
+              <text>{{ pageMode === 'draft' ? '放弃' : '取消' }}</text>
+            </view>
+            <view class="btn-confirm" :class="{ 'btn-disabled': pageMode === 'draft' && !canConfirm }" @click="handleRightAction">
+              <text v-if="pageMode === 'draft' && confirming">确认中...</text>
+              <text v-else>{{ pageMode === 'draft' ? '确认入库' : '保存' }}</text>
+            </view>
           </view>
         </view>
+
+        <view class="bottom-spacer"></view>
       </view>
     </scroll-view>
+
+    <!-- 指标帮助弹窗(置于 scroll-view 外,position:fixed 覆盖视口) -->
+    <view class="help-overlay" v-if="showHelpPopup" @click="closeHelp">
+      <view class="help-popup" @click.stop>
+        <view class="help-popup-header">
+          <text class="help-popup-title">{{ helpData.name }}</text>
+          <text class="help-popup-close" @click="closeHelp">✕</text>
+        </view>
+        <view class="help-popup-body">
+          <view class="help-row">
+            <text class="help-label">当前值</text>
+            <text class="help-value">{{ helpData.value }}</text>
+          </view>
+          <view class="help-row" v-if="helpData.refRange">
+            <text class="help-label">参考范围</text>
+            <text class="help-value">{{ helpData.refRange }}</text>
+          </view>
+          <view class="help-row">
+            <text class="help-label">状态</text>
+            <text class="help-value" :class="'help-status-' + _cssClass(helpData.status)">{{ statusText(helpData.status) }}</text>
+          </view>
+          <view class="help-desc" v-if="helpData.description">
+            <text class="help-desc-label">说明</text>
+            <text class="help-desc-text">{{ helpData.description }}</text>
+          </view>
+          <view class="help-desc" v-else>
+            <text class="help-desc-label">说明</text>
+            <text class="help-desc-text">{{ getDefaultDescription(helpData.name, helpData.status) }}</text>
+          </view>
+        </view>
+      </view>
+    </view>
   </view>
 </template>
 
 <script>
 import ReportBlocksRenderer from '../../components/report-blocks-renderer.vue'
-import { getReportDetail, getDanReportDetail, editHealthReport } from '../../utils/api.js'
+import { getReportDetail, getDanReportDetail, editHealthReport, parseReportDraft, confirmReportPreview, discardReportDraft, confirmTongue, discardTongue, getFamilyMembers, addFamilyMember } from '../../utils/api.js'
 import { parseDate } from '../../utils/format.js'
 
 export default {
-  components: { ReportBlocksRenderer },
   data() {
     return {
-      reportType: '',
+      reportType: 'gut_flora',
       reportId: null,
+      draftId: null,
+      memberId: null,
+      familyId: null,
       blocks: [],
       headInfo: null,
-      isEditing: false,
-      editRisks: [],
-      riskLevels: ['低风险', '需注意', '高风险', '异常']
+      pageMode: 'view', // view / draft / edit
+      parsing: false,
+      confirming: false,
+      payload: {
+        type: 'gut_flora',
+        summary: {},
+        indicators: [],
+        gutFlora: [],
+        diseaseRisks: [],
+        foods: [],
+        probioticSpecies: [],
+        taxonomyClass: [],
+        taxonomyOrder: [],
+        taxonomyFamily: [],
+        taxonomyGenus: [],
+        taxonomySpecies: [],
+        pathogenGenus: [],
+        pathogenDetection: []
+      },
+      extractedName: '',
+      extractedGender: '',
+      extractedAge: '',
+      matchedMemberId: null,
+      needBind: false,
+      selectedMemberId: null,
+      selectedMemberObj: null,
+      familyMembers: [],
+      matchConfidence: null,
+      matchLabel: '',
+      candidates: [],
+      showCandidatesOnly: false,
+      showMemberPanel: false,
+      showAutoCreateForm: false,
+      newMember: { name: '', gender: 'male', age: '' },
+      creatingMember: false,
+      createdMemberId: null,
+      groupExpandList: [],
+      showHelpPopup: false,
+      helpData: {}
     }
   },
   computed: {
-    typeLabel: function() {
-      var map = { 'gut_flora': '肠道菌群报告', 'dan': 'DAN测评报告', 'physical_exam': '体检报告', 'tongue': '舌诊报告' }
-      return map[this.reportType] || '报告详情'
+    typeLabel() {
+      var map = { gut_flora: '肠道菌群报告', dan: 'DAN测评报告', physical_exam: '体检报告', tongue: '舌诊报告' }
+      return map[this.reportType] || '健康报告'
+    },
+    isGutFlora() {
+      return this.reportType === 'gut_flora'
+    },
+    hasRiskBlocks() {
+      var list = this.blocks || []
+      return list.some(function(b) { return b.riskLevel === 'high' || b.riskLevel === 'medium' || b.riskLevel === 'moderate' })
+    },
+    totalAbnormalCount() {
+      var list = this.payload.indicators || []
+      var count = 0
+      for (var i = 0; i < list.length; i++) {
+        if (this.isAbnormalStatus(list[i].status)) count++
+      }
+      return count
+    },
+    indicatorGroups() {
+      var groups = []
+      var orderMap = {}
+      var list = this.payload.indicators || []
+      for (var i = 0; i < list.length; i++) {
+        var ind = list[i]
+        var cat = ind.category || '其他指标'
+        var gIdx = orderMap[cat]
+        if (gIdx === undefined) {
+          gIdx = groups.length
+          orderMap[cat] = gIdx
+          groups.push({ category: cat, items: [], abnormalCount: 0 })
+        }
+        groups[gIdx].items.push(ind)
+        if (this.isAbnormalStatus(ind.status)) groups[gIdx].abnormalCount++
+      }
+      return groups
+    },
+    memberDisplayText() {
+      if (this.selectedMemberObj) return this.selectedMemberObj.nickname || this.selectedMemberObj.name || '已选择'
+      if (this.extractedName) return this.extractedName + (this.needBind ? '(待绑定)' : '')
+      return '未选择'
+    },
+    canConfirm() {
+      if (this.parsing) return false
+      if (this.needBind && !this.selectedMemberId && !this.createdMemberId) return false
+      return this.draftId && !this.confirming
+    },
+    confidenceLevelClass() {
+      var c = this.matchConfidence
+      if (c === null || c === undefined) return ''
+      if (c >= 0.8) return 'high'
+      if (c >= 0.6) return 'medium'
+      return 'low'
+    },
+    confidenceLevelText() {
+      var cls = this.confidenceLevelClass
+      if (cls === 'high') return '高'
+      if (cls === 'medium') return '中'
+      if (cls === 'low') return '低'
+      return ''
+    },
+    confidencePercent() {
+      var c = this.matchConfidence
+      if (c === null || c === undefined) return 0
+      return Math.round(c * 100)
+    },
+    confidenceDesc() {
+      var cls = this.confidenceLevelClass
+      if (cls === 'high') return '报告信息与档案高度匹配,可信度较高。'
+      if (cls === 'medium') return '报告信息与档案部分匹配,请仔细核对被检测人。'
+      if (cls === 'low') return '报告信息与档案匹配度较低,建议手动选择被检测人。'
+      return ''
+    },
+    taxonomyTotalCount() {
+      var lists = [this.payload.taxonomyClass, this.payload.taxonomyOrder, this.payload.taxonomyFamily, this.payload.taxonomyGenus, this.payload.taxonomySpecies]
+      var total = 0
+      for (var i = 0; i < lists.length; i++) {
+        if (lists[i] && lists[i].length) total += lists[i].length
+      }
+      return total
+    },
+    pathogenTotalCount() {
+      var lists = [this.payload.pathogenGenus, this.payload.pathogenDetection]
+      var total = 0
+      for (var i = 0; i < lists.length; i++) {
+        if (lists[i] && lists[i].length) total += lists[i].length
+      }
+      return total
     }
   },
-  onLoad: function(options) {
+  onLoad(options) {
     this.reportType = options.reportType || 'gut_flora'
     this.reportId = options.reportId ? parseInt(options.reportId) : null
-    if (this.reportId) this.loadData()
+    this.draftId = options.draftId ? parseInt(options.draftId) : null
+    this.memberId = options.memberId ? parseInt(options.memberId) : null
+    this.familyId = options.familyId ? parseInt(options.familyId) : null
+    if (this.draftId) {
+      this.pageMode = 'draft'
+      this.parseByDraftId()
+    } else if (this.reportId) {
+      this.loadData()
+    }
   },
   methods: {
-    goBack: function() { uni.navigateBack() },
-    loadData: function() {
+    loadData() {
       var self = this
-      var loadFn = self.reportType === 'dan' ? getDanReportDetail : getReportDetail
-      loadFn(self.reportId).then(function(res) {
-        if (res.code === 200 && res.data) {
-          self.blocks = res.data.blocks || []
+      uni.showLoading({ title: '加载中...' })
+      var fn = self.reportType === 'dan' ? getDanReportDetail : getReportDetail
+      fn({ reportType: self.reportType, reportId: self.reportId }).then(function(res) {
+        uni.hideLoading()
+        if (res.code === 200) {
           var report = res.data.report || res.data
           var head = {}
-          if (self.reportType === 'dan') {
-            head.childName = report.childName
-            head.reportDateText = report.assessmentDate || ''
-          } else {
-            head.personName = report.personName
-            var d = parseDate(report.reportDate)
-            head.reportDateText = d ? (d.getFullYear() + '-' + (d.getMonth() + 1) + '-' + d.getDate()) : ''
-          }
+          head.personName = report.personName || report.childName || ''
+          head.childName = report.childName || ''
+          head.reportDateText = report.reportDate ? parseDate(report.reportDate) : ''
           self.headInfo = head
+          self.blocks = report.blocks || []
+          if (res.data.payload) {
+            self.payload = Object.assign({}, self.defaultPayload(), res.data.payload)
+          }
+          self.needBind = !!report.needBind
+          self.selectedMemberId = report.subjectId || null
+        } else {
+          uni.showToast({ title: res.message || '加载失败', icon: 'none' })
         }
       }).catch(function() {
-        uni.showToast({ title: '加载失败', icon: 'none' })
+        uni.hideLoading()
+        uni.showToast({ title: '加载异常', icon: 'none' })
       })
     },
-    isGutFlora: function() {
-      return this.reportType === 'gut_flora'
+    defaultPayload() {
+      return {
+        type: 'gut_flora',
+        summary: {},
+        indicators: [],
+        gutFlora: [],
+        diseaseRisks: [],
+        foods: [],
+        probioticSpecies: [],
+        taxonomyClass: [],
+        taxonomyOrder: [],
+        taxonomyFamily: [],
+        taxonomyGenus: [],
+        taxonomySpecies: [],
+        pathogenGenus: [],
+        pathogenDetection: []
+      }
+    },
+    enterEditMode() {
+      this.pageMode = 'edit'
+      this.showMemberPanel = false
     },
-    hasRiskBlocks: function() {
-      var blocks = this.blocks || []
-      for (var i = 0; i < blocks.length; i++) {
-        if (blocks[i].type === 'risk_group' && blocks[i].items && blocks[i].items.length > 0) return true
+    cancelEdit() {
+      this.pageMode = 'view'
+      this.showMemberPanel = false
+      this.showAutoCreateForm = false
+      this.showHelpPopup = false
+      this.loadData()
+    },
+    parseByDraftId() {
+      var self = this
+      self.parsing = true
+      parseReportDraft(self.draftId).then(function(res) {
+        self.parsing = false
+        if (res.code === 200) {
+          self.applyParseResult(res.data)
+        } else {
+          uni.showToast({ title: res.message || '解析失败', icon: 'none' })
+        }
+      }).catch(function() {
+        self.parsing = false
+        uni.showToast({ title: '解析异常', icon: 'none' })
+      })
+    },
+    applyParseResult(detail) {
+      var self = this
+      self.payload = detail.payload || self.defaultPayload()
+      self.extractedName = detail.extractedName || ''
+      self.extractedGender = detail.extractedGender || ''
+      self.extractedAge = detail.extractedAge || ''
+      self.matchConfidence = detail.matchConfidence !== undefined && detail.matchConfidence !== null ? detail.matchConfidence : null
+      self.matchLabel = detail.matchLabel || ''
+      self.matchedMemberId = detail.matchedMemberId || null
+      self.needBind = !!detail.needBind
+      self.candidates = detail.candidates || []
+      self.showCandidatesOnly = !!detail.showCandidatesOnly
+      self.loadFamilyMembers()
+      if (!self.needBind) {
+        self.selectedMemberId = self.matchedMemberId
+      }
+      var best = self.candidates[0]
+      if (best && (best.id || best.userId)) {
+        self.selectedMemberId = best.id || best.userId
+        self.selectedMemberObj = best
       }
-      return false
     },
-    toggleRiskEdit: function() {
+    loadFamilyMembers() {
       var self = this
-      if (this.isEditing) {
-        editHealthReport(this.reportId, { diseaseRisks: this.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' })
-          }
-        })
-      } else {
-        var risks = []
-        var blocks = this.blocks || []
-        for (var i = 0; i < blocks.length; i++) {
-          if (blocks[i].type === 'risk_group' && blocks[i].items) {
-            for (var j = 0; j < blocks[i].items.length; j++) {
-              risks.push({
-                diseaseName: blocks[i].items[j].name,
-                riskValue: blocks[i].items[j].value,
-                riskLevel: blocks[i].items[j].level
-              })
-            }
-          }
+      getFamilyMembers({ familyId: self.familyId || 0 }).then(function(res) {
+        if (res.code === 200) {
+          self.familyMembers = res.data || []
         }
-        this.editRisks = JSON.parse(JSON.stringify(risks))
-        this.isEditing = true
+      }).catch(function() {})
+    },
+    toggleMemberPanel() {
+      this.showMemberPanel = !this.showMemberPanel
+      if (this.showMemberPanel && this.familyMembers.length === 0) {
+        this.loadFamilyMembers()
       }
     },
-    getEditRiskKey: function(ei) { return 'e' + ei },
-    riskLevelIndex: function(level) {
-      var idx = this.riskLevels.indexOf(level)
-      return idx >= 0 ? idx : 0
+    selectAndClose(member) {
+      this.selectedMemberId = member.id || member.userId
+      this.selectedMemberObj = member
+      this.createdMemberId = null
+      this.needBind = false
+      this.showMemberPanel = false
+      this.showAutoCreateForm = false
+    },
+    genderText(g) {
+      if (g === 'male') return '男'
+      if (g === 'female') return '女'
+      return g || ''
+    },
+    toggleAutoCreate() {
+      this.showAutoCreateForm = !this.showAutoCreateForm
     },
-    onRiskLevelChange: function(e, risk) {
-      risk.riskLevel = this.riskLevels[e.detail.value]
+    doAutoCreate() {
+      var self = this
+      if (!self.newMember.name) {
+        uni.showToast({ title: '请填写姓名', icon: 'none' })
+        return
+      }
+      self.creatingMember = true
+      addFamilyMember({
+        name: self.newMember.name,
+        gender: self.newMember.gender,
+        age: self.newMember.age ? parseInt(self.newMember.age) : null
+      }).then(function(res) {
+        self.creatingMember = false
+        if (res.code === 200) {
+          var member = res.data || {}
+          self.selectedMemberId = member.id || member.userId
+          self.selectedMemberObj = member
+          self.createdMemberId = self.selectedMemberId
+          self.needBind = false
+          self.showMemberPanel = false
+          self.showAutoCreateForm = false
+          self.loadFamilyMembers()
+          uni.showToast({ title: '创建成功', icon: 'success' })
+        } else {
+          uni.showToast({ title: res.message || '创建失败', icon: 'none' })
+        }
+      }).catch(function() {
+        self.creatingMember = false
+        uni.showToast({ title: '创建异常', icon: 'none' })
+      })
+    },
+    toggleGroup(idx) {
+      this.groupExpandList[idx] = !this.groupExpandList[idx]
+    },
+    toggleAllGroups(open) {
+      var groups = this.indicatorGroups
+      var list = []
+      for (var i = 0; i < groups.length; i++) list.push(!!open)
+      this.groupExpandList = list
+    },
+    _cssClass(status) {
+      var s = (status || '').toString()
+      if (s === 'high' || s === '偏高') return 'high'
+      if (s === 'low' || s === '偏低') return 'low'
+      if (s === '缺乏' || s === '不足' || s === 'deficient' || s === 'insufficient') return 'deficient'
+      if (s === 'abnormal' || s === '异常') return 'abnormal'
+      if (s === 'warning' || s === '注意' || s === '需注意') return 'warning'
+      if (s === 'high-risk' || s === '高风险') return 'high-risk'
+      if (s === 'low-risk' || s === '低风险') return 'low-risk'
+      if (s === 'not-detected' || s === '未检出') return 'not-detected'
+      return 'normal'
+    },
+    statusText(status) {
+      var s = (status || '').toString()
+      if (s === 'high' || s === '偏高') return '偏高'
+      if (s === 'low' || s === '偏低') return '偏低'
+      if (s === '缺乏' || s === 'deficient') return '缺乏'
+      if (s === '不足' || s === 'insufficient') return '不足'
+      if (s === 'abnormal' || s === '异常') return '异常'
+      if (s === 'warning' || s === '注意' || s === '需注意') return '需注意'
+      if (s === 'high-risk' || s === '高风险') return '高风险'
+      if (s === 'low-risk' || s === '低风险') return '低风险'
+      if (s === 'not-detected' || s === '未检出') return '未检出'
+      if (s === 'normal') return '正常'
+      return '正常'
+    },
+    isAbnormalStatus(status) {
+      var s = (status || '').toString()
+      return !(s === '' || s === 'normal' || s === '正常' || s === 'low-risk' || s === '低风险' || s === 'not-detected' || s === '未检出')
+    },
+    showHelp(ind) {
+      this.helpData = {
+        name: ind.indicatorName || ind.name || '',
+        value: ind.indicatorValue || ind.value || '',
+        refRange: ind.refRange || '',
+        status: ind.status || 'normal',
+        description: ind.description || ''
+      }
+      this.showHelpPopup = true
+    },
+    closeHelp() {
+      this.showHelpPopup = false
+    },
+    getDefaultDescription(name, status) {
+      if (!status || status === 'normal') return '该指标处于正常范围内,无需特别关注。'
+      if (status === 'high' || status === '偏高') return '该指标偏高,建议咨询专业医生或营养师进行进一步评估。'
+      if (status === 'low' || status === '偏低') return '该指标偏低,建议咨询专业医生或营养师进行进一步评估。'
+      if (status === '缺乏' || status === '不足') return '该指标不足,建议通过饮食或补充剂适当补充。'
+      if (status === 'abnormal') return '该指标异常,建议咨询专业医生进行详细检查。'
+      if (status === '注意') return '该指标需要关注,建议定期监测并咨询专业人士。'
+      return '请参考专业医生的建议进行解读。'
+    },
+    riskLevelText(level) {
+      if (level === 'high') return '高风险'
+      if (level === 'medium' || level === 'moderate') return '中风险'
+      return '低风险'
+    },
+    indicatorLabel(code) {
+      var map = { tongueColor: '舌色', tongueShape: '舌形', tongueCoating: '舌苔', tongueMoisture: '润燥' }
+      return map[code] || code || ''
+    },
+    getTaxonomyKey(prefix, idx) {
+      return prefix + '_' + idx
+    },
+    hasTaxonomyData() {
+      return !!(this.payload.taxonomyClass && this.payload.taxonomyClass.length) ||
+        !!(this.payload.taxonomyOrder && this.payload.taxonomyOrder.length) ||
+        !!(this.payload.taxonomyFamily && this.payload.taxonomyFamily.length) ||
+        !!(this.payload.taxonomyGenus && this.payload.taxonomyGenus.length) ||
+        !!(this.payload.taxonomySpecies && this.payload.taxonomySpecies.length)
+    },
+    hasPathogenData() {
+      return !!(this.payload.pathogenGenus && this.payload.pathogenGenus.length) ||
+        !!(this.payload.pathogenDetection && this.payload.pathogenDetection.length)
+    },
+    handleLeftAction() {
+      if (this.pageMode === 'draft') this.doDiscard()
+      else this.cancelEdit()
+    },
+    handleRightAction() {
+      if (this.pageMode === 'draft') this.doConfirm()
+      else this.saveEdit()
+    },
+    doDiscard() {
+      var self = this
+      uni.showModal({
+        title: '提示',
+        content: '确定放弃该报告草稿吗?',
+        success: function(res) {
+          if (!res.confirm) return
+          var discardFn = self.payload.type === 'tongue' ? discardTongue : discardReportDraft
+          discardFn(self.draftId).then(function(res2) {
+            if (res2.code === 200) {
+              uni.showToast({ title: '已放弃', icon: 'success' })
+              setTimeout(function() { uni.navigateBack() }, 600)
+            } else {
+              uni.showToast({ title: res2.message || '操作失败', icon: 'none' })
+            }
+          })
+        }
+      })
+    },
+    doConfirm() {
+      if (!this.canConfirm) return
+      var self = this
+      self.confirming = true
+      uni.showLoading({ title: '确认入库...' })
+      var postData = { draftId: self.draftId, payload: self.payload }
+      if (self.selectedMemberId) postData.subjectId = self.selectedMemberId
+      else if (self.createdMemberId) postData.subjectId = self.createdMemberId
+      else if (self.matchedMemberId) postData.subjectId = self.matchedMemberId
+      else if (self.memberId) postData.subjectId = self.memberId
+      var confirmFn = self.payload.type === 'tongue' ? confirmTongue : confirmReportPreview
+      confirmFn(postData).then(function(res) {
+        uni.hideLoading()
+        self.confirming = false
+        if (res.code === 200) {
+          var reportId = res.data && res.data.reportId
+          if (reportId && self.payload.type !== 'tongue') {
+            setTimeout(function() {
+              uni.redirectTo({ url: '/pages/health/report-detail?reportType=' + (self.payload.type || 'gut_flora') + '&reportId=' + reportId })
+              uni.showToast({ title: '入库成功', icon: 'success' })
+            }, 600)
+          } else {
+            setTimeout(function() {
+              uni.navigateBack()
+              uni.showToast({ title: '入库成功', icon: 'success' })
+            }, 600)
+          }
+        } else {
+          uni.showToast({ title: res.message || '确认失败', icon: 'none' })
+        }
+      }).catch(function() {
+        uni.hideLoading()
+        self.confirming = false
+        uni.showToast({ title: '确认异常', icon: 'none' })
+      })
+    },
+    saveEdit() {
+      var self = this
+      if (!self.reportId) return
+      uni.showLoading({ title: '保存中...' })
+      var subjectId = self.selectedMemberId || self.createdMemberId || 0
+      editHealthReport(self.reportId, self.payload, subjectId).then(function(res) {
+        uni.hideLoading()
+        if (res.code === 200) {
+          uni.showToast({ title: '已保存', icon: 'success' })
+          self.pageMode = 'view'
+          self.groupExpandList = []
+          self.loadData()
+        } else {
+          uni.showToast({ title: res.message || '保存失败', icon: 'none' })
+        }
+      }).catch(function() {
+        uni.hideLoading()
+        uni.showToast({ title: '保存异常', icon: 'none' })
+      })
     }
   }
 }
 </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 { padding: 10rpx 0; }
-.nav-edit text { font-size: 28rpx; color: #4A9BD7; }
-.content { padding: 20rpx 30rpx; height: calc(100vh - 100rpx); }
-.report-head { background: #fff; border-radius: 16rpx; padding: 28rpx; margin-bottom: 20rpx; }
-.head-name { font-size: 34rpx; font-weight: 600; color: #333; }
-.head-meta { display: flex; align-items: center; margin-top: 12rpx; }
-.type-badge { font-size: 22rpx; color: #2E7D32; background: #E8F5E9; padding: 4rpx 16rpx; border-radius: 20rpx; margin-right: 16rpx; }
-.head-date { font-size: 24rpx; color: #999; }
-.empty-hint { text-align: center; color: #ccc; padding: 80rpx 0; font-size: 28rpx; }
-/* edit panel */
-.edit-panel { margin-top: 20rpx; }
-.edit-card { background: #fff; border-radius: 16rpx; padding: 24rpx; margin-bottom: 16rpx; }
-.edit-name { font-size: 28rpx; font-weight: 600; color: #333; display: block; margin-bottom: 12rpx; }
-.edit-row { display: flex; align-items: center; gap: 10rpx; }
-.edit-label { font-size: 26rpx; color: #666; }
-.edit-input { flex: 1; border: 1rpx solid #ddd; border-radius: 8rpx; padding: 8rpx 12rpx; font-size: 26rpx; }
-.edit-picker { font-size: 26rpx; color: #333; padding: 8rpx 16rpx; border: 1rpx solid #ddd; border-radius: 8rpx; }
-</style>
+.page {
+  min-height: 100vh;
+  background: #f8f8f8;
+}
+.content {
+  height: calc(100vh - 100rpx);
+  padding: 20rpx 30rpx;
+  box-sizing: border-box;
+}
+.report-head {
+  margin: 20rpx 0;
+  padding: 24rpx;
+  background: #fff;
+  border-radius: 16rpx;
+}
+.head-name {
+  font-size: 36rpx;
+  font-weight: 600;
+  color: #333;
+}
+.head-meta {
+  display: flex;
+  align-items: center;
+  gap: 12rpx;
+  margin-top: 10rpx;
+}
+.type-badge {
+  font-size: 22rpx;
+  color: #4a90d9;
+  background: #eef4ff;
+  padding: 4rpx 14rpx;
+  border-radius: 8rpx;
+}
+.head-date {
+  font-size: 24rpx;
+  color: #999;
+}
+.empty-hint {
+  text-align: center;
+  color: #999;
+  font-size: 26rpx;
+  padding: 60rpx 0;
+}
+.edit-btn {
+  margin: 30rpx 0;
+  display: flex;
+  justify-content: center;
+}
+.nav-edit {
+  background: #4a90d9;
+  color: #fff;
+  padding: 16rpx 60rpx;
+  border-radius: 40rpx;
+  font-size: 28rpx;
+}
+.title-count {
+  font-size: 24rpx;
+  font-weight: normal;
+  color: #94A3B8;
+  margin-left: 8rpx;
+}
+.title-abnormal {
+  font-size: 24rpx;
+  font-weight: normal;
+  color: #C62828;
+}
+.card-title-actions {
+  margin-left: auto;
+  display: flex;
+  flex-direction: row;
+  gap: 16rpx;
+}
+.expand-all-btn {
+  font-size: 22rpx;
+  color: #5B9BD5;
+  padding: 4rpx 12rpx;
+  border: 2rpx solid #5B9BD5;
+  border-radius: 20rpx;
+}
+.expand-all-btn:active { opacity: 0.7; }
+
+/* ===== 鎸囨爣鍒嗙粍鎶樺彔 ===== */
+.indicator-group {
+  border: 2rpx solid #E8EDF2;
+  border-radius: 12rpx;
+  margin-bottom: 12rpx;
+  overflow: hidden;
+}
+.indicator-group-header {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  padding: 20rpx 16rpx;
+  background: #F1F5F9;
+}
+.indicator-group-header:active { opacity: 0.8; }
+.indicator-group-header-left {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  flex: 1;
+  min-width: 0;
+}
+.indicator-group-arrow {
+  font-size: 30rpx;
+  color: #94A3B8;
+  margin-right: 12rpx;
+  transform: rotate(0deg);
+  transition: transform 0.2s ease;
+  flex-shrink: 0;
+}
+.indicator-group-arrow.arrow-open {
+  transform: rotate(90deg);
+}
+.indicator-group-name {
+  font-size: 26rpx;
+  color: #333;
+  font-weight: 600;
+  flex: 1;
+  min-width: 0;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+.indicator-group-header-right {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  gap: 12rpx;
+  flex-shrink: 0;
+  margin-left: 12rpx;
+}
+.indicator-group-need {
+  font-size: 20rpx;
+  color: #E65100;
+  background: #FFF3E0;
+  padding: 4rpx 12rpx;
+  border-radius: 20rpx;
+  font-weight: 500;
+}
+.indicator-group-count {
+  font-size: 22rpx;
+  color: #94A3B8;
+}
+.indicator-group-abnormal {
+  font-size: 22rpx;
+  color: #C62828;
+  font-weight: 500;
+  margin-right: 8rpx;
+}
+.indicator-group-body {
+  padding: 16rpx;
+}
+
+.field-row {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  margin-bottom: 16rpx;
+}
+.field-label {
+  font-size: 26rpx;
+  color: #666;
+  width: 160rpx;
+  flex-shrink: 0;
+}
+.field-value {
+  font-size: 26rpx;
+  color: #333;
+  font-weight: 500;
+  flex: 1;
+}
+.field-value.binding-required {
+  color: #E65100;
+}
+.member-select-row {
+  cursor: pointer;
+}
+.member-select-row:active {
+  opacity: 0.7;
+}
+.member-select-arrow {
+  font-size: 24rpx;
+  color: #5B9BD5;
+  margin-left: 12rpx;
+  flex-shrink: 0;
+}
+.member-panel {
+  background: #F8FAFB;
+  border-radius: 12rpx;
+  padding: 16rpx;
+  margin: 8rpx 0 16rpx;
+}
+.auto-create-inline {
+  margin-top: 16rpx;
+  padding-top: 16rpx;
+  border-top: 2rpx solid #E3F2FD;
+}
+.field-input {
+  flex: 1;
+  font-size: 26rpx;
+  color: #333;
+  padding: 8rpx 16rpx;
+  background: #F5F7FA;
+  border-radius: 8rpx;
+}
+.field-textarea {
+  width: 100%;
+  min-height: 120rpx;
+  font-size: 26rpx;
+  color: #333;
+  padding: 16rpx;
+  background: #F5F7FA;
+  border-radius: 12rpx;
+  box-sizing: border-box;
+  margin-top: 8rpx;
+}
+
+.member-list {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+  gap: 16rpx;
+}
+.member-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 20rpx 24rpx;
+  background: #F5F7FA;
+  border-radius: 16rpx;
+  border: 2rpx solid transparent;
+  min-width: 120rpx;
+}
+.member-selected {
+  background: #E3F2FD;
+  border-color: #5B9BD5;
+}
+.member-avatar {
+  width: 64rpx;
+  height: 64rpx;
+  border-radius: 50%;
+  background: #5B9BD5;
+  color: #fff;
+  font-size: 28rpx;
+  font-weight: bold;
+  text-align: center;
+  line-height: 64rpx;
+  margin-bottom: 8rpx;
+}
+.member-name { font-size: 24rpx; color: #333; }
+
+.indicator-item {
+  background: #F8FAFB;
+  border-radius: 12rpx;
+  padding: 16rpx;
+  margin-bottom: 12rpx;
+}
+.ind-header {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  gap: 12rpx;
+}
+.ind-name {
+  flex: 1;
+  font-size: 26rpx;
+  color: #333;
+  font-weight: 500;
+  padding: 4rpx 8rpx;
+  background: #fff;
+  border-radius: 6rpx;
+}
+.ind-value {
+  width: 160rpx;
+  font-size: 26rpx;
+  color: #1565C0;
+  font-weight: bold;
+  text-align: right;
+  padding: 4rpx 8rpx;
+  background: #fff;
+  border-radius: 6rpx;
+}
+.ind-detail {
+  display: flex;
+  flex-direction: row;
+  margin-top: 8rpx;
+  gap: 16rpx;
+}
+.ind-ref { font-size: 22rpx; color: #94A3B8; }
+.ind-unit { font-size: 22rpx; color: #64748B; }
+.ind-symptoms {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  margin-top: 8rpx;
+  gap: 8rpx;
+}
+.ind-symptom-label { font-size: 22rpx; color: #94A3B8; flex-shrink: 0; }
+.ind-symptom-input {
+  flex: 1;
+  font-size: 22rpx;
+  color: #333;
+  padding: 4rpx 8rpx;
+  background: #fff;
+  border-radius: 6rpx;
+}
+.status-tag {
+  display: inline-block;
+  font-size: 20rpx;
+  padding: 4rpx 12rpx;
+  border-radius: 8rpx;
+  font-weight: 500;
+}
+.status-normal { background: #E8F5E9; color: #2E7D32; }
+.status-high { background: #FFF3E0; color: #E65100; }
+.status-low { background: #E3F2FD; color: #1565C0; }
+.status-abnormal { background: #FFEBEE; color: #C62828; }
+
+/* ===== 鎸囨爣甯姪鎸夐挳 ===== */
+.ind-help-btn {
+  width: 36rpx;
+  height: 36rpx;
+  border-radius: 50%;
+  background: #E2E8F0;
+  color: #64748B;
+  font-size: 24rpx;
+  font-weight: bold;
+  text-align: center;
+  line-height: 36rpx;
+  flex-shrink: 0;
+}
+.ind-help-btn:active {
+  background: #CBD5E1;
+}
+
+/* ===== 鎸囨爣杈撳叆妗嗛鑹茬姸鎬?===== */
+.ind-name-normal, .ind-value-normal {
+  background: #fff;
+}
+.ind-name-high, .ind-value-high,
+.ind-name-low, .ind-value-low,
+.ind-name-abnormal, .ind-value-abnormal,
+.ind-name-deficient, .ind-value-deficient,
+.ind-name-insufficient, .ind-value-insufficient,
+.ind-name-warning, .ind-value-warning,
+.ind-name-high-risk, .ind-value-high-risk,
+.ind-name-low-risk, .ind-value-low-risk,
+.ind-name-not-detected, .ind-value-not-detected {
+  background: #FFF0F0;
+  color: #C62828;
+}
+
+/* ===== 鎸囨爣甯姪寮圭獥 ===== */
+.help-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0,0,0,0.4);
+  z-index: 999;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.help-popup {
+  width: 580rpx;
+  max-height: 70vh;
+  background: #fff;
+  border-radius: 24rpx;
+  overflow: hidden;
+}
+.help-popup-header {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  padding: 28rpx 30rpx;
+  border-bottom: 2rpx solid #F0F0F0;
+}
+.help-popup-title {
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #333;
+  flex: 1;
+}
+.help-popup-close {
+  font-size: 32rpx;
+  color: #999;
+  padding: 8rpx;
+}
+.help-popup-body {
+  padding: 24rpx 30rpx;
+}
+.help-row {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  margin-bottom: 16rpx;
+}
+.help-label {
+  font-size: 26rpx;
+  color: #94A3B8;
+  width: 140rpx;
+  flex-shrink: 0;
+}
+.help-value {
+  font-size: 28rpx;
+  color: #333;
+  font-weight: 500;
+  flex: 1;
+}
+.help-status-normal { color: #2E7D32; }
+.help-status-high { color: #C62828; }
+.help-status-low { color: #1565C0; }
+.help-status-abnormal { color: #C62828; }
+.help-desc {
+  margin-top: 8rpx;
+  padding: 20rpx;
+  background: #F8FAFB;
+  border-radius: 12rpx;
+}
+.help-desc-label {
+  display: block;
+  font-size: 24rpx;
+  color: #94A3B8;
+  margin-bottom: 8rpx;
+}
+.help-desc-text {
+  display: block;
+  font-size: 26rpx;
+  color: #333;
+  line-height: 1.6;
+}
+
+.flora-item {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  gap: 12rpx;
+  padding: 12rpx 16rpx;
+  background: #F8FAFB;
+  border-radius: 10rpx;
+  margin-bottom: 8rpx;
+}
+.flora-name {
+  flex: 1;
+  font-size: 26rpx;
+  color: #333;
+  padding: 4rpx 8rpx;
+  background: #fff;
+  border-radius: 6rpx;
+}
+.flora-abundance {
+  width: 140rpx;
+  font-size: 26rpx;
+  color: #1565C0;
+  text-align: right;
+  padding: 4rpx 8rpx;
+  background: #fff;
+  border-radius: 6rpx;
+}
+
+.taxonomy-group {
+  margin-bottom: 16rpx;
+}
+.taxonomy-group:last-child {
+  margin-bottom: 0;
+}
+.taxonomy-group-label {
+  display: block;
+  font-size: 24rpx;
+  font-weight: 500;
+  color: #666;
+  padding: 8rpx 4rpx 6rpx;
+}
+
+.pathogen-group {
+  margin-bottom: 16rpx;
+}
+.pathogen-group:last-child {
+  margin-bottom: 0;
+}
+.pathogen-group-label {
+  display: block;
+  font-size: 24rpx;
+  font-weight: 500;
+  color: #D32F2F;
+  padding: 8rpx 4rpx 6rpx;
+}
+
+.risk-item {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  gap: 12rpx;
+  padding: 12rpx 16rpx;
+  background: #F8FAFB;
+  border-radius: 10rpx;
+  margin-bottom: 8rpx;
+}
+.risk-name {
+  flex: 1;
+  font-size: 26rpx;
+  color: #333;
+  padding: 4rpx 8rpx;
+  background: #fff;
+  border-radius: 6rpx;
+}
+.risk-level {
+  font-size: 22rpx;
+  font-weight: 500;
+  padding: 4rpx 12rpx;
+  border-radius: 8rpx;
+}
+.risk-low { background: #E8F5E9; color: #2E7D32; }
+.risk-medium, .risk-moderate { background: #FFF3E0; color: #E65100; }
+.risk-high { background: #FFEBEE; color: #C62828; }
+
+.food-item {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  gap: 12rpx;
+  padding: 12rpx 16rpx;
+  background: #F8FAFB;
+  border-radius: 10rpx;
+  margin-bottom: 8rpx;
+}
+.food-name {
+  flex: 1;
+  font-size: 26rpx;
+  color: #333;
+  padding: 4rpx 8rpx;
+  background: #fff;
+  border-radius: 6rpx;
+}
+.food-category {
+  width: 160rpx;
+  font-size: 24rpx;
+  color: #64748B;
+  text-align: right;
+  padding: 4rpx 8rpx;
+  background: #fff;
+  border-radius: 6rpx;
+}
+
+.action-row {
+  display: flex;
+  flex-direction: row;
+  gap: 20rpx;
+  margin-top: 10rpx;
+  margin-bottom: 20rpx;
+}
+.btn-discard {
+  flex: 1;
+  background: #fff;
+  border: 2rpx solid #EF4444;
+  color: #EF4444;
+  font-size: 28rpx;
+  font-weight: bold;
+  text-align: center;
+  padding: 24rpx;
+  border-radius: 16rpx;
+}
+.btn-discard:active { opacity: 0.7; }
+.btn-confirm {
+  flex: 2;
+  background: linear-gradient(135deg, #5B9BD5, #3A7CC4);
+  color: #fff;
+  font-size: 28rpx;
+  font-weight: bold;
+  text-align: center;
+  padding: 24rpx;
+  border-radius: 16rpx;
+}
+.btn-confirm:active { opacity: 0.85; }
+.btn-disabled { opacity: 0.5; }
+
+.bottom-spacer { height: 80rpx; }
+
+/* ===== Phase 2: 鍖归厤鍙俊搴?===== */
+.confidence-card {
+  margin: 0 30rpx 20rpx;
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 24rpx;
+  box-shadow: 0 2rpx 6rpx rgba(0,0,0,0.04);
+}
+.confidence-header {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 16rpx;
+}
+.confidence-label {
+  font-size: 26rpx;
+  color: #333;
+  font-weight: 500;
+}
+.confidence-badge {
+  font-size: 22rpx;
+  font-weight: bold;
+  padding: 4rpx 16rpx;
+  border-radius: 20rpx;
+}
+.badge-high { background: #E8F5E9; color: #2E7D32; }
+.badge-medium { background: #FFF8E1; color: #F57F17; }
+.badge-low { background: #FFF3E0; color: #E65100; }
+.badge-none { background: #FFEBEE; color: #C62828; }
+
+.confidence-bar-track {
+  height: 12rpx;
+  background: #E2E8F0;
+  border-radius: 6rpx;
+  overflow: hidden;
+  margin-bottom: 12rpx;
+}
+.confidence-bar-fill {
+  height: 100%;
+  border-radius: 6rpx;
+  transition: width 0.3s ease;
+}
+.fill-high { background: linear-gradient(90deg, #66BB6A, #43A047); }
+.fill-medium { background: linear-gradient(90deg, #FFD54F, #FFC107); }
+.fill-low { background: linear-gradient(90deg, #FF8A65, #FF5722); }
+.fill-none { background: linear-gradient(90deg, #EF9A9A, #E53935); }
+
+.confidence-desc {
+  font-size: 22rpx;
+  color: #94A3B8;
+  line-height: 1.4;
+}
+
+/* ===== Phase 2: 鍊欓€夋垚鍛樺垪琛?===== */
+.candidate-section {
+  margin-bottom: 16rpx;
+}
+.candidate-section-hint {
+  display: block;
+  font-size: 24rpx;
+  color: #64748B;
+  margin-bottom: 12rpx;
+}
+.candidate-item {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  padding: 16rpx;
+  background: #F8FAFB;
+  border-radius: 12rpx;
+  margin-bottom: 10rpx;
+  border: 2rpx solid transparent;
+}
+.candidate-item:active { opacity: 0.8; }
+.candidate-selected {
+  background: #E3F2FD;
+  border-color: #5B9BD5;
+}
+.candidate-left {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  flex: 1;
+  min-width: 0;
+}
+.candidate-avatar {
+  width: 56rpx;
+  height: 56rpx;
+  border-radius: 50%;
+  background: #5B9BD5;
+  color: #fff;
+  font-size: 24rpx;
+  font-weight: bold;
+  text-align: center;
+  line-height: 56rpx;
+  margin-right: 16rpx;
+  flex-shrink: 0;
+}
+.candidate-info {
+  flex: 1;
+  min-width: 0;
+}
+.candidate-name {
+  font-size: 26rpx;
+  color: #333;
+  font-weight: 500;
+  display: block;
+}
+.candidate-detail {
+  font-size: 22rpx;
+  color: #94A3B8;
+  display: block;
+}
+.candidate-fields {
+  font-size: 20rpx;
+  color: #5B9BD5;
+  display: block;
+  margin-top: 2rpx;
+}
+.candidate-right {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  gap: 12rpx;
+  flex-shrink: 0;
+  margin-left: 16rpx;
+}
+.score-bar-track {
+  width: 80rpx;
+  height: 10rpx;
+  background: #E2E8F0;
+  border-radius: 5rpx;
+  overflow: hidden;
+}
+.score-bar-fill {
+  height: 100%;
+  background: linear-gradient(90deg, #5B9BD5, #3A7CC4);
+  border-radius: 5rpx;
+}
+.score-text {
+  font-size: 24rpx;
+  color: #1565C0;
+  font-weight: bold;
+  min-width: 48rpx;
+  text-align: right;
+}
+
+/* ===== Phase 2: 鏂板缓鎴愬憳鍏ュ彛 ===== */
+.create-member-entry {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: center;
+  padding: 20rpx;
+  border: 2rpx dashed #CBD5E1;
+  border-radius: 12rpx;
+  margin-top: 8rpx;
+}
+.create-member-entry:active { background: #F1F5F9; }
+.create-member-icon {
+  font-size: 28rpx;
+  color: #5B9BD5;
+  font-weight: bold;
+  margin-right: 8rpx;
+}
+.create-member-text {
+  font-size: 24rpx;
+  color: #5B9BD5;
+}
+
+/* ===== Phase 2: 鏂板缓鎴愬憳琛ㄥ崟锛堥潰鏉垮唴宓岋級 ===== */
+.form-item {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  margin-bottom: 20rpx;
+}
+.form-item-label {
+  font-size: 26rpx;
+  color: #333;
+  width: 100rpx;
+  flex-shrink: 0;
+}
+.form-item-input {
+  flex: 1;
+  font-size: 26rpx;
+  color: #333;
+  padding: 12rpx 16rpx;
+  background: #F5F7FA;
+  border-radius: 10rpx;
+}
+.gender-selector {
+  display: flex;
+  flex-direction: row;
+  gap: 16rpx;
+}
+.gender-option {
+  font-size: 26rpx;
+  color: #333;
+  padding: 12rpx 32rpx;
+  background: #F5F7FA;
+  border-radius: 10rpx;
+  border: 2rpx solid transparent;
+}
+.gender-option:active { opacity: 0.7; }
+.gender-selected {
+  background: #E3F2FD;
+  border-color: #5B9BD5;
+  color: #1565C0;
+}
+.btn-create-submit {
+  background: linear-gradient(135deg, #5B9BD5, #3A7CC4);
+  color: #fff;
+  font-size: 28rpx;
+  font-weight: bold;
+  text-align: center;
+  padding: 22rpx;
+  border-radius: 14rpx;
+  margin-top: 10rpx;
+}
+.btn-create-submit:active { opacity: 0.85; }
+
+/* ===== 鑸岃瘖鎸囨爣 ===== */
+.tongue-indicator-grid {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+  gap: 12rpx;
+}
+.tongue-ind-item {
+  width: calc(50% - 6rpx);
+  background: #F8FAFC;
+  border-radius: 16rpx;
+  padding: 20rpx 24rpx;
+  box-sizing: border-box;
+}
+.tongue-ind-label {
+  display: block;
+  font-size: 24rpx;
+  color: #94A3B8;
+  margin-bottom: 8rpx;
+}
+.tongue-ind-value {
+  display: block;
+  font-size: 28rpx;
+  color: #1E293B;
+  font-weight: 500;
+}
+.tongue-summary {
+  margin-top: 20rpx;
+  padding: 20rpx 24rpx;
+  background: #FFF8E1;
+  border-radius: 16rpx;
+}
+.tongue-summary-title {
+  font-size: 24rpx;
+  color: #F59E0B;
+  font-weight: 600;
+  display: block;
+  margin-bottom: 8rpx;
+}
+.tongue-summary-text {
+  font-size: 26rpx;
+  color: #333;
+.tip-banner,
+.parsing-banner,
+.confidence-card {
+  margin-left: 0;
+  margin-right: 0;
+}
+.form-section {
+  margin-left: 0;
+  margin-right: 0;
+}
+</style>

+ 1 - 1
cfc-frontend/pages/health/report-upload.vue

@@ -176,7 +176,7 @@ export default {
           return
         }
         // 只传 draftId,确认页自行解析
-        uni.redirectTo({ url: '/pages/health/report-confirm?draftId=' + draftId })
+        uni.redirectTo({ url: '/pages/health/report-detail?draftId=' + draftId })
       }).catch(function(err) {
         uni.hideLoading()
         self.uploading = false