2 Commits d1631c00d3 ... fa0c8af5cb

Author SHA1 Message Date
  jiapu fa0c8af5cb Merge remote-tracking branch 'origin/cfclub' into cfclub 2 weeks ago
  jiapu 58754e4d01 优化修复 2 weeks ago

+ 27 - 1
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -3244,6 +3244,32 @@ public class DatabaseInitializer implements CommandLineRunner {
             log.warn("迁移272失败: {}", e.getMessage());
         }
 
+        // 迁移273: dan_report_uploads.file_id 加宽(OSS 完整 URL 超过原 VARCHAR(100) 导致截断报错)
+        try {
+            jdbcTemplate.execute("ALTER TABLE dan_report_uploads MODIFY COLUMN file_id VARCHAR(500) COMMENT '媒资文件ID或相对路径'");
+            log.info("迁移273: 已加宽 dan_report_uploads.file_id 为 VARCHAR(500)");
+        } catch (Exception e) {
+            log.warn("迁移273失败: {}", e.getMessage());
+        }
+
+        // 迁移274: dan_assessment_results.report_file_id 加宽(确认入库写入 OSS URL 超长截断)
+        try {
+            jdbcTemplate.execute("ALTER TABLE dan_assessment_results MODIFY COLUMN report_file_id VARCHAR(500) COMMENT '上传文件媒资ID或相对路径'");
+            log.info("迁移274: 已加宽 dan_assessment_results.report_file_id 为 VARCHAR(500)");
+        } catch (Exception e) {
+            log.warn("迁移274失败: {}", e.getMessage());
+        }
+
+        // 迁移275: dan_assessment_results.dan_level 兼容旧库(NOT NULL 无默认导致家长上传确认失败)
+        ensureColumn("dan_assessment_results", "dan_level",
+                "VARCHAR(10) NULL DEFAULT NULL COMMENT 'DAN等级A/B/C/D'");
+        try {
+            jdbcTemplate.execute("ALTER TABLE dan_assessment_results MODIFY COLUMN dan_level VARCHAR(10) NULL DEFAULT NULL COMMENT 'DAN等级A/B/C/D'");
+            log.info("迁移275: 已将 dan_assessment_results.dan_level 改为可空并带默认");
+        } catch (Exception e) {
+            log.warn("迁移275失败: {}", e.getMessage());
+        }
+
     }
 
     private void migrateHealthDimensionsTable() {
@@ -7131,7 +7157,7 @@ public class DatabaseInitializer implements CommandLineRunner {
                     "dimension VARCHAR(16) NOT NULL COMMENT '维度: mind/wisdom', " +
                     "report_type VARCHAR(16) COMMENT 'A2/B4', " +
                     "file_url VARCHAR(500) NOT NULL COMMENT 'PDF文件URL', " +
-                    "file_id VARCHAR(100) COMMENT '媒资文件ID', " +
+                    "file_id VARCHAR(500) COMMENT '媒资文件ID或相对路径', " +
                     "file_name VARCHAR(200) COMMENT '原始文件名', " +
                     "file_size BIGINT COMMENT '文件大小(字节)', " +
                     "uploaded_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +

+ 12 - 3
cfc-backend/src/main/java/com/etotem/cfc/controller/DanAssessmentController.java

@@ -377,7 +377,7 @@ public class DanAssessmentController {
 
     @Operation(summary = "确认并保存 DAN 报告(Phase 2)— 已迁移到 dan_report_uploads 表")
     @PostMapping("/report/confirm")
-    public Result<String> confirmReport(@RequestBody Map<String, Object> body,
+    public Result<Map<String, Object>> confirmReport(@RequestBody Map<String, Object> body,
                                          @RequestAttribute("userId") Long userId) {
         Long draftId = ParamUtils.getLong(body.get("draftId"));
         if (draftId == null) {
@@ -385,9 +385,18 @@ public class DanAssessmentController {
         }
         String childName = (String) body.get("childName");
         String assessmentDateStr = (String) body.get("assessmentDate");
-        Result<?> r = danReportUploadService.confirmUpload(draftId, userId, childName, assessmentDateStr);
+        Result<com.etotem.cfc.dto.DanReportUploadVO> r =
+                danReportUploadService.confirmUpload(draftId, userId, childName, assessmentDateStr);
         if (r.getCode() != null && r.getCode() == 200) {
-            return Result.success("报告已确认并保存");
+            Map<String, Object> data = new java.util.LinkedHashMap<>();
+            data.put("message", "报告已确认并保存");
+            if (r.getData() != null) {
+                data.put("uploadId", r.getData().getId());
+                data.put("linkedResultId", r.getData().getLinkedResultId());
+                data.put("planId", r.getData().getPlanId());
+                data.put("dimension", r.getData().getDimension());
+            }
+            return Result.success(data);
         }
         return Result.error(r.getCode() == null ? 500 : r.getCode(), r.getMessage());
     }

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/family/FamilyMembersController.java

@@ -74,6 +74,8 @@ public Result<?> listMembers(@RequestBody(required = false) Map<String, Object>
  if (visibleOnly) {
    return Result.success(userService.getVisibleMembers(userId));
  }
+ // 与 visibleOnly/includeFamily 一致:无家庭时先兜底创建,避免空列表误伤前端选人
+ userService.ensureUserFamily(userId);
  List<FamilyMemberVO> members = familyMemberService.listMembers(userId);
  return Result.success(members);
 }

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/DanReportUploadVO.java

@@ -28,6 +28,8 @@ public class DanReportUploadVO {
     private Long confirmedBy;
     private Integer editCount;
     private Long linkedResultId;
+    /** 确认入库后自动生成的健康方案 ID(与健康报告 confirm 一致) */
+    private Long planId;
     /** 关联的测评结果(含各项分数,可为 null) */
     private Map<String, Object> result;
     private Date createdAt;

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/DanAssessmentResult.java

@@ -132,6 +132,8 @@ public class DanAssessmentResult implements Serializable {
     private String dimension; // mind(心-A2)/wisdom(智-B4)
     private String reportFileId; // 上传文件媒资ID
     private String draftStatus; // pending(待确认)/confirmed(已确认)
+    /** DAN 等级 A/B/C/D(旧库必填列;家长上传无分数时写 U) */
+    private String danLevel;
 
     // === 独立化扩展字段(迁移93新增) ===
     private Long sourceReportId; // 关联 dan_report_uploads.id

+ 152 - 6
cfc-backend/src/main/java/com/etotem/cfc/service/DanReportUploadService.java

@@ -5,16 +5,17 @@ import com.etotem.cfc.dto.DanReportEditDTO;
 import com.etotem.cfc.dto.DanReportUploadVO;
 import com.etotem.cfc.entity.DanAssessmentResult;
 import com.etotem.cfc.entity.DanReportUpload;
+import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.entity.UserIntent;
 import com.etotem.cfc.mapper.DanAssessmentResultMapper;
 import com.etotem.cfc.mapper.DanReportUploadMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.UserIntentMapper;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.web.multipart.MultipartFile;
@@ -63,6 +64,12 @@ public class DanReportUploadService {
     @Resource
     private NotificationDispatchService notificationDispatchService;
 
+    @Resource
+    private HealthPlanService healthPlanService;
+
+    @Resource
+    private FamilyMemberMapper familyMemberMapper;
+
     private final ObjectMapper objectMapper = new ObjectMapper();
 
     // Phase 1: 上传并预览 PDF
@@ -89,8 +96,12 @@ if (!DanReportUpload.DIM_MIND.equals(dimension) && !DanReportUpload.DIM_WISDOM.e
         upload.setFamilyMemberId(memberId);
         upload.setUploaderUserId(userId);
         upload.setDimension(dimension);
+        Long familyId = resolveFamilyId(memberId);
+        if (familyId != null) {
+            upload.setFamilyId(familyId);
+        }
         upload.setFileUrl(fileUrl);
-        upload.setFileId(fileUrl);  // 同义
+        upload.setFileId(toShortFileId(fileUrl));
         upload.setFileName(file.getOriginalFilename());
         upload.setFileSize(file.getSize());
         upload.setUploadedAt(new Date());
@@ -174,17 +185,36 @@ if (!DanReportUpload.DIM_MIND.equals(dimension) && !DanReportUpload.DIM_WISDOM.e
 
         // 创建数值结果记录
         DanAssessmentResult result = new DanAssessmentResult();
-        result.setFamilyMemberId(upload.getFamilyMemberId());
+        Long memberId = upload.getFamilyMemberId();
+        // child_id 为历史必填列,与 family_member_id 同义(均指向 family_members.id)
+        result.setChildId(memberId);
+        result.setFamilyMemberId(memberId);
         result.setTeacherId(upload.getUploaderUserId());
         result.setSource(DanAssessmentResult.SOURCE_PARENT_UPLOAD);
         result.setSourceReportId(upload.getId());
         result.setDimension(upload.getDimension());
-        result.setReportFileId(upload.getFileUrl());
+        result.setReportFileId(upload.getFileId() != null && !upload.getFileId().isEmpty()
+                ? upload.getFileId()
+                : toShortFileId(upload.getFileUrl()));
         result.setDraftStatus("confirmed");
         result.setAnalysisStatus("pending");
         result.setStatus("completed");
+        result.setDanLevel("U"); // 旧库 dan_level NOT NULL;无综合分时占位,有分数再覆盖
         result.setAssessmentDate(new Date());
-        result.setChildName(childName);
+        // 成员快照:优先入参姓名,否则取家庭成员昵称
+        FamilyMember member = memberId != null ? familyMemberMapper.selectById(memberId) : null;
+        String resolvedName = childName;
+        if ((resolvedName == null || resolvedName.isEmpty()) && member != null) {
+            resolvedName = member.getNickname();
+        }
+        result.setChildName(resolvedName);
+        if (member != null) {
+            result.setFamilyMemberName(member.getNickname());
+            result.setFamilyMemberBirthday(member.getBirthday());
+            result.setFamilyMemberGender(member.getGender());
+            result.setChildBirthday(member.getBirthday());
+            result.setChildGender(member.getGender());
+        }
         if (assessmentDateStr != null && !assessmentDateStr.isEmpty()) {
             try {
                 result.setAssessmentDate(new SimpleDateFormat("yyyy-MM-dd").parse(assessmentDateStr));
@@ -200,6 +230,12 @@ if (!DanReportUpload.DIM_MIND.equals(dimension) && !DanReportUpload.DIM_WISDOM.e
         result.setGrowthSuggestions(upload.getEditedSuggestions() != null
                 ? upload.getEditedSuggestions()
                 : upload.getParsedSuggestions());
+        // 尝试从解析项推断综合分并映射 DAN 等级
+        Integer overall = extractOverallScore(result.getStructuredAnalysis());
+        if (overall != null) {
+            result.setOverallScore(overall);
+            result.setDanLevel(mapScoreToDanLevel(overall));
+        }
         result.setCreatedAt(new Date());
         result.setUpdatedAt(new Date());
         resultMapper.insert(result);
@@ -237,7 +273,117 @@ if (!DanReportUpload.DIM_MIND.equals(dimension) && !DanReportUpload.DIM_WISDOM.e
                     upload.getFamilyMemberId(), result.getId(), upload.getDimension());
         } catch (Exception ignored) {}
 
-        return Result.success(DanReportUploadVO.fromEntity(upload));
+        // 与健康报告一致:确认入库后自动生成健康方案(draft,待用户在方案页点「确定」再分解任务)
+        DanReportUploadVO vo = DanReportUploadVO.fromEntity(upload);
+        Long planId = triggerAutoPlanAfterConfirm(upload);
+        if (planId != null) {
+            vo.setPlanId(planId);
+        }
+        return Result.success(vo);
+    }
+
+    /**
+     * DAN 确认后自动生成方案,维度按测评维度映射(mind→mind,wisdom→wisdom)。
+     * 失败仅记日志,不阻断确认主流程。
+     */
+    private Long triggerAutoPlanAfterConfirm(DanReportUpload upload) {
+        Long memberId = upload.getFamilyMemberId();
+        if (memberId == null) {
+            return null;
+        }
+        Long familyId = upload.getFamilyId();
+        if (familyId == null) {
+            familyId = resolveFamilyId(memberId);
+        }
+        if (familyId == null) {
+            log.warn("DAN确认后无法生成方案:缺少 familyId uploadId={}", upload.getId());
+            return null;
+        }
+        String planReportType = "dan_" + (upload.getDimension() != null ? upload.getDimension() : "mind");
+        try {
+            Long planId = healthPlanService.autoGenerateAndPublish(familyId, memberId, planReportType);
+            if (planId != null) {
+                log.info("DAN确认后自动生成方案 planId={}, uploadId={}, reportType={}",
+                        planId, upload.getId(), planReportType);
+            }
+            return planId;
+        } catch (Exception e) {
+            log.warn("DAN确认后自动生成方案失败 uploadId={}: {}", upload.getId(), e.getMessage());
+            return null;
+        }
+    }
+
+    private Long resolveFamilyId(Long memberId) {
+        if (memberId == null) {
+            return null;
+        }
+        FamilyMember member = familyMemberMapper.selectById(memberId);
+        return member != null ? member.getFamilyId() : null;
+    }
+
+    /**
+     * file_id 存相对路径或文件名,避免把完整 OSS URL 写入短字段导致 Data truncation。
+     * 完整地址放在 file_url。
+     */
+    private String toShortFileId(String fileUrl) {
+        if (fileUrl == null || fileUrl.isEmpty()) {
+            return null;
+        }
+        int uploadsIdx = fileUrl.indexOf("/uploads/");
+        if (uploadsIdx >= 0) {
+            return fileUrl.substring(uploadsIdx + 1);
+        }
+        int slash = fileUrl.lastIndexOf('/');
+        if (slash >= 0 && slash < fileUrl.length() - 1) {
+            return fileUrl.substring(slash + 1);
+        }
+        return fileUrl.length() > 500 ? fileUrl.substring(0, 500) : fileUrl;
+    }
+
+    /** 从 structured_analysis JSON 中尝试提取综合分(0-100) */
+    private Integer extractOverallScore(String structuredJson) {
+        if (structuredJson == null || structuredJson.isEmpty()) {
+            return null;
+        }
+        try {
+            java.util.List<java.util.Map<String, Object>> items = objectMapper.readValue(
+                    structuredJson, new TypeReference<java.util.List<java.util.Map<String, Object>>>() {});
+            if (items == null) {
+                return null;
+            }
+            for (java.util.Map<String, Object> item : items) {
+                if (item == null) continue;
+                String code = item.get("code") != null ? String.valueOf(item.get("code")) : "";
+                String name = item.get("name") != null ? String.valueOf(item.get("name")) : "";
+                String key = (code + name).toLowerCase();
+                if (key.contains("overall") || key.contains("综合") || key.contains("总分")
+                        || "total".equalsIgnoreCase(code)) {
+                    Object v = item.get("value");
+                    if (v == null) continue;
+                    String s = String.valueOf(v).replace("%", "").trim();
+                    try {
+                        double d = Double.parseDouble(s);
+                        int score = (int) Math.round(d);
+                        if (score > 100 && score <= 1000) {
+                            // 可能是百分位或放大分,截到 0-100
+                            score = Math.min(100, score);
+                        }
+                        return Math.max(0, Math.min(100, score));
+                    } catch (NumberFormatException ignored) {
+                    }
+                }
+            }
+        } catch (Exception e) {
+            log.debug("解析 structured_analysis 综合分失败: {}", e.getMessage());
+        }
+        return null;
+    }
+
+    private String mapScoreToDanLevel(int score) {
+        if (score >= 85) return "A";
+        if (score >= 70) return "B";
+        if (score >= 55) return "C";
+        return "D";
     }
 
     // 规划师完成分析

+ 34 - 1
cfc-backend/src/main/java/com/etotem/cfc/service/impl/HealthPlanServiceImpl.java

@@ -760,7 +760,7 @@ public class HealthPlanServiceImpl implements HealthPlanService {
             return null;
         }
         String memberIds = String.valueOf(memberId);
-        String dimensions = "body";
+        String dimensions = resolveDimensionsForReportType(reportType);
         String goal = buildGoalForReportType(reportType);
 
         String memberName = null;
@@ -801,9 +801,42 @@ public class HealthPlanServiceImpl implements HealthPlanService {
         if ("gut_flora".equals(reportType)) {
             return "基于菌群检测报告的个性化健康管理方案";
         }
+        if (reportType != null && reportType.startsWith("dan")) {
+            return "基于DAN测评报告的个性化成长方案";
+        }
+        if ("mind".equals(reportType) || "wisdom".equals(reportType)
+                || "cognition".equals(reportType) || "learning".equals(reportType)
+                || "behavior".equals(reportType) || "career".equals(reportType)
+                || "campus".equals(reportType) || "adolescent".equals(reportType)) {
+            return "基于DAN测评报告的个性化成长方案";
+        }
         return "基于健康检测报告的个性化健康管理方案";
     }
 
+    /** 按报告类型映射方案维度:菌群/体检→body,DAN心→mind,DAN智→wisdom */
+    private String resolveDimensionsForReportType(String reportType) {
+        if (reportType == null) {
+            return "body";
+        }
+        if ("gut_flora".equals(reportType) || "physical_exam".equals(reportType)
+                || "tongue".equals(reportType) || "bodyfat".equals(reportType)) {
+            return "body";
+        }
+        // dan_wisdom / wisdom / cognition / learning → 智维度
+        if ("dan_wisdom".equals(reportType) || "wisdom".equals(reportType)
+                || "cognition".equals(reportType) || "learning".equals(reportType)
+                || "career".equals(reportType) || "campus".equals(reportType)) {
+            return "wisdom";
+        }
+        // dan_mind / mind / behavior / adolescent → 心维度
+        if ("dan_mind".equals(reportType) || "mind".equals(reportType)
+                || "behavior".equals(reportType) || "adolescent".equals(reportType)
+                || "dan".equals(reportType)) {
+            return "mind";
+        }
+        return "body";
+    }
+
     private JsonNode callLangGraphPlanData(Long familyId, String memberIds, String dimensions, String goal) {
         if (langgraphBaseUrl == null || langgraphBaseUrl.trim().isEmpty()) {
             return null;

+ 3 - 2
cfc-backend/src/main/resources/schema.sql

@@ -457,8 +457,9 @@ CREATE TABLE IF NOT EXISTS dan_assessment_results (
     result_desc TEXT COMMENT '结果简评',
     source VARCHAR(20) DEFAULT 'planner_entry' COMMENT '报告来源: planner_entry/parent_upload/auto_fetch',
     dimension VARCHAR(10) DEFAULT 'mind' COMMENT '报告维度: mind(心-A2)/wisdom(智-B4)',
-    report_file_id VARCHAR(100) COMMENT '上传文件媒资ID',
+    report_file_id VARCHAR(500) COMMENT '上传文件媒资ID或相对路径',
     draft_status VARCHAR(10) DEFAULT 'pending' COMMENT '草稿状态: pending(待确认)/confirmed(已确认)',
+    dan_level VARCHAR(10) DEFAULT NULL COMMENT 'DAN等级A/B/C/D',
     INDEX idx_child_id (child_id),
     INDEX idx_teacher_id (teacher_id),
     INDEX idx_assessment_date (assessment_date)
@@ -3351,7 +3352,7 @@ CREATE TABLE IF NOT EXISTS dan_report_uploads (
     dimension            VARCHAR(16)  NOT NULL COMMENT '维度: mind(心-A2)/wisdom(智-B4)',
     report_type          VARCHAR(16)  COMMENT 'A2/B4 报告类型(解析后确认)',
     file_url             VARCHAR(500) NOT NULL COMMENT 'PDF 文件URL',
-    file_id              VARCHAR(100) COMMENT '媒资文件ID',
+    file_id              VARCHAR(500) COMMENT '媒资文件ID或相对路径',
     file_name            VARCHAR(200) COMMENT '原始文件名',
     file_size            BIGINT       COMMENT '文件大小(字节)',
     uploaded_at          DATETIME     DEFAULT CURRENT_TIMESTAMP COMMENT '上传时间',

+ 118 - 17
cfc-frontend/pages/dan-assessment/report-upload.vue

@@ -14,14 +14,11 @@
         <text class="dimension-label">报告类型</text>
         <view class="dimension-options">
           <view
-            :class="['dimension-option', dimension === 'mind' ? 'active' : '']"
-            @click="selectDimension('mind')">
-            <text class="dimension-name">&#x2764; 心维度 (A2)</text>
-          </view>
-          <view
-            :class="['dimension-option', dimension === 'wisdom' ? 'active' : '']"
-            @click="selectDimension('wisdom')">
-            <text class="dimension-name">&#x1F9E0; 智维度 (B4)</text>
+            v-for="item in dimensionOptions"
+            :key="item.code"
+            :class="['dimension-option', dimension === item.code ? 'active' : '']"
+            @click="selectDimension(item.code)">
+            <text class="dimension-name">{{ item.label }}</text>
           </view>
         </view>
       </view>
@@ -74,7 +71,7 @@
       <view class="preview-card" v-if="previewData">
         <view class="preview-row">
           <text class="preview-label">维度</text>
-          <text class="preview-value">{{ dimension === 'mind' ? '心维度 (A2)' : '智维度 (B4)' }}</text>
+          <text class="preview-value">{{ dimensionLabel }}</text>
         </view>
         <view class="preview-row">
           <text class="preview-label">解析概要</text>
@@ -122,10 +119,17 @@
         <text class="success-icon">&#x2705;</text>
         <text class="success-title">报告已保存</text>
         <text class="success-desc">DAN 测评数据已更新至五维能量体系</text>
+        <text class="success-desc" v-if="planId">成长方案已自动生成,请确认后分解为每日任务</text>
       </view>
       <view class="action-bar">
-        <view class="upload-btn" @click="goBack">
-          <text class="upload-btn-text">返回</text>
+        <view class="upload-btn" @click="goToPlan" v-if="planId">
+          <text class="upload-btn-text">查看成长方案</text>
+        </view>
+        <view class="retry-btn" @click="goToResult" v-if="linkedResultId">
+          <text class="retry-btn-text">查看测评结果</text>
+        </view>
+        <view class="retry-btn" @click="goBack">
+          <text class="retry-btn-text">返回</text>
         </view>
       </view>
     </view>
@@ -145,27 +149,90 @@ import config from '@/config.js'
 
 var REPORT_DOMAIN_MAP = {
   mind: 'mind',
-  wisdom: 'wisdom'
+  wisdom: 'wisdom',
+  cognition: 'wisdom',
+  learning: 'wisdom',
+  behavior: 'mind',
+  career: 'wisdom',
+  campus: 'wisdom',
+  adolescent: 'mind'
 }
 
+var DIMENSION_OPTIONS = [
+  { code: 'mind', label: '心维度 (A2)' },
+  { code: 'wisdom', label: '智维度 (B4)' },
+  { code: 'cognition', label: '认知能力' },
+  { code: 'learning', label: '学习力' },
+  { code: 'behavior', label: '行为' },
+  { code: 'career', label: '生涯' },
+  { code: 'campus', label: '校园' },
+  { code: 'adolescent', label: '青春期' }
+]
+
 export default {
   data() {
     return {
       step: 1,
       dimension: 'mind',
+      dimensionOptions: DIMENSION_OPTIONS,
       selectedFile: null,
       previewData: null,
       draftId: null,
+      planId: null,
+      linkedResultId: null,
       loading: false,
       confirming: false,
       recommendedProducts: [],
       loadingProducts: false
     }
   },
-  onLoad: function() {
+  computed: {
+    dimensionLabel: function() {
+      for (var i = 0; i < this.dimensionOptions.length; i++) {
+        if (this.dimensionOptions[i].code === this.dimension) {
+          return this.dimensionOptions[i].label
+        }
+      }
+      return this.dimension || 'DAN 测评'
+    }
+  },
+  onLoad: function(options) {
+    var validCodes = DIMENSION_OPTIONS.map(function(d) { return d.code })
+    if (options && options.dimension && validCodes.indexOf(options.dimension) >= 0) {
+      this.dimension = options.dimension
+    }
+    if (options && options.resume === '1') {
+      this.resumeFromUnifiedUpload()
+    }
     this.loadRecommendProducts(this.dimension)
   },
   methods: {
+    resumeFromUnifiedUpload: function() {
+      var cached = null
+      try {
+        cached = uni.getStorageSync('danUploadPreview')
+        uni.removeStorageSync('danUploadPreview')
+      } catch (e) {}
+      if (!cached || !cached.draftId) {
+        uni.showToast({ title: '预览数据失效,请重新上传', icon: 'none' })
+        return
+      }
+      if (cached.dimension) {
+        this.dimension = cached.dimension
+      }
+      this.draftId = cached.draftId
+      this.previewData = cached.previewData || {}
+      if (!this.previewData.items && this.previewData.itemsJson) {
+        try {
+          this.previewData.items = typeof this.previewData.itemsJson === 'string'
+            ? JSON.parse(this.previewData.itemsJson)
+            : this.previewData.itemsJson
+        } catch (e) {
+          this.previewData.items = []
+        }
+      }
+      this.step = 2
+    },
     goBack: function() {
       uni.navigateBack()
     },
@@ -236,7 +303,23 @@ export default {
         .then(function(res) {
           self.confirming = false
           if (res.code === 200) {
+            var data = res.data || {}
+            self.planId = data.planId || null
+            self.linkedResultId = data.linkedResultId || null
             self.step = 3
+            if (self.planId) {
+              uni.showModal({
+                title: '报告已入库',
+                content: '成长方案已自动生成,是否立即查看并确认任务?',
+                confirmText: '查看方案',
+                cancelText: '稍后',
+                success: function(modalRes) {
+                  if (modalRes.confirm) {
+                    self.goToPlan()
+                  }
+                }
+              })
+            }
           } else {
             uni.showToast({ title: res.message || '保存失败', icon: 'none' })
           }
@@ -246,11 +329,25 @@ export default {
           uni.showToast({ title: '保存失败', icon: 'none' })
         })
     },
+    goToPlan: function() {
+      if (!this.planId) return
+      uni.redirectTo({
+        url: '/pages/health/health-plan-summary?planId=' + this.planId
+      })
+    },
+    goToResult: function() {
+      if (!this.linkedResultId) return
+      uni.redirectTo({
+        url: '/pages/dan-assessment/report-result?reportId=' + this.linkedResultId
+      })
+    },
     resetUpload: function() {
       this.step = 1
       this.selectedFile = null
       this.previewData = null
       this.draftId = null
+      this.planId = null
+      this.linkedResultId = null
     },
     selectDimension: function(dim) {
       this.dimension = dim
@@ -336,22 +433,26 @@ export default {
 }
 .dimension-options {
   display: flex;
-  gap: 20rpx;
+  flex-direction: row;
+  flex-wrap: wrap;
 }
 .dimension-option {
-  flex: 1;
-  padding: 24rpx;
+  width: 48%;
+  margin-right: 2%;
+  margin-bottom: 16rpx;
+  padding: 20rpx 12rpx;
   border-radius: 12rpx;
   background: #F5F5F7;
   text-align: center;
   border: 2rpx solid transparent;
+  box-sizing: border-box;
 }
 .dimension-option.active {
   background: #EEF2FF;
   border-color: #6366F1;
 }
 .dimension-name {
-  font-size: 28rpx;
+  font-size: 24rpx;
   color: #333;
   font-weight: 500;
 }

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

@@ -649,10 +649,30 @@ report.gutType = s.gutType || ''
         self.confirming = false
         if (res.code === 200) {
           var reportId = res.data && res.data.reportId
-          if (reportId) {
-            uni.redirectTo({ url: '/pages/health/gut-flora-detail?reportId=' + reportId })
+          var planId = res.data && res.data.planId
+          var goDetail = function() {
+            if (reportId) {
+              uni.redirectTo({ url: '/pages/health/gut-flora-detail?reportId=' + reportId })
+            } else {
+              uni.navigateBack()
+            }
+          }
+          if (planId) {
+            uni.showModal({
+              title: '报告已入库',
+              content: '健康方案已自动生成,是否立即查看并确认任务?',
+              confirmText: '查看方案',
+              cancelText: '看报告',
+              success: function(modalRes) {
+                if (modalRes.confirm) {
+                  uni.redirectTo({ url: '/pages/health/health-plan-summary?planId=' + planId })
+                } else {
+                  goDetail()
+                }
+              }
+            })
           } else {
-            uni.navigateBack()
+            goDetail()
           }
         } else {
           uni.showToast({ title: res.message || '确认失败', icon: 'none' })

+ 106 - 136
cfc-frontend/pages/health/health-plan-summary.vue

@@ -16,25 +16,19 @@
         <text class="selected-count-badge" v-if="selectedMemberIds.length > 0">{{ selectedMemberIds.length }} 人已选</text>
       </view>
       <view class="hint-text">选择方案针对的家庭成员</view>
-      <view class="member-grid">
-        <view v-for="(m, idx) in executors" :key="getMemberKey(m, idx)"
-              class="member-tile" :class="{ active: isMemberSelected(m.id) }"
-              @click="toggleMember(m.id)">
-          <view class="member-tile-avatar">{{ (m.nickname || m.name || '成').charAt(0) }}</view>
-          <text class="member-tile-name">{{ m.nickname || m.name || '成员' }}</text>
-          <view class="member-tile-check" v-if="isMemberSelected(m.id)">✓</view>
-        </view>
-        <view v-if="executors.length === 0" class="empty-state">
-          <text class="empty-icon">👨‍👩‍👧</text>
-          <text class="empty-text">暂无家庭成员</text>
-        </view>
-      </view>
+      <member-chip-picker
+        ref="memberPicker"
+        :selected-ids="selectedMemberIds"
+        @change="onPickerChange"
+        @loaded="onPickerLoaded"
+        @add-member="onAddMember"
+      />
       <view class="section-title" style="margin-top:20rpx">健康目标</view>
       <view class="hint-text">描述您希望改善的方向,AI将为您定制方案</view>
       <textarea class="goal-textarea" v-model="userGoal" placeholder="例如:孩子最近睡眠不好,希望改善睡眠质量,同时提升免疫力..."
-                :maxlength="300" auto-height :disabled="loading"></textarea>
+                :maxlength="300" auto-height :disabled="loading" @input="onGoalInput"></textarea>
       <view class="goal-footer">
-        <text class="goal-count">{{ userGoal.length }}/300</text>
+        <text class="goal-count">{{ goalLen }}/300</text>
         <button class="btn-next" :disabled="!canGenerate" @click="goToStep2">{{ loading ? '加载中...' : '下一步' }}</button>
       </view>
     </view>
@@ -275,8 +269,9 @@
 </template>
 
 <script>
-import { getFamilyMemberList, generateHealthPlan, regenerateSection, confirmPlan, saveHealthPlan, getHealthPlanDetail, approvePlan } from '../../utils/api'
+import { generateHealthPlan, regenerateSection, confirmPlan, saveHealthPlan, getHealthPlanDetail, approvePlan } from '../../utils/api'
 import { getHealthStatus, saveHealthStatus } from '../../utils/api'
+import MemberChipPicker from '../../components/MemberChipPicker.vue'
 
 // v2: 预置疾病清单(菌群报告病症风险 ★ + 互联网统计常见病 两源合并去重,8 组 54 项)
 var DISEASE_GROUPS = [
@@ -353,15 +348,18 @@ var DISEASE_GROUPS = [
 ]
 
 export default {
+  components: { MemberChipPicker },
   data() {
     return {
       // 步骤控制
       step: 1,
-      // 成员选择
+      // 成员选择(字段名对齐 report-list,避免 executors 在部分基础库 setData 异常)
       selectedMemberIds: [],
-      executors: [],
+      pickerMembers: [],
+      needReloadMembers: false,
       // 目标
       userGoal: '',
+      goalLen: 0,
       // 健康问卷表单
       form: {
         heightCm: '',
@@ -371,7 +369,7 @@ export default {
         notes: ''
       },
       lipidCholesterol: '',
-      diseaseGroups: DISEASE_GROUPS,
+      // 疾病清单很大,不放进首屏 data,避免微信 setData 超限导致成员列表无法渲染
       expandedGroups: [],
       selectedDiseases: [],
       diseaseInput: '',
@@ -401,8 +399,12 @@ export default {
     }
   },
   computed: {
+    // 仅在问卷步骤暴露疾病清单,减小 step1 的 setData 体积
+    diseaseGroups: function() {
+      return this.step >= 2 ? DISEASE_GROUPS : []
+    },
     canGenerate: function() {
-      return this.selectedMemberIds.length > 0 && this.userGoal && this.userGoal.trim().length > 0 && !this.loading
+      return this.selectedMemberIds.length > 0 && this.goalLen > 0 && !this.loading
     },
     allConfirmed: function() {
       return this.sections.nutrition.confirmed && this.sections.diet.confirmed && this.sections.exercise.confirmed
@@ -439,9 +441,9 @@ selectedMembers: function() {
       var out = []
       this.selectedMemberIds.forEach(function(id) {
         var found = null
-        for (var i = 0; i < self.executors.length; i++) {
-          var mid = self.executors[i].memberId || self.executors[i].id
-          if (String(mid) === String(id)) { found = self.executors[i]; break }
+        for (var i = 0; i < self.pickerMembers.length; i++) {
+          var mid = self.pickerMembers[i].memberId || self.pickerMembers[i].id
+          if (String(mid) === String(id)) { found = self.pickerMembers[i]; break }
         }
         if (found) out.push(found)
       })
@@ -455,28 +457,21 @@ selectedMembers: function() {
         }
       }
       return '成员'
-    },
-    planTaskCompletedCount: function() {
-      var count = 0
-      for (var i = 0; i < this.planTasks.length; i++) {
-        if (this.planTasks[i] && this.planTasks[i].status === 'completed') count++
-      }
-      return count
     }
   },
   onLoad: function(options) {
     var self = this
     // 处理初始成员ID
     if (options.initialMemberIds) {
-      this.selectedMemberIds = options.initialMemberIds.split(',').filter(function(id) { return id }).map(function(id) { return parseInt(id) || id })
+      this.selectedMemberIds = options.initialMemberIds.split(',').filter(function(id) { return id }).map(function(id) { return String(parseInt(id) || id) })
+    } else if (options.memberId) {
+      this.selectedMemberIds = [String(options.memberId)]
     }
     // 处理 planId 查看模式
     this.planId = options.planId ? parseInt(options.planId) : null
     if (this.planId) {
       this.viewMode = true
       this.loadPlanDetail()
-    } else {
-      this.loadExecutors()
     }
     // 初始化健康问卷时间
     var now = new Date()
@@ -486,55 +481,44 @@ selectedMembers: function() {
     this.currentMonth = y + '-' + mStr
     var lastDay = new Date(y, m, 0).getDate()
     this.monthEnd = y + '-' + mStr + '-' + (lastDay < 10 ? '0' + lastDay : '' + lastDay)
-    // 注意:健康问卷数据由 loadExecutors 内按成员异步加载(含当前成员快照恢复),此处不重复调用
+    // 健康问卷数据在成员选择组件 loaded 后按成员异步加载
     // 自动执行
     if (options.autoExecute === '1') {
       setTimeout(function() { self.doAutoExecute() }, 1500)
     }
   },
   onShow: function() {
-    if (!this._executorsLoaded) {
-      this.loadExecutors()
+    if (this.needReloadMembers) {
+      this.needReloadMembers = false
+      if (this.$refs.memberPicker && this.$refs.memberPicker.reload) {
+        this.$refs.memberPicker.reload()
+      }
     }
   },
   methods: {
-    // ===== 成员选择 (保持原有) =====
-    getMemberKey: function(m, idx) { return m.id || idx },
-    loadExecutors: function() {
-      if (this._executorsLoading) return
-      this._executorsLoading = true
-      var self = this
-      getFamilyMemberList({ visibleOnly: true }).then(function(res) {
-        if (res && res.code === 200 && res.data) {
-          var list = res.data instanceof Array ? res.data : []
-          // 兼容双模型:多成员问卷用 memberId/name,方案打卡用 id
-          self.executors = list.filter(function(m) { return m.id }).map(function(m) {
-            var nm = m.nickname || m.name || '成员'
-            return { id: m.id, memberId: String(m.id), name: nm, nickname: nm }
-          })
-          if (self.selectedMemberIds.length === 0 && self.executors.length > 0) {
-            self.selectedMemberIds = [String(self.executors[0].id)]
-          }
-          // 默认当前填写成员为第一个选中成员
-          if (!self.activeFormMemberId || self.selectedMemberIds.indexOf(String(self.activeFormMemberId)) < 0) {
-            self.activeFormMemberId = self.selectedMemberIds[0] || ''
-          }
-          self.loadHealthForm(self.activeFormMemberId)
-        }
-        self._executorsLoaded = true
-        self._executorsLoading = false
-      }).catch(function() { self.executors = []; self._executorsLoaded = true; self._executorsLoading = false })
+    onGoalInput: function(e) {
+      var val = (e && e.detail && e.detail.value) || this.userGoal || ''
+      this.goalLen = String(val).length
     },
-    isMemberSelected: function(id) {
-      return this.selectedMemberIds.indexOf(String(id)) >= 0
+    onPickerChange: function(payload) {
+      if (!payload) return
+      this.selectedMemberIds = payload.selectedIds || []
+      this.pickerMembers = payload.members || []
     },
-    toggleMember: function(id) {
-      var strId = String(id)
-      var idx = this.selectedMemberIds.indexOf(strId)
-      if (idx >= 0) this.selectedMemberIds.splice(idx, 1)
-      else this.selectedMemberIds.push(strId)
+    onPickerLoaded: function(payload) {
+      if (!payload) return
+      this.selectedMemberIds = payload.selectedIds || []
+      this.pickerMembers = payload.members || []
+      var formId = (payload.selectedIds && payload.selectedIds[0]) || ''
+      if (formId) {
+        this.activeFormMemberId = String(formId)
+        var self = this
+        setTimeout(function() { self.loadHealthForm(formId) }, 100)
+      }
+    },
+    onAddMember: function() {
+      this.needReloadMembers = true
     },
-
     // ===== 时间格式化 =====
     formatTime: function(iso) {
       if (!iso) return ''
@@ -545,6 +529,9 @@ selectedMembers: function() {
 
     // ===== 步骤导航 =====
     goToStep2: function() {
+      if (this.expandedGroups.length === 0) {
+        this.expandedGroups = DISEASE_GROUPS.slice(0, 3).map(function(g) { return g.key })
+      }
       this.step = 2
     },
     skipToGenerate: function() {
@@ -681,10 +668,10 @@ selectedMembers: function() {
       else this.expandedGroups.push(key)
     },
     findGroupKey: function(name) {
-      for (var i = 0; i < this.diseaseGroups.length; i++) {
-        var items = this.diseaseGroups[i].items
+      for (var i = 0; i < DISEASE_GROUPS.length; i++) {
+        var items = DISEASE_GROUPS[i].items
         for (var j = 0; j < items.length; j++) {
-          if (items[j].name === name) return this.diseaseGroups[i].key
+          if (items[j].name === name) return DISEASE_GROUPS[i].key
         }
       }
       return ''
@@ -1017,9 +1004,9 @@ selectedMembers: function() {
       getHealthPlanDetail({ planId: self.planId }).then(function(res) {
         if (res && res.code === 200 && res.data) {
           self.planDetail = res.data
-          self.userGoal = res.data.goal || ''
+          self.userGoal = res.data.goal != null ? String(res.data.goal) : ''
           if (res.data.memberIds) {
-            self.selectedMemberIds = res.data.memberIds.split(',').filter(function(id) { return id }).map(function(id) { return parseInt(id) || id })
+            self.selectedMemberIds = res.data.memberIds.split(',').filter(function(id) { return id }).map(function(id) { return String(parseInt(id) || id) })
           }
           try {
             var pj = JSON.parse(res.data.planJson || '{}')
@@ -1150,10 +1137,14 @@ selectedMembers: function() {
       this.selectedDiseases = []
       this.diseaseInput = ''
       this.medRows = [{ name: '', startDate: '', durationText: '', time: '' }]
-      this.expandedGroups = this.diseaseGroups.slice(0, 3).map(function(g) { return g.key })
+      this.expandedGroups = DISEASE_GROUPS.slice(0, 3).map(function(g) { return g.key })
       this.memberFormMap = {}
       this.activeFormMemberId = ''
-      this.loadExecutors()
+      this.pickerMembers = []
+      this.goalLen = 0
+      if (this.$refs.memberPicker && this.$refs.memberPicker.reload) {
+        this.$refs.memberPicker.reload()
+      }
     },
     goBack: function() {
       uni.navigateBack()
@@ -1240,74 +1231,39 @@ selectedMembers: function() {
   margin-bottom: 20rpx;
 }
 
-/* 成员卡片 */
-.member-grid {
-  display: flex;
-  flex-wrap: wrap;
-  gap: 20rpx;
-  margin-bottom: 24rpx;
+/* 成员选择 chips(与 report-list 同模式) */
+.member-scroll {
+  width: 100%;
+  white-space: nowrap;
+  margin-bottom: 16rpx;
 }
-.member-tile {
-  position: relative;
-  width: calc(33.333% - 14rpx);
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  padding: 28rpx 12rpx 20rpx;
-  border-radius: 20rpx;
-  border: 3rpx solid #e8e8e8;
-  background: #FAFAFA;
-  transition: all 0.2s;
+.member-chips {
+  display: inline-flex;
+  flex-direction: row;
+  padding: 4rpx 0;
 }
-.member-tile.active {
-  border-color: #F97316;
-  background: #FFFBF0;
-  box-shadow: 0 4rpx 16rpx rgba(249, 115, 22, 0.15);
-}
-.member-tile-avatar {
-  width: 96rpx;
-  height: 96rpx;
-  border-radius: 50%;
-  background: #ececec;
-  border: 3rpx solid #ddd;
-  color: #888;
-  font-size: 36rpx;
-  font-weight: bold;
-  display: flex;
+.member-chip {
+  display: inline-flex;
   align-items: center;
   justify-content: center;
-  margin-bottom: 12rpx;
+  padding: 12rpx 28rpx;
+  margin-right: 16rpx;
+  border-radius: 32rpx;
+  background: #F1F5F9;
+  border: 2rpx solid transparent;
 }
-.member-tile.active .member-tile-avatar {
-  background: #F97316;
+.member-chip-on {
+  background: #FFF7ED;
   border-color: #F97316;
-  color: #fff;
 }
-.member-tile-name {
+.member-chip-text {
   font-size: 26rpx;
+  color: #334155;
   font-weight: 600;
-  color: #333;
-  max-width: 100%;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
 }
-.member-tile.active .member-tile-name {
+.member-chip-on .member-chip-text {
   color: #F97316;
 }
-.member-tile-check {
-  position: absolute;
-  top: 8rpx;
-  right: 8rpx;
-  width: 36rpx;
-  height: 36rpx;
-  border-radius: 50%;
-  background: #F97316;
-  color: #fff;
-  font-size: 20rpx;
-  line-height: 36rpx;
-  text-align: center;
-}
 
 /* 目标输入 */
 .goal-textarea {
@@ -1696,11 +1652,25 @@ selectedMembers: function() {
 
 /* 空状态 */
 .empty-state {
+  width: 100%;
   display: flex;
   flex-direction: column;
   align-items: center;
-  padding: 60rpx 0;
+  padding: 40rpx 0 24rpx;
+  box-sizing: border-box;
+}
+.empty-text { font-size: 26rpx; color: #999; }
+.member-load-hint {
+  font-size: 22rpx;
+  color: #F97316;
+  margin-bottom: 12rpx;
+}
+.empty-action {
+  margin-top: 20rpx;
+  padding: 12rpx 32rpx;
+  font-size: 26rpx;
+  color: #F97316;
+  border: 2rpx solid #F97316;
+  border-radius: 32rpx;
 }
-.empty-icon { font-size: 80rpx; margin-bottom: 16rpx; }
-.empty-text { font-size: 26rpx; color: #bbb; }
 </style>

+ 33 - 13
cfc-frontend/pages/health/report-confirm.vue

@@ -822,20 +822,40 @@ export default {
         if (res.code === 200) {
           var data = res.data || {}
           var reportId = data.reportId || (data.report && data.report.id)
-           if (reportId) {
-            uni.showToast({ title: '入库成功', icon: 'success' })
-            setTimeout(function() {
-              var reportType = data.reportType || ''
-              var gotoUrl
-              if (reportType === 'gut_flora') {
-                gotoUrl = '/pages/health/gut-flora-detail?reportId=' + reportId
-              } else if (reportType === 'dan') {
-                gotoUrl = '/pages/dan-assessment/report-result?reportId=' + reportId
-              } else {
-                gotoUrl = '/pages/health/report-detail?reportType=' + reportType + '&reportId=' + reportId
+          var planId = data.planId
+          var goReport = function() {
+            if (!reportId) {
+              uni.navigateBack()
+              return
+            }
+            var reportType = data.reportType || ''
+            var gotoUrl
+            if (reportType === 'gut_flora') {
+              gotoUrl = '/pages/health/gut-flora-detail?reportId=' + reportId
+            } else if (reportType === 'dan') {
+              gotoUrl = '/pages/dan-assessment/report-result?reportId=' + reportId
+            } else {
+              gotoUrl = '/pages/health/report-detail?reportType=' + reportType + '&reportId=' + reportId
+            }
+            uni.redirectTo({ url: gotoUrl })
+          }
+          if (reportId && planId) {
+            uni.showModal({
+              title: '报告已入库',
+              content: '方案已自动生成,是否立即查看并确认任务?',
+              confirmText: '查看方案',
+              cancelText: '看报告',
+              success: function(modalRes) {
+                if (modalRes.confirm) {
+                  uni.redirectTo({ url: '/pages/health/health-plan-summary?planId=' + planId })
+                } else {
+                  goReport()
+                }
               }
-              uni.redirectTo({ url: gotoUrl })
-            }, 500)
+            })
+          } else if (reportId) {
+            uni.showToast({ title: '入库成功', icon: 'success' })
+            setTimeout(goReport, 500)
           } else {
             uni.navigateBack()
           }

+ 448 - 180
cfc-frontend/pages/health/report-upload.vue

@@ -3,23 +3,68 @@
     <!-- 说明提示 -->
     <view class="tip-banner">
       <text class="tip-icon">📋</text>
-      <text class="tip-text">上传体检/菌群报告、认知测评、心理测评或情绪打卡记录,为您生成个性化成长方案</text>
+      <text class="tip-text">{{ tipText }}</text>
     </view>
 
     <!-- 表单 -->
     <view class="form-section">
-      <!-- 上传方式:并排卡片 -->
-      <view class="upload-methods">
+      <!-- 步骤1:报告大类 -->
+      <view class="form-item category-box">
+        <text class="form-label">选择报告类型</text>
+        <view class="category-row">
+          <view
+            :class="['category-card', reportKind === 'health' ? 'category-active' : '']"
+            @click="selectReportKind('health')">
+            <text class="category-icon">🏥</text>
+            <text class="category-title">健康报告</text>
+            <text class="category-desc">体检 / 菌群 / 舌诊 / 体脂</text>
+          </view>
+          <view
+            :class="['category-card', reportKind === 'dan' ? 'category-active category-dan' : '']"
+            @click="selectReportKind('dan')">
+            <text class="category-icon">🧠</text>
+            <text class="category-title">DAN测评</text>
+            <text class="category-desc">A2 / B4 等成长测评</text>
+          </view>
+        </view>
+      </view>
+
+      <!-- 步骤2:DAN 维度(仅 DAN) -->
+      <view class="form-item" v-if="reportKind === 'dan'">
+        <text class="form-label">选择 DAN 测评维度</text>
+        <view class="dim-grid">
+          <view
+            v-for="item in danDimensions"
+            :key="item.code"
+            :class="['dim-chip', danDimension === item.code ? 'dim-active' : '']"
+            @click="selectDanDimension(item.code)">
+            <text class="dim-chip-text">{{ item.label }}</text>
+          </view>
+        </view>
+      </view>
+
+      <!-- 上传方式 -->
+      <view class="upload-methods" v-if="reportKind">
         <view class="upload-card card-file" @click="pickFile">
           <view class="upload-card-icon">📄</view>
           <text class="upload-card-title">从聊天选择文件</text>
-          <text class="upload-card-desc">PDF / Word / 图片<br/>体检报告、测评报告、情绪打卡</text>
+          <text class="upload-card-desc" v-if="reportKind === 'health'">PDF / 图片<br/>体检、菌群等健康报告</text>
+          <text class="upload-card-desc" v-else>PDF<br/>DAN 测评报告(推荐)</text>
         </view>
-        <view class="upload-card card-photo" @click="takePhoto">
+        <view class="upload-card card-photo" @click="takePhoto" v-if="reportKind === 'health'">
           <view class="upload-card-icon">📷</view>
           <text class="upload-card-title">拍照上传</text>
           <text class="upload-card-desc">拍摄舌象照片<br/>或单页纸质报告</text>
         </view>
+        <view class="upload-card card-photo" @click="goBodyFatScale" v-if="reportKind === 'health'">
+          <view class="upload-card-icon">⚖️</view>
+          <text class="upload-card-title">体脂秤</text>
+          <text class="upload-card-desc">拍摄体脂秤<br/>显示屏照片</text>
+        </view>
+      </view>
+
+      <view class="kind-hint" v-if="!reportKind">
+        <text class="kind-hint-text">请先选择「健康报告」或「DAN测评」,再上传文件</text>
       </view>
 
       <!-- 责任声明 -->
@@ -39,18 +84,18 @@
       <view class="form-item" v-if="uploading">
         <text class="form-label">上传中...</text>
         <view class="analyzing-status">
-          <text class="analyzing-text">正在上传文件,即将进入分析页面...</text>
+          <text class="analyzing-text">{{ uploadingText }}</text>
         </view>
       </view>
     </view>
 
-    <!-- 上传说明 -->
+    <!-- 上传说明:随类型切换 -->
     <view class="guide-section">
       <view class="guide-header">
         <text class="guide-header-icon">📖</text>
-        <text class="guide-header-title">可上传的报告类型</text>
+        <text class="guide-header-title">{{ reportKind === 'dan' ? 'DAN 测评说明' : '健康报告说明' }}</text>
       </view>
-      <view class="guide-list">
+      <view class="guide-list" v-if="reportKind !== 'dan'">
         <view class="guide-item">
           <view class="guide-icon-box guide-icon-blue"><text class="guide-icon">📄</text></view>
           <view class="guide-info">
@@ -73,46 +118,78 @@
           </view>
         </view>
         <view class="guide-item">
-          <view class="guide-icon-box guide-icon-purple"><text class="guide-icon">🧠</text></view>
+          <view class="guide-icon-box guide-icon-teal"><text class="guide-icon">⚖️</text></view>
           <view class="guide-info">
-            <text class="guide-name">认知能力测评报告</text>
-            <text class="guide-desc">韦氏、瑞文等标准化认知测评,以及双培强基工程DAN认知能力测评,AI 解读认知结构与发展建议</text>
+            <text class="guide-name">体脂秤报告</text>
+            <text class="guide-desc">拍摄体脂秤显示屏,自动提取体重、BMI、体脂率等</text>
           </view>
         </view>
+      </view>
+      <view class="guide-list" v-else>
         <view class="guide-item">
-          <view class="guide-icon-box guide-icon-teal"><text class="guide-icon">💭</text></view>
+          <view class="guide-icon-box guide-icon-teal"><text class="guide-icon">💖</text></view>
           <view class="guide-info">
-            <text class="guide-name">心理测评报告</text>
-            <text class="guide-desc">气质类型、情绪智力、人格特征等心理测评,以及双培强基工程DAN人格、情绪、自我概念和自驱力测评,辅助家庭教育决策</text>
+            <text class="guide-name">心维度 A2</text>
+            <text class="guide-desc">人格、情绪、自我概念与自驱力等心理测评报告</text>
           </view>
         </view>
         <view class="guide-item">
-          <view class="guide-icon-box guide-icon-pink"><text class="guide-icon">📝</text></view>
+          <view class="guide-icon-box guide-icon-purple"><text class="guide-icon">🧠</text></view>
           <view class="guide-info">
-            <text class="guide-name">情绪打卡记录</text>
-            <text class="guide-desc">手机端情绪打卡截图或记录文件,追踪情绪变化趋势</text>
+            <text class="guide-name">智维度 B4</text>
+            <text class="guide-desc">认知能力、学习力等双培强基 DAN 智维度报告</text>
           </view>
         </view>
-        <view class="guide-item" @click="goBodyFatScale">
-          <view class="guide-icon-box guide-icon-teal"><text class="guide-icon">⚖️</text></view>
+        <view class="guide-item">
+          <view class="guide-icon-box guide-icon-blue"><text class="guide-icon">📚</text></view>
           <view class="guide-info">
-            <text class="guide-name">体脂秤报告</text>
-            <text class="guide-desc">拍摄体脂秤显示屏照片,AI 自动提取体重、BMI、体脂率等指标</text>
+            <text class="guide-name">其他 DAN 维度</text>
+            <text class="guide-desc">认知 / 学习 / 行为 / 生涯 / 校园 / 青春期等专项报告</text>
           </view>
         </view>
       </view>
       <view class="guide-tips">
         <text class="guide-tips-title">温馨提示</text>
-        <text class="guide-tips-item">· 单次上传 1 个文件,支持 PDF / JPG / PNG 格式</text>
-        <text class="guide-tips-item">· 拍照时请保持光线充足,确保报告内容完整清晰</text>
-        <text class="guide-tips-item">· 上传后由 AI 自动解析,解析结果可在线编辑确认</text>
+        <text class="guide-tips-item">· 请先选择报告类型,再选择文件上传</text>
+        <text class="guide-tips-item">· 健康报告走菌群/体检解析;DAN 走测评维度解析</text>
+        <text class="guide-tips-item">· 确认入库后将自动生成成长方案,可再分解为每日任务</text>
+      </view>
+    </view>
+
+    <!-- 点击上传后的类型选择弹层(未预选时兜底) -->
+    <view class="sheet-mask" v-if="showKindSheet" @click="closeKindSheet">
+      <view class="sheet-panel" @click.stop>
+        <text class="sheet-title">请选择要上传的报告</text>
+        <view class="sheet-item" @click="onKindSheetPick('health')">
+          <text class="sheet-item-title">健康报告</text>
+          <text class="sheet-item-desc">体检、肠道菌群、舌诊、体脂等</text>
+        </view>
+        <view class="sheet-item" @click="onKindSheetPick('dan')">
+          <text class="sheet-item-title">DAN测评</text>
+          <text class="sheet-item-desc">A2 / B4 等成长测评 PDF</text>
+        </view>
+        <view class="sheet-cancel" @click="closeKindSheet">
+          <text class="sheet-cancel-text">取消</text>
+        </view>
       </view>
     </view>
   </view>
 </template>
 
 <script>
-import { uploadReportOnly } from '../../utils/api.js'
+import { uploadReportOnly, danParsePreview } from '../../utils/api.js'
+import config from '@/config.js'
+
+var DAN_DIMENSIONS = [
+  { code: 'mind', label: '心 A2' },
+  { code: 'wisdom', label: '智 B4' },
+  { code: 'cognition', label: '认知' },
+  { code: 'learning', label: '学习' },
+  { code: 'behavior', label: '行为' },
+  { code: 'career', label: '生涯' },
+  { code: 'campus', label: '校园' },
+  { code: 'adolescent', label: '青春期' }
+]
 
 export default {
   data() {
@@ -123,11 +200,42 @@ export default {
       photoPath: null,
       agreedToTerms: false,
       currentMemberId: null,
-      extractedMetrics: []
+      extractedMetrics: [],
+      reportKind: '',
+      danDimension: 'mind',
+      danDimensions: DAN_DIMENSIONS,
+      showKindSheet: false,
+      pendingIsPhoto: false
+    }
+  },
+  computed: {
+    tipText: function() {
+      if (this.reportKind === 'dan') {
+        return '上传 DAN 测评 PDF(A2/B4 等),解析确认后生成个性化成长方案'
+      }
+      if (this.reportKind === 'health') {
+        return '上传体检/菌群/舌诊等健康报告,解析确认后生成个性化健康方案'
+      }
+      return '请先选择「健康报告」或「DAN测评」,再上传文件生成方案'
+    },
+    uploadingText: function() {
+      return this.reportKind === 'dan'
+        ? '正在解析 DAN 测评报告...'
+        : '正在上传文件,即将进入分析页面...'
     }
   },
   onLoad: function(options) {
     this.memberId = options.memberId ? parseInt(options.memberId) : null
+    if (options.reportKind === 'health' || options.reportKind === 'dan') {
+      this.reportKind = options.reportKind
+    }
+    if (options.dimension) {
+      var codes = DAN_DIMENSIONS.map(function(d) { return d.code })
+      if (codes.indexOf(options.dimension) >= 0) {
+        this.danDimension = options.dimension
+        this.reportKind = 'dan'
+      }
+    }
     if (options.bodyFatMetrics) {
       try {
         this.extractedMetrics = JSON.parse(decodeURIComponent(options.bodyFatMetrics))
@@ -135,37 +243,92 @@ export default {
     }
   },
   methods: {
-    // 切换同意声明
-    toggleAgree() {
+    selectReportKind: function(kind) {
+      this.reportKind = kind
+      if (kind === 'dan' && !this.danDimension) {
+        this.danDimension = 'mind'
+      }
+    },
+    selectDanDimension: function(code) {
+      this.danDimension = code
+    },
+    toggleAgree: function() {
       this.agreedToTerms = !this.agreedToTerms
     },
-    // 未勾选声明时弹窗确认
-    ensureAgreed(isPhoto) {
+    ensureAgreed: function(callback) {
       if (this.agreedToTerms) {
-        this.startPick(isPhoto)
+        callback()
         return
       }
+      var self = this
       uni.showModal({
         title: '数据使用声明',
         content: '上传报告即代表您同意平台使用本报告数据进行分析,为您提供个性化健康解决方案;未经您授权,平台不会以任何形式将您的报告内容提供给其他任何个人或机构。点击「同意继续」即表示您已阅读并同意。',
         confirmText: '同意继续',
         cancelText: '暂不',
-        success: (res) => {
+        success: function(res) {
           if (res.confirm) {
-            this.agreedToTerms = true
-            this.startPick(isPhoto)
+            self.agreedToTerms = true
+            callback()
           }
         }
       })
     },
-    startPick(isPhoto) {
+    // 点击上传:未选类型时弹层;已选则直接走对应链路
+    pickFile: function() {
+      var self = this
+      this.ensureAgreed(function() {
+        self.beginUpload(false)
+      })
+    },
+    takePhoto: function() {
+      var self = this
+      this.ensureAgreed(function() {
+        self.beginUpload(true)
+      })
+    },
+    beginUpload: function(isPhoto) {
+      if (!this.reportKind) {
+        this.pendingIsPhoto = isPhoto
+        this.showKindSheet = true
+        return
+      }
+      if (this.reportKind === 'dan') {
+        if (!this.danDimension) {
+          uni.showToast({ title: '请选择 DAN 测评维度', icon: 'none' })
+          return
+        }
+        this.startPickDan()
+        return
+      }
+      this.startPickHealth(isPhoto)
+    },
+    closeKindSheet: function() {
+      this.showKindSheet = false
+    },
+    onKindSheetPick: function(kind) {
+      this.reportKind = kind
+      this.showKindSheet = false
+      var isPhoto = this.pendingIsPhoto
+      this.pendingIsPhoto = false
+      if (kind === 'dan') {
+        if (isPhoto) {
+          uni.showToast({ title: 'DAN 请上传 PDF 文件', icon: 'none' })
+        }
+        this.startPickDan()
+        return
+      }
+      this.startPickHealth(isPhoto)
+    },
+    startPickHealth: function(isPhoto) {
+      var self = this
       if (isPhoto) {
         uni.chooseImage({
           count: 1,
           sourceType: ['camera', 'album'],
-          success: (res) => {
-            this.photoPath = res.tempFilePaths[0]
-            this.uploadAndGoConfirm(this.photoPath, 'image')
+          success: function(res) {
+            self.photoPath = res.tempFilePaths[0]
+            self.uploadHealthAndGoConfirm(self.photoPath, 'image')
           }
         })
       } else {
@@ -173,98 +336,114 @@ export default {
           count: 1,
           type: 'file',
           extension: ['pdf'],
-          success: (res) => {
+          success: function(res) {
             var files = res.tempFiles
             if (files && files.length > 0) {
-              this.selectedFile = files[0]
-              this.uploadAndGoConfirm(files[0].path, '')
+              self.selectedFile = files[0]
+              self.uploadHealthAndGoConfirm(files[0].path, '')
             }
           },
-          fail: (e) => {
+          fail: function(e) {
             console.log('选择文件取消或失败', e)
           }
         })
       }
     },
-    // ===== 拍照上传 =====
-    takePhoto() {
-      if (this.agreedToTerms) {
-        this.startPick(true)
-        return
-      }
-      uni.showModal({
-        title: '数据使用声明',
-        content: '上传报告即代表您同意平台使用本报告数据进行分析,为您提供个性化健康解决方案;未经您授权,平台不会以任何形式将您的报告内容提供给其他任何个人或机构。点击「同意继续」即表示您已阅读并同意。',
-        confirmText: '同意继续',
-        cancelText: '暂不',
-        success: (res) => {
-          if (res.confirm) {
-            this.agreedToTerms = true
-            this.startPick(true)
-          }
-        }
-      })
-    },
-    // ===== 文件上传 =====
-    pickFile() {
-      if (this.agreedToTerms) {
-        this.startPick(false)
-        return
-      }
-      uni.showModal({
-        title: '数据使用声明',
-        content: '上传报告即代表您同意平台使用本报告数据进行分析,为您提供个性化健康解决方案;未经您授权,平台不会以任何形式将您的报告内容提供给其他任何个人或机构。点击「同意继续」即表示您已阅读并同意。',
-        confirmText: '同意继续',
-        cancelText: '暂不',
-        success: (res) => {
-          if (res.confirm) {
-            this.agreedToTerms = true
-            this.startPick(false)
+    startPickDan: function() {
+      var self = this
+      uni.chooseMessageFile({
+        count: 1,
+        type: 'file',
+        extension: ['pdf'],
+        success: function(res) {
+          var files = res.tempFiles
+          if (files && files.length > 0) {
+            self.selectedFile = files[0]
+            self.uploadDanAndGoPreview(files[0].path)
           }
+        },
+        fail: function() {
+          uni.chooseImage({
+            count: 1,
+            success: function(res) {
+              if (res.tempFiles && res.tempFiles.length > 0) {
+                self.selectedFile = res.tempFiles[0]
+                var path = res.tempFiles[0].path || (res.tempFilePaths && res.tempFilePaths[0])
+                self.uploadDanAndGoPreview(path)
+              }
+            }
+          })
         }
       })
     },
-    // 上传文件 → 自动跳转确认页(确认页按 draftId 触发解析并自动刷新显示结果)
-    uploadAndGoConfirm(filePath, type) {
+    // 健康报告:upload-only → 菌群/健康解析页
+    uploadHealthAndGoConfirm: function(filePath, type) {
       if (!filePath) return
-      this.uploading = true
+      var self = this
+      self.uploading = true
       uni.showLoading({ title: '上传中...' })
-      uploadReportOnly(filePath, type).then((res) => {
+      uploadReportOnly(filePath, type).then(function(res) {
         uni.hideLoading()
-        this.uploading = false
+        self.uploading = false
         var draftId = res.data && res.data.draftId
         if (!draftId) {
           uni.showToast({ title: (res && res.message) || '上传失败', icon: 'none' })
           return
         }
         uni.redirectTo({ url: '/pages/health/gut-flora-detail?draftId=' + draftId })
-      }).catch((err) => {
+      }).catch(function(err) {
         uni.hideLoading()
-        this.uploading = false
+        self.uploading = false
         var msg = (err && err.message) || '上传失败,请重试'
         uni.showToast({ title: msg, icon: 'none' })
       })
     },
-    // ===== 体脂秤专用上传 =====
-    goBodyFatScale() {
-      if (!this.agreedToTerms) {
-        uni.showModal({
-          title: '数据使用声明',
-          content: '上传体脂秤照片即代表您同意平台使用本报告数据进行分析。点击「同意继续」即表示您已阅读并同意。',
-          confirmText: '同意继续',
-          cancelText: '暂不',
-          success: (res) => {
-            if (res.confirm) {
-              this.agreedToTerms = true
-              this.startBodyFatUpload()
-            }
-          }
-        })
+    // DAN测评:parse-preview → 预览确认页
+    uploadDanAndGoPreview: function(filePath) {
+      if (!filePath) return
+      var self = this
+      var memberId = self.memberId
+          || (self.$store.state && self.$store.state.currentChildId)
+          || uni.getStorageSync('currentChildId')
+          || uni.getStorageSync('currentMemberId')
+      if (!memberId) {
+        uni.showToast({ title: '请先选择家庭成员', icon: 'none' })
         return
       }
-      this.startBodyFatUpload()
+      self.uploading = true
+      uni.showLoading({ title: '解析中...' })
+      danParsePreview(filePath, self.danDimension, memberId).then(function(res) {
+        uni.hideLoading()
+        self.uploading = false
+        if (!res || res.code !== 200 || !res.data) {
+          uni.showToast({ title: (res && res.message) || '解析失败', icon: 'none' })
+          return
+        }
+        try {
+          uni.setStorageSync('danUploadPreview', {
+            draftId: res.data.draftId || res.data.uploadId,
+            previewData: res.data,
+            dimension: self.danDimension,
+            memberId: memberId
+          })
+        } catch (e) {}
+        uni.redirectTo({
+          url: '/pages/dan-assessment/report-upload?resume=1&dimension=' + self.danDimension
+        })
+      }).catch(function(err) {
+        uni.hideLoading()
+        self.uploading = false
+        uni.showToast({ title: (err && err.message) || '上传失败', icon: 'none' })
+      })
     },
-    startBodyFatUpload() {
+    goBodyFatScale: function() {
+      var self = this
+      this.ensureAgreed(function() {
+        self.reportKind = 'health'
+        self.startBodyFatUpload()
+      })
+    },
+    startBodyFatUpload: function() {
       var self = this
       uni.chooseImage({
         count: 1,
@@ -275,7 +454,7 @@ export default {
         }
       })
     },
-    uploadBodyFat(filePath) {
+    uploadBodyFat: function(filePath) {
       var self = this
       self.uploading = true
       uni.showLoading({ title: 'AI 识别中...' })
@@ -315,12 +494,6 @@ export default {
   background: #F5F7FA;
 }
 
-/* ===== 导航栏 ===== */
-.nav-placeholder {
-  width: 80rpx;
-}
-
-/* ===== 提示条 ===== */
 .tip-banner {
   display: flex;
   flex-direction: row;
@@ -341,7 +514,6 @@ export default {
   line-height: 1.5;
 }
 
-/* ===== 表单 ===== */
 .form-section {
   margin: 0 30rpx;
 }
@@ -359,7 +531,82 @@ export default {
   margin-bottom: 16rpx;
 }
 
-/* 上传中状态 */
+.category-row {
+  display: flex;
+  flex-direction: row;
+  gap: 16rpx;
+}
+.category-card {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 28rpx 16rpx;
+  border-radius: 16rpx;
+  background: #F8FAFC;
+  border: 2rpx solid #E2E8F0;
+}
+.category-active {
+  background: #EFF6FF;
+  border-color: #3B82F6;
+}
+.category-dan.category-active {
+  background: #F5F3FF;
+  border-color: #8B5CF6;
+}
+.category-icon {
+  font-size: 40rpx;
+  margin-bottom: 10rpx;
+}
+.category-title {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #1E293B;
+  margin-bottom: 6rpx;
+}
+.category-desc {
+  font-size: 20rpx;
+  color: #94A3B8;
+  text-align: center;
+}
+
+.dim-grid {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+}
+.dim-chip {
+  width: 23%;
+  margin-right: 2%;
+  margin-bottom: 12rpx;
+  padding: 16rpx 0;
+  text-align: center;
+  border-radius: 12rpx;
+  background: #F1F5F9;
+  border: 2rpx solid transparent;
+  box-sizing: border-box;
+}
+.dim-active {
+  background: #EDE9FE;
+  border-color: #8B5CF6;
+}
+.dim-chip-text {
+  font-size: 22rpx;
+  color: #334155;
+}
+
+.kind-hint {
+  background: #FFFBEB;
+  border-radius: 12rpx;
+  padding: 20rpx;
+  margin-bottom: 20rpx;
+  border: 2rpx dashed #FCD34D;
+}
+.kind-hint-text {
+  font-size: 24rpx;
+  color: #92400E;
+}
+
 .analyzing-status {
   display: flex;
   flex-direction: row;
@@ -370,12 +617,6 @@ export default {
   color: #5B9BD5;
 }
 
-/* ===== 底部 ===== */
-.bottom-spacer {
-  height: 60rpx;
-}
-
-/* ===== 责任声明 ===== */
 .disclaimer-box {
   background: #FFF8F0;
   border: 2rpx solid #FDE68A;
@@ -424,11 +665,10 @@ export default {
   line-height: 1.6;
 }
 
-/* ===== 上传方式:并排卡片 ===== */
 .upload-methods {
   display: flex;
   flex-direction: row;
-  gap: 20rpx;
+  gap: 16rpx;
   margin-bottom: 20rpx;
 }
 .upload-card {
@@ -437,32 +677,32 @@ export default {
   flex-direction: column;
   align-items: center;
   justify-content: center;
-  padding: 40rpx 20rpx;
+  padding: 32rpx 12rpx;
   border-radius: 20rpx;
   text-align: center;
-  min-height: 240rpx;
+  min-height: 220rpx;
 }
 .upload-card:active {
   opacity: 0.85;
 }
 .upload-card-icon {
-  width: 96rpx;
-  height: 96rpx;
-  border-radius: 24rpx;
-  font-size: 48rpx;
+  width: 80rpx;
+  height: 80rpx;
+  border-radius: 20rpx;
+  font-size: 40rpx;
   display: flex;
   align-items: center;
   justify-content: center;
-  margin-bottom: 20rpx;
+  margin-bottom: 16rpx;
 }
 .upload-card-title {
-  font-size: 28rpx;
+  font-size: 26rpx;
   font-weight: 600;
   color: #333;
-  margin-bottom: 10rpx;
+  margin-bottom: 8rpx;
 }
 .upload-card-desc {
-  font-size: 22rpx;
+  font-size: 20rpx;
   color: #94A3B8;
   line-height: 1.5;
 }
@@ -473,7 +713,6 @@ export default {
 .card-file .upload-card-icon {
   background: linear-gradient(135deg, #3B82F6, #1D4ED8);
   color: #fff;
-  box-shadow: 0 4rpx 12rpx rgba(59, 130, 246, 0.3);
 }
 .card-photo {
   background: #FFF7ED;
@@ -482,10 +721,8 @@ export default {
 .card-photo .upload-card-icon {
   background: linear-gradient(135deg, #FB923C, #EA580C);
   color: #fff;
-  box-shadow: 0 4rpx 12rpx rgba(234, 88, 12, 0.25);
 }
 
-/* ===== 上传说明 ===== */
 .guide-section {
   margin: 0 30rpx 30rpx;
   background: #fff;
@@ -500,93 +737,124 @@ export default {
   margin-bottom: 20rpx;
 }
 .guide-header-icon {
-  font-size: 30rpx;
-  margin-right: 10rpx;
+  font-size: 32rpx;
+  margin-right: 12rpx;
 }
 .guide-header-title {
   font-size: 28rpx;
-  font-weight: bold;
-  color: #333;
-}
-.guide-list {
-  display: flex;
-  flex-direction: column;
+  font-weight: 600;
+  color: #1E293B;
 }
 .guide-item {
   display: flex;
   flex-direction: row;
-  align-items: center;
-  padding: 16rpx 0;
-  border-bottom: 1rpx solid #F0F0F0;
-}
-.guide-item:last-of-type {
-  border-bottom: none;
+  align-items: flex-start;
+  margin-bottom: 20rpx;
 }
 .guide-icon-box {
-  width: 72rpx;
-  height: 72rpx;
+  width: 64rpx;
+  height: 64rpx;
   border-radius: 16rpx;
   display: flex;
   align-items: center;
   justify-content: center;
+  margin-right: 16rpx;
   flex-shrink: 0;
-  margin-right: 20rpx;
 }
 .guide-icon {
-  font-size: 36rpx;
-}
-.guide-icon-blue {
-  background: #E3F2FD;
-}
-.guide-icon-green {
-  background: #E8F5E9;
-}
-.guide-icon-orange {
-  background: #FFF3E0;
-}
-.guide-icon-purple {
-  background: #F3E5F5;
-}
-.guide-icon-teal {
-  background: #E0F2F1;
-}
-.guide-icon-pink {
-  background: #FCE4EC;
+  font-size: 32rpx;
 }
+.guide-icon-blue { background: #DBEAFE; }
+.guide-icon-green { background: #DCFCE7; }
+.guide-icon-orange { background: #FFEDD5; }
+.guide-icon-purple { background: #EDE9FE; }
+.guide-icon-teal { background: #CCFBF1; }
 .guide-info {
   flex: 1;
-  min-width: 0;
-  display: flex;
-  flex-direction: column;
 }
 .guide-name {
   font-size: 26rpx;
   font-weight: 600;
-  color: #333;
+  color: #334155;
+  display: block;
   margin-bottom: 4rpx;
 }
 .guide-desc {
   font-size: 22rpx;
   color: #94A3B8;
-  line-height: 1.4;
+  line-height: 1.5;
 }
 .guide-tips {
-  background: #F8FAFC;
-  border-radius: 12rpx;
-  padding: 16rpx 20rpx;
-  margin-top: 16rpx;
-  display: flex;
-  flex-direction: column;
+  margin-top: 8rpx;
+  padding-top: 16rpx;
+  border-top: 1rpx solid #F1F5F9;
 }
 .guide-tips-title {
   font-size: 24rpx;
   font-weight: 600;
-  color: #5B9BD5;
+  color: #64748B;
+  display: block;
   margin-bottom: 8rpx;
 }
 .guide-tips-item {
   font-size: 22rpx;
-  color: #64748B;
+  color: #94A3B8;
+  display: block;
   line-height: 1.7;
 }
+
+.sheet-mask {
+  position: fixed;
+  left: 0;
+  right: 0;
+  top: 0;
+  bottom: 0;
+  background: rgba(15, 23, 42, 0.45);
+  z-index: 1000;
+  display: flex;
+  flex-direction: column;
+  justify-content: flex-end;
+}
+.sheet-panel {
+  background: #fff;
+  border-radius: 24rpx 24rpx 0 0;
+  padding: 32rpx 28rpx calc(32rpx + env(safe-area-inset-bottom));
+}
+.sheet-title {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #0F172A;
+  display: block;
+  text-align: center;
+  margin-bottom: 24rpx;
+}
+.sheet-item {
+  background: #F8FAFC;
+  border-radius: 16rpx;
+  padding: 28rpx 24rpx;
+  margin-bottom: 16rpx;
+}
+.sheet-item:active {
+  background: #EFF6FF;
+}
+.sheet-item-title {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #1E293B;
+  display: block;
+  margin-bottom: 6rpx;
+}
+.sheet-item-desc {
+  font-size: 22rpx;
+  color: #94A3B8;
+}
+.sheet-cancel {
+  margin-top: 8rpx;
+  padding: 24rpx;
+  text-align: center;
+}
+.sheet-cancel-text {
+  font-size: 28rpx;
+  color: #64748B;
+}
 </style>