Przeglądaj źródła

docs: 综合健康调研实现计划(3 任务:ComprehensiveSurvey 组件/独立调研页+入口改造/方案制定页替换+DEPRECATED)

iwt 1 tydzień temu
rodzic
commit
de023c6c5e

+ 2 - 1
docs/superpowers/PROJECT-OVERVIEW.md

@@ -382,7 +382,7 @@
 | `2026-09-19-article-reading-quiz-design.md` | 🟢 已实施(补全现有阅读答题:article_quiz_records 表 + LangGraph article_quiz_graph 出题 + AiGateway 接入 + 提交答题落库 + CF值/五维能量双奖励 + profile_snapshot.quiz_summary 认知雷达) | 文章阅读后答题奖励 + 认知雷达记录设计 |
 | `2026-09-22-guide-popup-config-design.md` | ✅ 已实施(4表/guide_popup+step+trigger+log,9接口,ConfigurableGuide.vue组件+管理端2页;旧 sys_config 方案保留并行) | 弹窗引导页配置化管理设计 |
 | `2026-10-10-address-contact-backfill-realname-design.md` | 🟡 设计稿(后端统一回填方案:`RealNameBackfillService` 独立组件 + `UserAddressService.create/update` + `ConsigneeService.save` 两入口回填,realName 为空才回填,幂等) | 地址联系人回填真实姓名设计 |
-| `2026-10-16-comprehensive-health-survey-design.md` | 🟡 设计稿(8 模块 step 式综合调研:基本情况/当前状态/饮食偏好/疾病史/排便运动/睡眠/用药/过敏源;URL `modules=` 传参可只调研子集;ComprehensiveSurvey 组件复用至方案制定 Step2;仅当前成员;合并式增量保存;lifestyle 新 key currentStatus/exerciseFrequency/exerciseDuration;过敏源独立完整采集;饮食结构 3 项补采;零后端改动) | 综合健康调研 — 身体状态+饮食偏好合并 |
+| `2026-10-16-comprehensive-health-survey-design.md` | 🟢 规格已定稿(自检修正:URL `modules=` 逗号分隔子集、v-if step 切换、进度条 `currentStep/modules.length`、currentStep 组件内部 data、空模块允许提交、submit emit `{form,diet,lifestyleDiff}`、旧 wizard 仅标记不删;8 模块 step 式综合调研:基本情况/当前状态/饮食偏好/疾病史/排便运动/睡眠/用药/过敏源;ComprehensiveSurvey 组件复用至方案制定 Step2;仅当前成员;合并式增量保存;lifestyle 新 key currentStatus/exerciseFrequency/exerciseDuration;过敏源独立完整采集双写两表;饮食结构 3 项补采;零后端改动) | 综合健康调研 — 身体状态+饮食偏好合并 |
 
 ### 实施计划(plans/)
 
@@ -394,6 +394,7 @@
 | `2026-10-09-indicator-change-curve-webadmin.md` | ✅ 已实施(W1-W6 完成:src/api/indicator.js 6 封装 + IndicatorCorrelations.vue + IndicatorMappings.vue + utils/indicatorSeeds.js + 路由注册 + 菜单回退项;jest 契约 7/7 + seeds 8/8、npx vue-cli-service build SUCCESS;零后端改动) | 指标关联/映射 — Web 管理端实施计划(P5,依赖后端 P4-P5) |
 | `2026-10-04-plan-review-workbench.md` | 🟢 已实施(15 任务全部完成:0 修 test-compile 基线、1 GuideFamilyAccessGuard、2 confirmBind 重绑复位、3 七个端点接入绑定校验、4 rejectPlan 状态守卫 + getPlanForFamily IDOR 反查、5 approveAndPublish 返回 taskGenerated(新增 PlanApproveResultDTO)、6 GuidePlanReviewController 方案审核 4 端点(含 planId IDOR 反查,6/6 绿)、7 GuideFamilyQueryService/Controller 新增 my-families + members 端点(授权源与 isBound 同源,4/4 绿)、8 后端整体验证(compile/test-compile EXIT 0 + 五个新增单测类共 32 个用例全绿 + 全仓 1688 条映射零重复路由)、9 utils/api.js 新增审核工作台 8 个 API 封装(各导出唯一,语法校验须用 .mjs 副本)、10 review-workbench.vue 审核台首页、11 review-family.vue 单家庭双 Tab(Task 用 deadline 非 dueDate;HealthPlan 无 title)、12 plan-detail.vue 方案详情(含 taskGenerated=false 部分成功提示;updatePlanContent 用 `if (planJson != null)` 守卫,3 参调用不会清空 plan_json)、13 pages.json 分包注册 3 页面(裸 path)+ families.vue 加入口、14 文档同步 API_REFERENCE 4.47;计划本身核实并修正 26 处初版事实错误) | 规划师审核工作台实现计划(拆 2 层编排:后端并行 + 前端并行;验收门为 `mvn test-compile` + 纯 Mockito 单测,不依赖真实数据库。⚠️ `pom.xml:28` 有 `<skipTests>true</skipTests>`,跑单测必须显式 `-DskipTests=false`,否则只输出 `Tests are skipped` 的假通过。⚠️ 路由查重必须按「类级 @RequestMapping 前缀 + 方法级字面量」归一化比较,只比方法字面量会产生假阳性) |
 | `2026-09-10-report-quick-analyze.md` | 🟡 待执行(5 任务:Redis 依赖/配置 + AIChatController 抽 buildChatInputs 新增 report/analyze 端点 + api.js 封装 + report-detail 浮层 + 验证) | 报告快速分析实现计划 |
+| `2026-10-16-comprehensive-survey.md` | 🟡 待执行(3 任务:①ComprehensiveSurvey.vue 核心组件(8 模块 step + 进度条 + 合并式增量保存 + 暖灶主题样式,含 8 组 54 项疾病清单/全部预设常量/条件渲染人群专属字段)②health-survey.vue 独立调研页 + pages.json 注册 + index-home/health-main 两处 goSelfTest 改跳 ③health-plan-summary Step2 组件替换(memberFormMap 快照传 form 子对象 + buildHealthPayload/parseLifestyle 补 3 新 key + 删 6 个 onWizard 方法)+ 旧 wizard 6 文件标记 DEPRECATED;验证:node --check + audit exit0 + 零 wizard 引用 + 13 项手工验证含增量保存专项) | 综合健康调研实现计划(设计稿:2026-10-16-comprehensive-health-survey-design.md) |
 | `2026-09-10-key-person-expansion-octopus.md` | 🟡 待执行(12 任务:迁移305-307 三表 + 3实体/Mapper/Service/Controller + 新 OctopusDiagram 三层画布 + 成效/关键人录入页 + action-detail 集成 + API 文档同步 + 旧章鱼图清理) | 关键人拓展章鱼图替换实现计划 |
 | `2026-08-18-homepage-health-funnel.md` | ✅ 已完成(登录态四段式重排 + 自测大卡 + 方法与计算属性 + 样式;未登录态零改动) | 首页健康漏斗实现计划 |
 | `2026-07-25-e2e-test-rearchitecture-phase1.md` | ✅ Phase 1 complete | E2E 测试体系 Phase 1 实施 |

+ 1566 - 0
docs/superpowers/plans/2026-10-16-comprehensive-survey.md

@@ -0,0 +1,1566 @@
+# 综合健康调研(Comprehensive Survey)实现计划
+
+> **日期:** 2026-10-16
+> **状态:** 待执行
+> **前置设计:** `docs/superpowers/specs/2026-10-16-comprehensive-health-survey-design.md`(已定稿 commit `564e29eab`)
+> **执行方式:** 按 superpowers:subagent-driven-development 逐任务实现,或 superpowers:executing-plans 内联执行
+> **技术栈:** uni-app 微信小程序(Vue2 Options API)+ Spring Boot 后端零改动
+
+---
+
+## 一、目标
+
+新建 `ComprehensiveSurvey.vue` 组件承载 8 模块 step 式综合调研(基本情况/当前状态/饮食偏好/疾病史/排便及运动/睡眠情况/用药情况/过敏源),首页新增独立调研页(URL `modules=` 控制模块子集),方案制定页 Step2 同步替换旧向导,实现「调研与方案制定共享组件和数据」。
+
+## 二、方案选型(已确认,详见设计文档 §二)
+
+| # | 决策 |
+|---|------|
+| 1 | **组件化替换**:新建 `ComprehensiveSurvey.vue`,首页新页面与方案制定页 Step2 嵌入同一组件 |
+| 2 | 模块由调用方传参(URL query `modules=`,8 key 逗号分隔子集;方案制定页传全量 8 模块) |
+| 3 | 仅当前成员维度(`currentChildId`/`currentMemberId` 快照),方案制定页多成员 tab 由父页切换传快照 |
+| 4 | `index-home` 无待填调研分支 + `health-main` 两处 `goSelfTest` 改跳新页 |
+| 5 | 新字段(`currentStatus`/`exerciseFrequency`/`exerciseDuration`)走 `health_status.lifestyle` JSON 自由 key |
+| 6 | 过敏原独立成 `allergy` 模块:过敏原多选 → `diet_preferences.allergies`、过敏症状多选 → `lifestyle.allergySymptoms` |
+| 7 | 饮食偏好模块补采 `vegetableFruit/dairy/sugaryDrinks`(后端 DTO 已有,页面漏采) |
+| 8 | 旧 `HealthQuestionnaireWizard.vue` + 5 子 step 标记 DEPRECATED,保留文件不删 |
+
+## 三、文件结构
+
+| 文件 | 操作 | 职责 |
+|------|------|------|
+| `cfc-frontend/pages/health/components/ComprehensiveSurvey.vue` | **新增** | 核心组件:8 模块 step 问卷、进度条、合并式增量保存、暖灶主题样式(预计 1000-1200 行) |
+| `cfc-frontend/pages/health/health-survey.vue` | **新增** | 独立调研包装页:解析 URL modules + 单成员快照 + 会员门槛 + 保存返回(预计 200 行) |
+| `cfc-frontend/pages.json` | 修改 | 注册 `health/health-survey` 到 health 分包 |
+| `cfc-frontend/pages/index-home/index.vue` | 修改 | `goSelfTest` 无待填调研分支改跳新页(L1639-1648) |
+| `cfc-frontend/pages/health-main/index.vue` | 修改 | `goSelfTest` 改跳新页(L518) |
+| `cfc-frontend/pages/health/health-plan-summary.vue` | 修改 | Step2 替换 Wizard 为 ComprehensiveSurvey;新增 `update-survey` 事件处理;`memberFormMap` 结构不变(lifestyle 全量存快照) |
+| `cfc-frontend/pages/health/components/HealthQuestionnaireWizard.vue` + 5 子 step | 标记 | 文件头加 DEPRECATED 注释,保留不删 |
+| 后端任何文件 | 不改 | 零后端、零 DDL |
+
+**字段归属:**
+- `basic` → health_status 列:`heightCm/weightKg/bloodPressure/bloodGlucose/bloodLipids`
+- `status` → lifestyle:`currentStatus/menstrualCycle/specialPeriod/growthDevelopment/pickyEating/osteoporosisRisk/fallRisk/screenTime/teenMood`(含人群专属条件渲染)
+- `diet` → diet_preferences 13 key(**不含 allergies**,含补采 3 项)
+- `disease` → health_status 列:`diseaseHistory`(JSON 数组)
+- `bowel` → lifestyle:`bowelFrequency/bowelForm/exerciseFrequency/exerciseDuration`
+- `sleep` → lifestyle:`sleepDuration/bedtime`
+- `med` → health_status 列:`medications`(JSON 数组)
+- `allergy` → diet_preferences:`allergies` + lifestyle:`allergySymptoms`(双写)
+
+---
+
+## 任务 1:创建核心组件 `ComprehensiveSurvey.vue`
+
+**文件:**
+- 创建:`cfc-frontend/pages/health/components/ComprehensiveSurvey.vue`
+
+**目标:** 8 模块 step 问卷组件,支持按 `modules` props 渲染子集,合并式增量保存,暖灶主题。
+
+---
+
+### 步骤 1.1:组件骨架 + props/events + 数据层
+
+创建文件,写入以下内容(**完整代码,非占位**):
+
+```vue
+<template>
+  <view class="survey-wrap">
+    <!-- 顶部进度条 -->
+    <view class="progress-bar">
+      <view class="progress-track">
+        <view class="progress-fill" :style="'width:' + progressPercent + '%'"></view>
+      </view>
+      <view class="progress-labels">
+        <text v-for="(m, i) in activeModules" :key="i"
+              class="progress-label" :class="{ 'progress-label-active': currentStep > i }">{{ moduleTitles[m] }}</text>
+      </view>
+    </view>
+
+    <!-- 步骤内容 -->
+    <scroll-view class="content" scroll-y>
+      <!-- Step 1: 基本情况 -->
+      <view v-if="currentModule === 'basic'">
+        <view class="step-head">
+          <text class="step-icon">📏</text>
+          <view class="step-head-text">
+            <text class="step-title">基本情况</text>
+            <text class="step-sub">身高、体重、血压、血糖、总胆固醇</text>
+          </view>
+        </view>
+        <view class="card">
+          <view class="card-row"><text class="card-label">身高(cm)</text>
+            <input class="card-input" type="digit" v-model="form.heightCm" placeholder="如 170" /></view>
+          <view class="card-row"><text class="card-label">体重(kg)</text>
+            <input class="card-input" type="digit" v-model="form.weightKg" placeholder="如 65" /></view>
+          <view class="card-row"><text class="card-label">血压(mmHg)</text>
+            <input class="card-input" v-model="form.bloodPressure" placeholder="如 120/80" /></view>
+          <view class="card-row"><text class="card-label">空腹血糖(mmol/L)</text>
+            <input class="card-input" type="digit" v-model="form.bloodGlucose" placeholder="如 5.6" /></view>
+          <view class="card-row"><text class="card-label">总胆固醇(mmol/L)</text>
+            <input class="card-input" type="digit" v-model="form.lipidCholesterol" placeholder="如 4.5" /></view>
+        </view>
+      </view>
+
+      <!-- Step 2: 当前状态 -->
+      <view v-if="currentModule === 'status'">
+        <view class="step-head">
+          <text class="step-icon">🩺</text>
+          <view class="step-head-text">
+            <text class="step-title">当前状态</text>
+            <text class="step-sub">特殊身体状态 + 人群专属情况</text>
+          </view>
+        </view>
+        <view class="card">
+          <text class="card-title">特殊状态</text>
+          <view class="chip-group">
+            <view v-for="opt in currentStatusOptions" :key="opt"
+                  class="chip" :class="{ 'chip-active': lifestyle.currentStatus === opt }"
+                  @tap="setLifestyle('currentStatus', opt)"><text>{{ opt }}</text></view>
+          </view>
+        </view>
+        <!-- 女性专属 -->
+        <block v-if="isFemale">
+          <view class="card"><text class="card-title">月经周期</text>
+            <view class="chip-group">
+              <view v-for="opt in menstrualOptions" :key="opt" class="chip"
+                    :class="{ 'chip-active': lifestyle.menstrualCycle === opt }"
+                    @tap="setLifestyle('menstrualCycle', opt)"><text>{{ opt }}</text></view>
+            </view>
+          </view>
+          <view class="card"><text class="card-title">备孕/怀孕/哺乳</text>
+            <view class="chip-group">
+              <view v-for="opt in specialPeriodOptions" :key="opt" class="chip"
+                    :class="{ 'chip-active': lifestyle.specialPeriod === opt }"
+                    @tap="setLifestyle('specialPeriod', opt)"><text>{{ opt }}</text></view>
+            </view>
+          </view>
+        </block>
+        <!-- 儿童专属 -->
+        <block v-if="isChild">
+          <view class="card"><text class="card-title">发育情况</text>
+            <view class="chip-group">
+              <view v-for="opt in growthOptions" :key="opt" class="chip"
+                    :class="{ 'chip-active': lifestyle.growthDevelopment === opt }"
+                    @tap="setLifestyle('growthDevelopment', opt)"><text>{{ opt }}</text></view>
+            </view>
+          </view>
+          <view class="card"><text class="card-title">挑食程度</text>
+            <view class="chip-group">
+              <view v-for="opt in pickyOptions" :key="opt" class="chip"
+                    :class="{ 'chip-active': lifestyle.pickyEating === opt }"
+                    @tap="setLifestyle('pickyEating', opt)"><text>{{ opt }}</text></view>
+            </view>
+          </view>
+        </block>
+        <!-- 老年专属 -->
+        <block v-if="isElderly">
+          <view class="card"><text class="card-title">跌倒风险</text>
+            <view class="chip-group">
+              <view v-for="opt in fallOptions" :key="opt" class="chip"
+                    :class="{ 'chip-active': lifestyle.fallRisk === opt }"
+                    @tap="setLifestyle('fallRisk', opt)"><text>{{ opt }}</text></view>
+            </view>
+          </view>
+          <view class="card"><text class="card-title">骨质疏松风险</text>
+            <view class="chip-group">
+              <view v-for="opt in osteoOptions" :key="opt" class="chip"
+                    :class="{ 'chip-active': lifestyle.osteoporosisRisk === opt }"
+                    @tap="setLifestyle('osteoporosisRisk', opt)"><text>{{ opt }}</text></view>
+            </view>
+          </view>
+        </block>
+        <!-- 青少年专属 -->
+        <block v-if="isTeenager">
+          <view class="card"><text class="card-title">屏幕时间</text>
+            <view class="chip-group">
+              <view v-for="opt in screenOptions" :key="opt" class="chip"
+                    :class="{ 'chip-active': lifestyle.screenTime === opt }"
+                    @tap="setLifestyle('screenTime', opt)"><text>{{ opt }}</text></view>
+            </view>
+          </view>
+          <view class="card"><text class="card-title">情绪状态</text>
+            <view class="chip-group">
+              <view v-for="opt in teenMoodOptions" :key="opt" class="chip"
+                    :class="{ 'chip-active': lifestyle.teenMood === opt }"
+                    @tap="setLifestyle('teenMood', opt)"><text>{{ opt }}</text></view>
+            </view>
+          </view>
+        </block>
+      </view>
+
+      <!-- Step 3: 饮食偏好 -->
+      <view v-if="currentModule === 'diet'">
+        <view class="step-head">
+          <text class="step-icon">🍽️</text>
+          <view class="step-head-text">
+            <text class="step-title">饮食偏好</text>
+            <text class="step-sub">忌口、口味、菜系、烹饪方式、饮食结构</text>
+          </view>
+        </view>
+        <!-- 无特殊偏好:一键清空 -->
+        <view class="pref-none-row">
+          <button class="pref-none-btn" :class="{ 'pref-none-btn-active': diet.noPreference }"
+                  @tap="setNoPreference">{{ diet.noPreference ? '已选:无特殊偏好' : '无特殊偏好' }}</button>
+        </view>
+        <view class="card"><text class="card-title">绝对忌口(多选)</text>
+          <view class="chip-group">
+            <view v-for="item in presetAvoid" :key="item" class="chip"
+                  :class="{ 'chip-active': diet.absoluteAvoid.indexOf(item) >= 0 }"
+                  @tap="toggleChip('diet', 'absoluteAvoid', item)"><text>{{ item }}</text></view>
+          </view>
+          <view class="custom-row" v-if="customField === 'absoluteAvoid'">
+            <input class="custom-input" placeholder="输入其他忌口" v-model="customInput"
+                   confirm-type="done" @confirm="addCustom" />
+            <view class="custom-confirm" @tap="addCustom"><text>添加</text></view>
+          </view>
+          <view class="custom-add" v-else @tap="openCustom('absoluteAvoid')"><text>+ 自定义添加</text></view>
+        </view>
+        <view class="card"><text class="card-title">必吃食物(多选)</text>
+          <view class="chip-group">
+            <view v-for="item in presetMustEat" :key="item" class="chip"
+                  :class="{ 'chip-active': diet.mustEat.indexOf(item) >= 0 }"
+                  @tap="toggleChip('diet', 'mustEat', item)"><text>{{ item }}</text></view>
+          </view>
+        </view>
+        <view class="card"><text class="card-title">口味偏好(多选)</text>
+          <view class="chip-group">
+            <view v-for="item in presetFlavor" :key="item" class="chip"
+                  :class="{ 'chip-active': diet.flavorPref.indexOf(item) >= 0 }"
+                  @tap="toggleChip('diet', 'flavorPref', item)"><text>{{ item }}</text></view>
+          </view>
+        </view>
+        <view class="card"><text class="card-title">菜系偏好(多选)</text>
+          <view class="chip-group">
+            <view v-for="item in presetCuisine" :key="item" class="chip"
+                  :class="{ 'chip-active': diet.cuisinePref.indexOf(item) >= 0 }"
+                  @tap="toggleChip('diet', 'cuisinePref', item)"><text>{{ item }}</text></view>
+          </view>
+        </view>
+        <view class="card"><text class="card-title">烹饪方式(多选)</text>
+          <view class="chip-group">
+            <view v-for="item in presetCooking" :key="item" class="chip"
+                  :class="{ 'chip-active': diet.cookingMethods.indexOf(item) >= 0 }"
+                  @tap="toggleChip('diet', 'cookingMethods', item)"><text>{{ item }}</text></view>
+          </view>
+        </view>
+        <view class="card"><text class="card-title">健康目标(多选)</text>
+          <view class="chip-group">
+            <view v-for="item in presetGoals" :key="item" class="chip"
+                  :class="{ 'chip-active': diet.healthGoals.indexOf(item) >= 0 }"
+                  @tap="toggleChip('diet', 'healthGoals', item)"><text>{{ item }}</text></view>
+          </view>
+        </view>
+        <view class="card"><text class="card-title">辣度(0=不辣,5=极辣)</text>
+          <view class="slider-row">
+            <text class="slider-val">{{ diet.spiceLevel }}</text>
+            <slider :value="diet.spiceLevel" min="0" max="5" step="1"
+                    @changing="onSpiceChanging" @change="onSpiceChange" activeColor="#FF8C42" />
+          </view>
+        </view>
+        <view class="card"><text class="card-title">宗教饮食</text>
+          <view class="chip-group">
+            <view v-for="opt in religiousOptions" :key="opt.value" class="chip"
+                  :class="{ 'chip-active': diet.religiousDiet === opt.value }"
+                  @tap="setDiet('religiousDiet', opt.value)"><text>{{ opt.label }}</text></view>
+          </view>
+        </view>
+        <view class="card"><text class="card-title">饮食结构现状(单选)</text>
+          <view class="struct-row"><text class="struct-label">每日蔬果量</text>
+            <view class="chip-group">
+              <view v-for="opt in vegFruitOptions" :key="opt" class="chip"
+                    :class="{ 'chip-active': diet.vegetableFruit === opt }"
+                    @tap="setDiet('vegetableFruit', opt)"><text>{{ opt }}</text></view>
+            </view>
+          </view>
+          <view class="struct-row"><text class="struct-label">乳制品摄入</text>
+            <view class="chip-group">
+              <view v-for="opt in dairyOptions" :key="opt" class="chip"
+                    :class="{ 'chip-active': diet.dairy === opt }"
+                    @tap="setDiet('dairy', opt)"><text>{{ opt }}</text></view>
+            </view>
+          </view>
+          <view class="struct-row"><text class="struct-label">含糖饮料频率</text>
+            <view class="chip-group">
+              <view v-for="opt in sugaryOptions" :key="opt" class="chip"
+                    :class="{ 'chip-active': diet.sugaryDrinks === opt }"
+                    @tap="setDiet('sugaryDrinks', opt)"><text>{{ opt }}</text></view>
+            </view>
+          </view>
+        </view>
+      </view>
+
+      <!-- Step 4: 疾病史 -->
+      <view v-if="currentModule === 'disease'">
+        <view class="step-head">
+          <text class="step-icon">📋</text>
+          <view class="step-head-text">
+            <text class="step-title">疾病史</text>
+            <text class="step-sub">选择过往确诊疾病({{ form.selectedDiseases.length }} 项已选)</text>
+          </view>
+        </view>
+        <view v-for="(group, gi) in diseaseGroups" :key="group.key" class="card">
+          <view class="card-head" @tap="toggleGroup(group.key)">
+            <text class="card-title">{{ group.title }}</text>
+            <text class="group-arrow">{{ expandedGroups.indexOf(group.key) >= 0 ? '收起' : '展开' }}</text>
+          </view>
+          <view v-if="expandedGroups.indexOf(group.key) >= 0" class="chip-group">
+            <view v-for="(item, ii) in group.items" :key="ii" class="chip"
+                  :class="{ 'chip-active': isDiseaseSelected(item.name) }"
+                  @tap="toggleDisease(item.name)">
+              <text>{{ item.label }}</text>
+            </view>
+          </view>
+        </view>
+        <view class="card">
+          <view class="card-head"><text class="card-title">自定义疾病</text></view>
+          <view v-if="customDiseaseMode" class="custom-row">
+            <input class="custom-input" placeholder="疾病名称" v-model="customDisease.name" />
+            <view class="custom-confirm" @tap="confirmCustomDisease"><text>添加</text></view>
+            <view class="custom-cancel" @tap="customDiseaseMode = false"><text>取消</text></view>
+          </view>
+          <view class="custom-add" v-else @tap="customDiseaseMode = true"><text>+ 添加自定义疾病</text></view>
+        </view>
+      </view>
+
+      <!-- Step 5: 排便及运动 -->
+      <view v-if="currentModule === 'bowel'">
+        <view class="step-head">
+          <text class="step-icon">🚽</text>
+          <view class="step-head-text">
+            <text class="step-title">排便及运动</text>
+            <text class="step-sub">排便习惯 + 运动频率与时长</text>
+          </view>
+        </view>
+        <view class="card"><text class="card-title">排便频率</text>
+          <view class="chip-group">
+            <view v-for="opt in bowelFreqOptions" :key="opt" class="chip"
+                  :class="{ 'chip-active': lifestyle.bowelFrequency === opt }"
+                  @tap="setLifestyle('bowelFrequency', opt)"><text>{{ opt }}</text></view>
+          </view>
+        </view>
+        <view class="card"><text class="card-title">粪便性状</text>
+          <view class="chip-group">
+            <view v-for="opt in bowelFormOptions" :key="opt" class="chip"
+                  :class="{ 'chip-active': lifestyle.bowelForm === opt }"
+                  @tap="setLifestyle('bowelForm', opt)"><text>{{ opt }}</text></view>
+          </view>
+        </view>
+        <view class="card"><text class="card-title">运动频率</text>
+          <view class="chip-group">
+            <view v-for="opt in exerciseFreqOptions" :key="opt" class="chip"
+                  :class="{ 'chip-active': lifestyle.exerciseFrequency === opt }"
+                  @tap="setLifestyle('exerciseFrequency', opt)"><text>{{ opt }}</text></view>
+          </view>
+        </view>
+        <view class="card"><text class="card-title">每次运动时长</text>
+          <view class="chip-group">
+            <view v-for="opt in exerciseDurationOptions" :key="opt" class="chip"
+                  :class="{ 'chip-active': lifestyle.exerciseDuration === opt }"
+                  @tap="setLifestyle('exerciseDuration', opt)"><text>{{ opt }}</text></view>
+          </view>
+        </view>
+      </view>
+
+      <!-- Step 6: 睡眠情况 -->
+      <view v-if="currentModule === 'sleep'">
+        <view class="step-head">
+          <text class="step-icon">😴</text>
+          <view class="step-head-text">
+            <text class="step-title">睡眠情况</text>
+            <text class="step-sub">睡眠时长与入睡时间</text>
+          </view>
+        </view>
+        <view class="card"><text class="card-title">睡眠时长</text>
+          <view class="chip-group">
+            <view v-for="opt in sleepDurationOptions" :key="opt" class="chip"
+                  :class="{ 'chip-active': lifestyle.sleepDuration === opt }"
+                  @tap="setLifestyle('sleepDuration', opt)"><text>{{ opt }}</text></view>
+          </view>
+        </view>
+        <view class="card"><text class="card-title">入睡时间</text>
+          <view class="chip-group">
+            <view v-for="opt in bedtimeOptions" :key="opt" class="chip"
+                  :class="{ 'chip-active': lifestyle.bedtime === opt }"
+                  @tap="setLifestyle('bedtime', opt)"><text>{{ opt }}</text></view>
+          </view>
+        </view>
+      </view>
+
+      <!-- Step 7: 用药情况 -->
+      <view v-if="currentModule === 'med'">
+        <view class="step-head">
+          <text class="step-icon">💊</text>
+          <view class="step-head-text">
+            <text class="step-title">用药情况</text>
+            <text class="step-sub">当前长期用药;如无请勾选「无长期用药」</text>
+          </view>
+        </view>
+        <view class="card">
+          <view class="checkbox-row" @tap="form.noMedication = !form.noMedication">
+            <view class="checkbox" :class="{ 'checkbox-on': form.noMedication }">
+              <text v-if="form.noMedication">✓</text>
+            </view>
+            <text class="checkbox-label">无长期用药</text>
+          </view>
+        </view>
+        <view v-if="!form.noMedication" v-for="(row, i) in form.medRows" :key="i" class="card">
+          <view class="card-head"><text class="card-title">用药 {{ i + 1 }}</text>
+            <text class="card-del" @tap="delMedRow(i)">删除</text></view>
+          <view class="card-row"><text class="card-label">药名</text>
+            <input class="card-input" v-model="row.name" placeholder="如 二甲双胍" /></view>
+          <view class="card-row"><text class="card-label">开始年月</text>
+            <input class="card-input" v-model="row.startDate" placeholder="如 2024-03" /></view>
+          <view class="card-row"><text class="card-label">服药时间</text>
+            <input class="card-input" v-model="row.time" placeholder="如 早饭后" /></view>
+        </view>
+        <view class="add-row" v-if="!form.noMedication && form.medRows.length < 10" @tap="addMedRow">
+          <text class="add-icon">+</text><text class="add-text">添加用药</text>
+        </view>
+      </view>
+
+      <!-- Step 8: 过敏源 -->
+      <view v-if="currentModule === 'allergy'">
+        <view class="step-head">
+          <text class="step-icon">⚠️</text>
+          <view class="step-head-text">
+            <text class="step-title">过敏源</text>
+            <text class="step-sub">过敏原与过敏症状,可多选</text>
+          </view>
+        </view>
+        <view class="card"><text class="card-title">过敏原(多选)</text>
+          <view class="chip-group">
+            <view v-for="item in presetAllergies" :key="item" class="chip"
+                  :class="{ 'chip-active': diet.allergies.indexOf(item) >= 0 }"
+                  @tap="toggleChip('diet', 'allergies', item)"><text>{{ item }}</text></view>
+          </view>
+          <view class="custom-row" v-if="customField === 'allergies'">
+            <input class="custom-input" placeholder="输入其他过敏原" v-model="customInput"
+                   confirm-type="done" @confirm="addCustom" />
+            <view class="custom-confirm" @tap="addCustom"><text>添加</text></view>
+          </view>
+          <view class="custom-add" v-else @tap="openCustom('allergies')"><text>+ 自定义添加</text></view>
+        </view>
+        <view class="card"><text class="card-title">过敏症状(多选)</text>
+          <view class="chip-group">
+            <view v-for="item in allergySymptomOptions" :key="item" class="chip"
+                  :class="{ 'chip-active': lifestyle.allergySymptoms.indexOf(item) >= 0 }"
+                  @tap="toggleLifestyleArray('allergySymptoms', item)"><text>{{ item }}</text></view>
+          </view>
+        </view>
+      </view>
+    </scroll-view>
+
+    <!-- 底部固定栏 -->
+    <view class="footer-bar">
+      <button class="btn-prev" v-if="currentStep > 1" @tap="prevStep">上一步</button>
+      <button class="btn-next" v-if="currentStep < activeModules.length" @tap="nextStep">下一步</button>
+      <button class="btn-done" v-else :disabled="submitting" @tap="onSubmit">
+        {{ submitting ? '保存中...' : '完成' }}
+      </button>
+    </view>
+  </view>
+</view>
+
+<script>
+// 预设选项常量
+var PRESET_ALLERGIES = ['海鲜', '花生', '乳糖', '鸡蛋', '牛奶', '豆类', '麸质', '坚果', '芒果', '其他']
+var PRESET_AVOID = ['牛肉', '羊肉', '猪肉', '内脏', '油腻', '辛辣', '生冷', '腌制']
+var PRESET_FLAVOR = ['酸', '甜', '咸', '鲜', '苦', '清淡', '浓郁']
+var PRESET_CUISINE = ['川', '粤', '鲁', '淮扬', '日料', '西餐', '东南亚', '韩餐']
+var PRESET_COOKING = ['蒸', '煮', '炒', '烤', '炖', '凉拌', '煎', '炸']
+var PRESET_MUSTEAT = ['蔬菜', '水果', '粗粮', '鱼类', '鸡蛋', '豆制品', '菌菇']
+var PRESET_GOALS = ['减脂', '增肌', '控糖', '肠道调理', '增强免疫']
+
+export default {
+  name: 'ComprehensiveSurvey',
+  props: {
+    // 启用模块 key 列表(顺序决定步骤顺序)
+    modules: { type: Array, default: function() { return [] } },
+    // health_status 快照(父页维护)
+    form: { type: Object, default: function() { return {} } },
+    // diet_preferences 快照(父页维护)
+    diet: { type: Object, default: function() { return {} } },
+    // 成员画像(驱动条件渲染)
+    profile: { type: Object, default: function() { return {} } },
+    // 外部提交态
+    submitting: { type: Boolean, default: false }
+  },
+  data: function() {
+    return {
+      currentStep: 1,
+      // lifestyle 工作副本(含全部 15 旧 key + 3 新 key)
+      lifestyle: {},
+      // 用 inheritable:deep watch props.form/diet,快照变更时同步
+      innerForm: {},
+      innerDiet: {},
+      // 自定义输入
+      customField: '',
+      customInput: '',
+      customDiseaseMode: false,
+      customDisease: { name: '', diagnosedAt: '' },
+      // 疾病分组展开态
+      expandedGroups: [],
+      // 选项常量
+      presetAllergies: PRESET_ALLERGIES,
+      presetAvoid: PRESET_AVOID,
+      presetFlavor: PRESET_FLAVOR,
+      presetCuisine: PRESET_CUISINE,
+      presetCooking: PRESET_COOKING,
+      presetMustEat: PRESET_MUSTEAT,
+      presetGoals: PRESET_GOALS,
+      currentStatusOptions: ['正常', '哺乳期', '备孕', '怀孕', '住院期', '恢复期', '其他'],
+      menstrualOptions: ['规律', '不规律', '已绝经'],
+      specialPeriodOptions: ['无', '备孕中', '已怀孕', '哺乳期'],
+      growthOptions: ['正常', '偏慢', '偏快'],
+      pickyOptions: ['不挑食', '轻微挑食', '严重挑食'],
+      fallOptions: ['低风险', '中风险', '高风险'],
+      osteoOptions: ['低风险', '中风险', '高风险'],
+      screenOptions: ['<1小时', '1-3小时', '3-5小时', '>5小时'],
+      teenMoodOptions: ['稳定', '易焦虑', '易低落', '波动大'],
+      bowelFreqOptions: ['每天1次', '每天2-3次', '每2-3天1次', '每周1-2次', '不定期'],
+      bowelFormOptions: ['成型软便', '稀便', '硬结', '水样', '羊粪状'],
+      exerciseFreqOptions: ['不运动', '每周1-2次', '每周3-4次', '每周5次以上'],
+      exerciseDurationOptions: ['30分钟以内', '30-60分钟', '60分钟以上'],
+      sleepDurationOptions: ['<6小时', '6-7小时', '7-8小时', '8-9小时', '>9小时'],
+      bedtimeOptions: ['21点前', '21-22点', '22-23点', '23-24点', '24点后'],
+      vegFruitOptions: ['<200g', '200-400g', '400-600g', '>600g'],
+      dairyOptions: ['不喝', '每周1-2次', '每天1次', '每天2次以上'],
+      sugaryOptions: ['不喝', '每周1-2次', '每天1次', '每天2次以上'],
+      allergySymptomOptions: ['无', '皮疹', '鼻痒', '眼痒', '哮喘样', '食物过敏'],
+      moduleTitles: {
+        basic: '基本情况', status: '当前状态', diet: '饮食偏好', disease: '疾病史',
+        bowel: '排便运动', sleep: '睡眠', med: '用药', allergy: '过敏源'
+      },
+      diseaseGroups: [
+        { key: 'meta', title: '代谢与内分泌', items: [
+          { name: '糖尿病', label: '糖尿病' }, { name: '甲状腺功能减退', label: '甲减' },
+          { name: '甲状腺功能亢进', label: '甲亢' }, { name: '高血脂', label: '高血脂' },
+          { name: '痛风', label: '痛风' }, { name: '肥胖症', label: '肥胖症' }
+        ]},
+        { key: 'cardio', title: '心脑血管', items: [
+          { name: '高血压', label: '高血压' }, { name: '冠心病', label: '冠心病' },
+          { name: '心肌梗死', label: '心梗' }, { name: '心律失常', label: '心律失常' },
+          { name: '脑卒中', label: '脑卒中' }, { name: '心力衰竭', label: '心衰' }
+        ]},
+        { key: 'digest', title: '消化系统', items: [
+          { name: '慢性胃炎', label: '慢性胃炎' }, { name: '胃溃疡', label: '胃溃疡' },
+          { name: '十二指肠溃疡', label: '十二指肠溃疡' }, { name: '炎症性肠病', label: '炎症性肠病' },
+          { name: '肠易激综合征', label: '肠易激综合征' }, { name: '脂肪肝', label: '脂肪肝' },
+          { name: '慢性肝炎', label: '慢性肝炎' }, { name: '胆结石', label: '胆结石' },
+          { name: '慢性胰腺炎', label: '慢性胰腺炎' }
+        ]},
+        { key: 'resp', title: '呼吸系统', items: [
+          { name: '哮喘', label: '哮喘' }, { name: '慢性支气管炎', label: '慢性支气管炎' },
+          { name: '慢性阻塞性肺疾病', label: '慢阻肺' }, { name: '支气管扩张', label: '支气管扩张' },
+          { name: '睡眠呼吸暂停', label: '睡眠呼吸暂停' }
+        ]},
+        { key: 'renal', title: '泌尿系统', items: [
+          { name: '慢性肾炎', label: '慢性肾炎' }, { name: '肾病综合征', label: '肾病综合征' },
+          { name: '慢性肾衰竭', label: '慢性肾衰竭' }, { name: '肾结石', label: '肾结石' },
+          { name: '前列腺增生', label: '前列腺增生' }
+        ]},
+        { key: 'immune', title: '免疫与过敏', items: [
+          { name: '类风湿关节炎', label: '类风湿关节炎' }, { name: '系统性红斑狼疮', label: '红斑狼疮' },
+          { name: '强直性脊柱炎', label: '强直性脊柱炎' }, { name: '干燥综合征', label: '干燥综合征' },
+          { name: '食物过敏', label: '食物过敏' }, { name: '药物过敏', label: '药物过敏' },
+          { name: '过敏性鼻炎', label: '过敏性鼻炎' }, { name: '慢性荨麻疹', label: '慢性荨麻疹' },
+          { name: '乳糖不耐受', label: '乳糖不耐受' }, { name: '麸质不耐受', label: '麸质不耐受' }
+        ]},
+        { key: 'neuro', title: '神经系统', items: [
+          { name: '偏头痛', label: '偏头痛' }, { name: '癫痫', label: '癫痫' },
+          { name: '帕金森病', label: '帕金森病' }, { name: '阿尔茨海默病', label: '阿尔茨海默病' },
+          { name: '周围神经病变', label: '周围神经病变' }
+        ]},
+        { key: 'other', title: '其他', items: [
+          { name: '贫血', label: '贫血' }, { name: '骨质疏松', label: '骨质疏松' },
+          { name: '甲状腺结节', label: '甲状腺结节' }, { name: '恶性肿瘤', label: '恶性肿瘤' },
+          { name: '抑郁症', label: '抑郁症' }, { name: '焦虑症', label: '焦虑症' }
+        ]}
+      ],
+      religiousOptions: [
+        { value: 'none', label: '无限制' },
+        { value: 'halal', label: '清真' },
+        { value: 'vegetarian', label: '素食' },
+        { value: 'vegan', label: '纯素' }
+      ]
+    }
+  },
+  computed: {
+    // 过滤非法 module key + 兜底全量
+    activeModules: function() {
+      var valid = ['basic', 'status', 'diet', 'disease', 'bowel', 'sleep', 'med', 'allergy']
+      var list = (this.modules || []).filter(function(m) { return valid.indexOf(m) >= 0 })
+      if (list.length === 0) list = valid.slice()
+      // 去重(保持顺序)
+      var seen = {}
+      return list.filter(function(m) { if (seen[m]) return false; seen[m] = true; return true })
+    },
+    currentModule: function() {
+      return this.activeModules[this.currentStep - 1] || ''
+    },
+    progressPercent: function() {
+      return Math.round((this.currentStep / this.activeModules.length) * 100)
+    },
+    isFemale: function() {
+      var g = this.profile && this.profile.gender
+      return g === 'female' || g === '女'
+    },
+    isChild: function() {
+      var age = this.profile && this.profile.age
+      return age != null && Number(age) > 0 && Number(age) < 12
+    },
+    isTeenager: function() {
+      var age = this.profile && this.profile.age
+      return age != null && Number(age) >= 12 && Number(age) < 18
+    },
+    isElderly: function() {
+      var age = this.profile && this.profile.age
+      return age != null && Number(age) >= 60
+    }
+  },
+  watch: {
+    // props.form/diet/lifestyle 变更 → 同步内部工作副本
+    form: {
+      handler: function(v) { this.innerForm = v || {} },
+      immediate: true,
+      deep: true
+    },
+    diet: {
+      handler: function(v) { this.innerDiet = v || {} },
+      immediate: true,
+      deep: true
+    }
+  },
+  created: function() {
+    // 快照默认值(父页未传时兜底)
+    this.innerForm = this.form || {}
+    this.innerDiet = this.diet || {}
+    // 从 form.lifestyle 还原 lifestyle 工作副本
+    var ls = this.form && this.form.lifestyle
+    this.lifestyle = (ls && typeof ls === 'object') ? JSON.parse(JSON.stringify(ls)) : {}
+  },
+  methods: {
+    // ===== 步骤导航 =====
+    prevStep: function() { if (this.currentStep > 1) this.currentStep-- },
+    nextStep: function() {
+      if (this.currentStep < this.activeModules.length) {
+        this.syncToParent()
+        this.currentStep++
+      }
+    },
+    // ===== 状态同步(切步/提交前把工作副本推回父页) =====
+    syncToParent: function() {
+      var formPatch = this.buildFormPatch()
+      if (Object.keys(formPatch).length > 0) {
+        this.$emit('update-form', formPatch)
+      }
+      var dietPatch = this.buildDietPatch()
+      if (Object.keys(dietPatch).length > 0) {
+        this.$emit('update-diet', dietPatch)
+      }
+    },
+    // ===== 提交 =====
+    onSubmit: function() {
+      if (this.submitting) return
+      // 数值校验(仅基本情况的启用模块)
+      if (this.activeModules.indexOf('basic') >= 0) {
+        var h = Number(this.innerForm.heightCm)
+        var w = Number(this.innerForm.weightKg)
+        var g = Number(this.innerForm.bloodGlucose)
+        var bp = this.innerForm.bloodPressure
+        if (this.innerForm.heightCm && (h < 30 || h > 250)) {
+          uni.showToast({ title: '身高应在 30-250 之间', icon: 'none' }); return
+        }
+        if (this.innerForm.weightKg && (w < 3 || w > 300)) {
+          uni.showToast({ title: '体重应在 3-300 之间', icon: 'none' }); return
+        }
+        if (this.innerForm.bloodGlucose && (g < 1 || g > 30)) {
+          uni.showToast({ title: '血糖应在 1-30 之间', icon: 'none' }); return
+        }
+        if (bp && !/^\d{2,3}\/\d{2,3}$/.test(bp)) {
+          uni.showToast({ title: '血压格式应为 120/80', icon: 'none' }); return
+        }
+      }
+      // 空提交拦截:所有启用模块均未填任何内容
+      var formPatch = this.buildFormPatch()
+      var dietPatch = this.buildDietPatch()
+      var lifestyleDiff = this.buildLifestyleDiff()
+      if (Object.keys(formPatch).length === 0 && Object.keys(dietPatch).length === 0 &&
+          Object.keys(lifestyleDiff).length === 0) {
+        uni.showToast({ title: '请至少填写一项', icon: 'none' }); return
+      }
+      this.$emit('submit', { form: this.innerForm, diet: this.innerDiet, lifestyleDiff: lifestyleDiff })
+    },
+    // ===== form/diet patch 组装(仅含本次有值的字段) =====
+    buildFormPatch: function() {
+      var patch = {}
+      var f = this.innerForm
+      if (f.heightCm != null && f.heightCm !== '') patch.heightCm = f.heightCm
+      if (f.weightKg != null && f.weightKg !== '') patch.weightKg = f.weightKg
+      if (f.bloodPressure != null && f.bloodPressure !== '') patch.bloodPressure = f.bloodPressure
+      if (f.bloodGlucose != null && f.bloodGlucose !== '') patch.bloodGlucose = f.bloodGlucose
+      if (f.lipidCholesterol != null && f.lipidCholesterol !== '') {
+        patch.bloodLipids = JSON.stringify([{ name: '总胆固醇', value: parseFloat(f.lipidCholesterol), unit: 'mmol/L', note: '' }])
+      }
+      if (f.notes != null && f.notes !== '') patch.notes = f.notes
+      // 疾病史
+      var dis = (f.selectedDiseases || []).map(function(item) {
+        var d = { name: item.name }
+        if (item.diagnosedAt) d.diagnosedAt = item.diagnosedAt
+        return d
+      })
+      if (dis.length > 0) patch.diseaseHistory = JSON.stringify(dis)
+      // 用药
+      var meds = (f.medRows || []).filter(function(item) { return item.name }).map(function(item) {
+        var m = { name: item.name }
+        if (item.startDate) m.startDate = item.startDate
+        if (item.durationText) m.durationText = item.durationText
+        if (item.time) m.time = item.time
+        return m
+      })
+      if (meds.length > 0) patch.medications = JSON.stringify(meds)
+      if (f.noMedication) patch.noMedication = true
+      return patch
+    },
+    buildDietPatch: function() {
+      var patch = {}
+      var d = this.innerDiet
+      var arrKeys = ['allergies', 'absoluteAvoid', 'mustEat', 'flavorPref', 'cuisinePref', 'cookingMethods', 'healthGoals']
+      arrKeys.forEach(function(k) { if (d[k] && d[k].length > 0) patch[k] = d[k].slice() })
+      var strKeys = ['vegetableFruit', 'dairy', 'sugaryDrinks']
+      strKeys.forEach(function(k) { if (d[k]) patch[k] = d[k] })
+      if (d.religiousDiet && d.religiousDiet !== 'none') patch.religiousDiet = d.religiousDiet
+      if (d.spiceLevel && Number(d.spiceLevel) > 0) patch.spiceLevel = Number(d.spiceLevel)
+      if (d.noPreference) patch.noPreference = true
+      return patch
+    },
+    // lifestyle diff:仅含本次修改过的 key
+    buildLifestyleDiff: function() {
+      var diff = {}
+      var orig = (this.form && this.form.lifestyle) || {}
+      var keys = ['currentStatus', 'menstrualCycle', 'specialPeriod', 'growthDevelopment', 'pickyEating',
+        'osteoporosisRisk', 'fallRisk', 'screenTime', 'teenMood',
+        'bowelFrequency', 'bowelForm', 'exerciseFrequency', 'exerciseDuration',
+        'sleepDuration', 'bedtime', 'antibioticHistory', 'mood', 'allergySymptoms']
+      var self = this
+      keys.forEach(function(k) {
+        var val = self.lifestyle[k]
+        if (val == null) return
+        if (val instanceof Array) { if (val.length > 0) diff[k] = val.slice() }
+        else if (val !== '') diff[k] = val
+      })
+      // 移除与原始快照完全相同的 key(未修改)
+      Object.keys(diff).forEach(function(k) {
+        var a = JSON.stringify(diff[k])
+        var b = JSON.stringify(orig[k] != null ? orig[k] : null)
+        if (a === b) delete diff[k]
+      })
+      return diff
+    },
+    // ===== lifestyle 工具 =====
+    setLifestyle: function(key, val) {
+      this.$set(this.lifestyle, key, this.lifestyle[key] === val ? '' : val)
+    },
+    toggleLifestyleArray: function(key, val) {
+      var arr = (this.lifestyle[key] || []).slice()
+      var idx = arr.indexOf(val)
+      if (idx >= 0) arr.splice(idx, 1); else arr.push(val)
+      this.$set(this.lifestyle, key, arr)
+    },
+    // ===== diet 工具 =====
+    setDiet: function(key, val) {
+      this.$set(this.innerDiet, key, val)
+    },
+    toggleChip: function(scope, key, val) {
+      var arr = (this.innerDiet[key] || []).slice()
+      var idx = arr.indexOf(val)
+      if (idx >= 0) arr.splice(idx, 1); else arr.push(val)
+      this.$set(this.innerDiet, key, arr)
+    },
+    setNoPreference: function() {
+      this.$set(this.innerDiet, 'noPreference', !this.innerDiet.noPreference)
+    },
+    onSpiceChanging: function(e) {
+      this.$set(this.innerDiet, 'spiceLevel', e.detail.value)
+    },
+    onSpiceChange: function(e) {
+      this.$set(this.innerDiet, 'spiceLevel', e.detail.value)
+    },
+    // ===== 自定义添加 =====
+    openCustom: function(field) { this.customField = field; this.customInput = '' },
+    addCustom: function() {
+      var val = (this.customInput || '').trim()
+      if (!val) { this.customField = ''; return }
+      this.toggleChip('diet', this.customField, val)
+      this.customInput = ''; this.customField = ''
+    },
+    // ===== 疾病 =====
+    toggleGroup: function(key) {
+      var idx = this.expandedGroups.indexOf(key)
+      if (idx >= 0) this.expandedGroups.splice(idx, 1); else this.expandedGroups.push(key)
+    },
+    isDiseaseSelected: function(name) {
+      return (this.innerForm.selectedDiseases || []).some(function(d) { return d.name === name })
+    },
+    toggleDisease: function(name) {
+      var list = (this.innerForm.selectedDiseases || []).slice()
+      var idx = -1
+      for (var i = 0; i < list.length; i++) { if (list[i].name === name) { idx = i; break } }
+      if (idx >= 0) list.splice(idx, 1); else list.push({ name: name, diagnosedAt: '', custom: false })
+      this.$set(this.innerForm, 'selectedDiseases', list)
+    },
+    confirmCustomDisease: function() {
+      var name = (this.customDisease.name || '').trim()
+      if (!name) return
+      this.innerForm.selectedDiseases = (this.innerForm.selectedDiseases || []).concat([{ name: name, diagnosedAt: '', custom: true }])
+      this.customDisease.name = ''; this.customDiseaseMode = false
+    },
+    // ===== 用药 =====
+    addMedRow: function() {
+      var rows = (this.innerForm.medRows || []).slice()
+      rows.push({ name: '', startDate: '', durationText: '', time: '' })
+      this.$set(this.innerForm, 'medRows', rows)
+    },
+    delMedRow: function(i) {
+      var rows = (this.innerForm.medRows || []).slice()
+      rows.splice(i, 1)
+      this.$set(this.innerForm, 'medRows', rows)
+    }
+  }
+}
+</script>
+
+<style scoped>
+.survey-wrap { min-height: 100vh; background: linear-gradient(180deg, #FFF7ED, #FFF3E6); display: flex; flex-direction: column; }
+.progress-bar { padding: 24rpx 32rpx 8rpx; background: #FFF7ED; }
+.progress-track { height: 8rpx; background: #FFE6D2; border-radius: 4rpx; overflow: hidden; }
+.progress-fill { height: 100%; background: #FF8C42; border-radius: 4rpx; transition: width 0.3s; }
+.progress-labels { display: flex; justify-content: space-between; margin-top: 12rpx; }
+.progress-label { font-size: 20rpx; color: #B08968; }
+.progress-label-active { color: #FF8C42; font-weight: 600; }
+.content { flex: 1; padding: 24rpx 32rpx 200rpx; }
+.step-head { display: flex; align-items: flex-start; margin-bottom: 24rpx; }
+.step-icon { font-size: 48rpx; margin-right: 20rpx; }
+.step-head-text { flex: 1; }
+.step-title { display: block; font-size: 36rpx; font-weight: 700; color: #4A3728; }
+.step-sub { display: block; font-size: 24rpx; color: #B08968; margin-top: 8rpx; }
+.card { background: #FFFFFF; border-radius: 24rpx; padding: 28rpx; margin-bottom: 24rpx; box-shadow: 0 4rpx 16rpx rgba(255, 140, 66, 0.08); }
+.card-title { display: block; font-size: 28rpx; font-weight: 600; color: #4A3728; margin-bottom: 16rpx; }
+.card-head { display: flex; justify-content: space-between; align-items: center; }
+.card-del { font-size: 24rpx; color: #E53E3E; }
+.card-count { font-size: 22rpx; color: #B08968; }
+.group-arrow { font-size: 22rpx; color: #B08968; }
+.card-row { display: flex; align-items: center; justify-content: space-between; padding: 20rpx 0; border-bottom: 1rpx solid #F7EFE6; }
+.card-row:last-child { border-bottom: none; }
+.card-label { font-size: 26rpx; color: #6B5744; }
+.card-input { flex: 1; text-align: right; font-size: 28rpx; color: #4A3728; }
+.chip-group { display: flex; flex-wrap: wrap; gap: 16rpx; }
+.chip { padding: 12rpx 28rpx; background: #FFF3E6; color: #6B5744; border-radius: 999rpx; font-size: 24rpx; border: 2rpx solid transparent; }
+.chip-active { background: #FF8C42; color: #FFFFFF; border-color: #FF8C42; font-weight: 600; }
+.custom-row { display: flex; align-items: center; margin-top: 16rpx; gap: 16rpx; }
+.custom-input { flex: 1; background: #FFF3E6; border-radius: 16rpx; padding: 16rpx 20rpx; font-size: 24rpx; color: #4A3728; }
+.custom-confirm { padding: 12rpx 24rpx; background: #FF8C42; color: #FFF; border-radius: 16rpx; font-size: 24rpx; }
+.custom-cancel { padding: 12rpx 24rpx; background: #F0E6DC; color: #6B5744; border-radius: 16rpx; font-size: 24rpx; }
+.custom-add { margin-top: 16rpx; font-size: 24rpx; color: #FF8C42; }
+.add-row { display: flex; align-items: center; justify-content: center; padding: 24rpx; border: 2rpx dashed #FFD4B8; border-radius: 24rpx; color: #FF8C42; font-size: 26rpx; }
+.add-icon { margin-right: 8rpx; }
+.pref-none-row { margin-bottom: 24rpx; }
+.pref-none-btn { width: 100%; background: #FFF3E6; color: #6B5744; border-radius: 999rpx; font-size: 26rpx; padding: 16rpx; border: 2rpx solid #FFD4B8; }
+.pref-none-btn-active { background: #FF8C42; color: #FFF; border-color: #FF8C42; }
+.struct-row { margin-bottom: 20rpx; }
+.struct-label { display: block; font-size: 24rpx; color: #6B5744; margin-bottom: 12rpx; }
+.slider-row { display: flex; align-items: center; gap: 24rpx; }
+.slider-val { font-size: 32rpx; font-weight: 700; color: #FF8C42; width: 48rpx; }
+.checkbox-row { display: flex; align-items: center; gap: 16rpx; }
+.checkbox { width: 40rpx; height: 40rpx; border-radius: 8rpx; border: 2rpx solid #FFD4B8; display: flex; align-items: center; justify-content: center; color: #FFF; font-size: 26rpx; background: #FFF; }
+.checkbox-on { background: #FF8C42; border-color: #FF8C42; }
+.checkbox-label { font-size: 26rpx; color: #4A3728; }
+.footer-bar { position: fixed; left: 0; right: 0; bottom: 0; padding: 20rpx 32rpx calc(20rpx + env(safe-area-inset-bottom)); background: #FFFFFF; box-shadow: 0 -4rpx 20rpx rgba(0,0,0,0.06); display: flex; gap: 20rpx; }
+.btn-prev, .btn-next, .btn-done { flex: 1; border-radius: 999rpx; font-size: 28rpx; font-weight: 600; padding: 20rpx 0; background: #FFF3E6; color: #6B5744; }
+.btn-next, .btn-done { background: #FF8C42; color: #FFFFFF; }
+.btn-done[disabled] { opacity: 0.6; }
+</style>
+```
+
+**关键实现说明(给执行者的补充,非代码):**
+
+1. **工作副本模式**:`innerForm`/`innerDiet` 是 props 的浅拷贝(watch+created 初始化),用户在 UI 上编辑的是内部副本;`syncToParent()` 把 patch 推回父页。`lifestyle` 同理,但它在 `buildLifestyleDiff` 时对比原始 props `form.lifestyle` 得到 diff(未修改的 key 不 emit)。这一步对应设计 §5.2「lifestyle JSON merge」的 diff 来源。
+2. **疾病 8 组 54 项**:`diseaseGroups` 数组必须覆盖旧 `MedicalHistoryStep.vue` 的全部预置疾病——执行时对照 `cfc-frontend/pages/health/components/MedicalHistoryStep.vue` 的 `diseaseGroups` 常量逐项核对,**两个来源必须一致**(含 name/label 字段名)。
+3. **`med` 模块的时长计算**:`durationText` 为后端落库字段,若需要自动计算("已服 X 年"),执行时对照旧 MedicalHistoryStep 的 durationText 生成逻辑补一个 computed 或在提交前填充;若旧实现也是纯手工输入则保持纯输入。
+4. **`basic` 的 `lipidCholesterol`**:扁平输入值,提交时转 `bloodLipids` JSON 数组(沿用 health-plan-summary `buildHealthPayload` 的格式),父页 receive patch 字段名为 `bloodLipids`。
+5. **`selectedDiseases`/`medRows`/`noMedication` 三个 key** 存在于 `innerForm`(父页 memberFormMap 快照同构),`buildFormPatch` 转 JSON 字符串后以 `diseaseHistory`/`medications` 发出。父页只需把 patch 合到 `form` 快照。
+6. **空提交拦截语义**:仅当 patch 和 diff 全部为空时拦截(设计 §5.3),单模块空提交允许。
+7. **模板禁项自查**(必须逐条过):
+   - 无 `?.`(用 `&&`)✓
+   - `:key` 无表达式(用常量字符串 / 索引)
+
+### 步骤 1.2:语法校验
+
+```bash
+cd /sc-data/cfc/cfc-frontend
+node -e "
+var fs = require('fs')
+var src = fs.readFileSync('pages/health/components/ComprehensiveSurvey.vue', 'utf8')
+var m = src.match(/<script>([\s\S]*?)<\/script>/)
+require('fs').writeFileSync('/tmp/opencode/_survey_check.js', m[1])
+"
+node --check /tmp/opencode/_survey_check.js && echo "SYNTAX OK"
+```
+
+预期输出 `SYNTAX OK`。
+
+### 步骤 1.3:禁项扫描
+
+```bash
+cd /sc-data/cfc/cfc-frontend
+# 模板禁项:可选链 / :key 表达式 / 中文类名
+grep -nE '\?\.[a-zA-Z]' pages/health/components/ComprehensiveSurvey.vue || echo "PASS: no optional chaining"
+grep -nE ':key="[^"]*(\|\||&&|\?)' pages/health/components/ComprehensiveSurvey.vue || echo "PASS: no key expression"
+grep -nE '\.[\x{4e00}-\x{9fa5}]' -P pages/health/components/ComprehensiveSurvey.vue || echo "PASS: no chinese class"
+```
+
+三条均输出 PASS 才算通过(若无 `-P` 支持可省略第三条,改用人眼核对 `<style>` 段类名全英文)。
+
+### 步骤 1.4:提交
+
+```bash
+git add cfc-frontend/pages/health/components/ComprehensiveSurvey.vue
+git commit -m "feat(health): 新增 ComprehensiveSurvey 综合调研组件(8模块step+合并式增量保存)"
+```
+
+---
+
+## 任务 2:新增独立调研页 + 注册 + 两处入口改造
+
+**文件:**
+- 创建:`cfc-frontend/pages/health/health-survey.vue`
+- 修改:`cfc-frontend/pages.json`(注册页)
+- 修改:`cfc-frontend/pages/index-home/index.vue`(goSelfTest 无待填分支)
+- 修改:`cfc-frontend/pages/health-main/index.vue`(goSelfTest)
+
+**目标:** 从首页进入独立调研页(仅调研,不生成方案),URL 控制模块子集。
+
+---
+
+### 步骤 2.1:创建 `health-survey.vue`
+
+创建 `cfc-frontend/pages/health/health-survey.vue`:
+
+```vue
+<template>
+  <view class="survey-page">
+    <!-- 加载中 -->
+    <view v-if="loading" class="loading-state">
+      <text class="loading-text">加载中...</text>
+    </view>
+    <!-- 调研组件 -->
+    <comprehensive-survey
+      v-else
+      :modules="activeModules"
+      :form="formSnapshot"
+      :diet="dietSnapshot"
+      :profile="memberProfile"
+      :submitting="submitting"
+      @update-form="onUpdateForm"
+      @update-diet="onUpdateDiet"
+      @submit="onSubmit"
+    />
+  </view>
+</template>
+
+<script>
+import { getHealthStatus, saveHealthStatus, getDietPreferences, saveDietPreferences, getMyMembership } from '../../utils/api'
+import ComprehensiveSurvey from '@/pages/health/components/ComprehensiveSurvey.vue'
+
+var ALL_MODULES = ['basic', 'status', 'diet', 'disease', 'bowel', 'sleep', 'med', 'allergy']
+
+export default {
+  components: { ComprehensiveSurvey },
+  data: function() {
+    return {
+      loading: true,
+      submitting: false,
+      activeModules: ALL_MODULES.slice(),
+      familyMemberId: '',
+      formSnapshot: {},
+      dietSnapshot: {},
+      memberProfile: {},
+      _loaded: false
+    }
+  },
+  onLoad: function(options) {
+    // 1. 解析 modules(缺省/空串/全非法 → 全量)
+    var modules = options && options.modules
+    if (modules) {
+      var list = String(modules).split(',').filter(function(m) { return ALL_MODULES.indexOf(m) >= 0 })
+      if (list.length > 0) this.activeModules = list
+    }
+    // 2. 当前成员快照(与 preferences.vue 一致)
+    this.familyMemberId = uni.getStorageSync('currentChildId') || uni.getStorageSync('currentMemberId') || uni.getStorageSync('userId') || ''
+    // 3. 会员门槛(与 health-plan-summary checkQuestionnaireGate 语义一致)
+    this.checkGateAndLoad()
+  },
+  methods: {
+    // 会员门槛:未登录静默/付费墙引导(复用 health-plan-summary 逻辑)
+    checkGateAndLoad: function() {
+      var self = this
+      getMyMembership().then(function(res) {
+        var hasMember = res && res.code === 200 && res.data &&
+          (res.data.isFamilyMember || res.data.hasHealthPlan || res.data.isMember || res.data.level)
+        if (res && res.code !== 200) {
+          self.loading = false
+          self.showPaywall()
+          return
+        }
+        if (!hasMember) {
+          self.loading = false
+          self.showPaywall()
+          return
+        }
+        self.loadData()
+      }).catch(function() {
+        // 网络失败放行(不阻断调研,保存时再让后端校验)
+        self.loadData()
+      })
+    },
+    showPaywall: function() {
+      var self = this
+      uni.showModal({
+        title: '开通 9.9 元/月体验会员',
+        content: '填写健康调研需先开通体验会员(9.9 元/月),开通后可填写并保存。',
+        confirmText: '去开通',
+        cancelText: '再想想',
+        success: function(r) {
+          if (r.confirm) {
+            uni.setStorageSync('SURVEY_RESUME_AFTER_PURCHASE', '1')
+            uni.navigateTo({ url: '/pages/membership/pay?levelCode=BASIC&period=monthly' })
+          } else {
+            uni.navigateBack()
+          }
+        }
+      })
+    },
+    // 加载快照(health_status + diet_preferences 并行)
+    loadData: function() {
+      var self = this
+      self.loading = true
+      var tasks = []
+      tasks.push(getHealthStatus(self.familyMemberId || undefined).then(function(res) {
+        var d = (res && res.code === 200 && res.data) ? res.data : null
+        self.formSnapshot = d ? {
+          heightCm: d.heightCm || '',
+          weightKg: d.weightKg || '',
+          bloodPressure: d.bloodPressure || '',
+          bloodGlucose: d.bloodGlucose || '',
+          lipidCholesterol: parseLipid(d.bloodLipids),
+          notes: d.notes || '',
+          selectedDiseases: parseDiseases(d.diseaseHistory),
+          medRows: parseMeds(d.medications),
+          noMedication: false,
+          lifestyle: d.lifestyle ? parseLifestyle(d.lifestyle) : {}
+        } : { selectedDiseases: [], medRows: [], noMedication: false, lifestyle: {} }
+        // 成员画像(性别/年龄)
+        self.memberProfile = {
+          gender: d && (d.gender || d.sex) || '',
+          age: d && d.age != null ? d.age : ''
+        }
+      }).catch(function() {}))
+      tasks.push(getDietPreferences({ memberId: self.familyMemberId }).then(function(res) {
+        var d = (res && res.code === 200 && res.data) ? res.data : null
+        self.dietSnapshot = normalizeDiet(d)
+      }).catch(function() { self.dietSnapshot = normalizeDiet(null) }))
+      Promise.all(tasks).then(function() {
+        self.loading = false
+        self._loaded = true
+      })
+    },
+    // 组件 patch → 合并到快照
+    onUpdateForm: function(patch) {
+      var self = this
+      Object.keys(patch || {}).forEach(function(k) {
+        self.$set(self.formSnapshot, k, patch[k])
+      })
+    },
+    onUpdateDiet: function(patch) {
+      var self = this
+      Object.keys(patch || {}).forEach(function(k) {
+        self.$set(self.dietSnapshot, k, patch[k])
+      })
+    },
+    // 提交:health_status 侧 merge lifestyle + diet_preferences 侧
+    onSubmit: function(payload) {
+      var self = this
+      if (self.submitting) return
+      self.submitting = true
+      // 血压格式兜底校验
+      if (payload.form.bloodPressure && !/^\d{2,3}\/\d{2,3}$/.test(payload.form.bloodPressure)) {
+        self.submitting = false
+        uni.showToast({ title: '血压格式应为 120/80', icon: 'none' })
+        return
+      }
+      // 1) health_status payload:启用模块涉及的 form 字段 + lifestyle merge
+      var formFields = ['heightCm', 'weightKg', 'bloodPressure', 'bloodGlucose', 'bloodLipids', 'diseaseHistory', 'medications', 'notes', 'noMedication']
+      var hsPayload = {}
+      formFields.forEach(function(k) {
+        if (payload.form[k] != null && payload.form[k] !== '' && payload.form[k] !== false) hsPayload[k] = payload.form[k]
+      })
+      // lifestyle merge:现有 lifestyle + diff
+      var existing = self.formSnapshot.lifestyle || {}
+      var merged = Object.assign({}, existing)
+      Object.keys(payload.lifestyleDiff || {}).forEach(function(k) { merged[k] = payload.lifestyleDiff[k] })
+      if (Object.keys(merged).length > 0) hsPayload.lifestyle = JSON.stringify(merged)
+      // 2) diet payload:启用模块字段 + allergies(allergy 模块)
+      var dietFields = ['allergies', 'absoluteAvoid', 'mustEat', 'flavorPref', 'cuisinePref', 'cookingMethods', 'healthGoals', 'vegetableFruit', 'dairy', 'sugaryDrinks', 'religiousDiet', 'spiceLevel', 'noPreference']
+      var dietPayload = {}
+      dietFields.forEach(function(k) {
+        if (payload.diet[k] != null && payload.diet[k] !== '' && payload.diet[k] !== false && !(payload.diet[k] instanceof Array && payload.diet[k].length === 0)) {
+          dietPayload[k] = payload.diet[k]
+        }
+      })
+      var saveTasks = []
+      if (Object.keys(hsPayload).length > 0) {
+        hsPayload.memberId = self.familyMemberId
+        saveTasks.push(saveHealthStatus(hsPayload))
+      }
+      if (Object.keys(dietPayload).length > 0) {
+        dietPayload.familyMemberId = self.familyMemberId
+        saveTasks.push(saveDietPreferences(dietPayload))
+      }
+      Promise.all(saveTasks).then(function() {
+        uni.showToast({ title: '已保存', icon: 'success' })
+        self.submitting = false
+        setTimeout(function() { uni.navigateBack() }, 800)
+      }).catch(function(err) {
+        self.submitting = false
+        uni.showToast({ title: (err && err.message) || '保存失败,请重试', icon: 'none' })
+      })
+    },
+    onShow: function() {
+      // 购买返回:恢复调研
+      if (uni.getStorageSync('SURVEY_RESUME_AFTER_PURCHASE') === '1') {
+        uni.removeStorageSync('SURVEY_RESUME_AFTER_PURCHASE')
+        if (!this._loaded) this.checkGateAndLoad()
+      }
+    }
+  }
+}
+
+// ===== 本地工具函数(模块外,避免污染 data) =====
+function parseLipid(json) {
+  try {
+    var arr = JSON.parse(json || '[]')
+    if (arr instanceof Array && arr.length > 0 && arr[0].value) return String(arr[0].value)
+  } catch (e) {}
+  return ''
+}
+function parseDiseases(json) {
+  var list = []
+  try {
+    var arr = JSON.parse(json || '[]')
+    if (arr instanceof Array) {
+      arr.forEach(function(item) {
+        if (item.name) list.push({ name: item.name, diagnosedAt: item.diagnosedAt || '', custom: false })
+      })
+    }
+  } catch (e) {}
+  return list
+}
+function parseMeds(json) {
+  var list = []
+  try {
+    var arr = JSON.parse(json || '[]')
+    if (arr instanceof Array) {
+      arr.forEach(function(item) {
+        list.push({ name: item.name || '', startDate: item.startDate || '', durationText: item.durationText || '', time: item.time || '' })
+      })
+    }
+  } catch (e) {}
+  return list
+}
+function parseLifestyle(json) {
+  var obj = {}
+  try {
+    var parsed = JSON.parse(json)
+    if (parsed && typeof parsed === 'object') {
+      var keys = ['mood', 'sleepDuration', 'bedtime', 'bowelFrequency', 'bowelForm', 'antibioticHistory',
+        'currentStatus', 'exerciseFrequency', 'exerciseDuration',
+        'allergySymptoms', 'menstrualCycle', 'specialPeriod', 'growthDevelopment',
+        'pickyEating', 'osteoporosisRisk', 'fallRisk', 'screenTime', 'teenMood']
+      keys.forEach(function(k) { obj[k] = parsed[k] || (Array.isArray(parsed[k]) ? [] : '') })
+    }
+  } catch (e) {}
+  return obj
+}
+function normalizeDiet(d) {
+  return {
+    allergies: (d && d.allergies) || [],
+    absoluteAvoid: (d && d.absoluteAvoid) || [],
+    mustEat: (d && d.mustEat) || [],
+    religiousDiet: (d && d.religiousDiet) || 'none',
+    spiceLevel: (d && d.spiceLevel) || 0,
+    flavorPref: (d && d.flavorPref) || [],
+    cuisinePref: (d && d.cuisinePref) || [],
+    cookingMethods: (d && d.cookingMethods) || [],
+    healthGoals: (d && d.healthGoals) || [],
+    noPreference: !!(d && d.noPreference),
+    vegetableFruit: (d && d.vegetableFruit) || '',
+    dairy: (d && d.dairy) || '',
+    sugaryDrinks: (d && d.sugaryDrinks) || ''
+  }
+}
+</script>
+
+<style>
+.survey-page { min-height: 100vh; background: linear-gradient(180deg, #FFF7ED, #FFF3E6); }
+.loading-state { padding: 200rpx 0; text-align: center; }
+.loading-text { font-size: 28rpx; color: #B08968; }
+</style>
+```
+
+**关键实现说明:**
+
+1. **会员门槛对齐**:执行时打开 `health-plan-summary.vue` 的 `checkQuestionnaireGate()`(:752 附近)与 `showPaywall()`(:776 附近),**从后端响应字段判断是否开通**——本页 `checkGateAndLoad` 的判断字段必须与 health-plan-summary 的判定表达式逐字段对齐(`isFamilyMember`/`hasHealthPlan`/`level` 等)。若 health-plan-summary 用的是不同字段名,以它为准改本页。
+2. **`onShow` 购买恢复**:`SURVEY_RESUME_AFTER_PURCHASE` 标记是本页自有约定(不干扰 health-plan-summary 的 `PLAN_RESUME_AFTER_PURCHASE`)。
+3. **首次加载只走一次**:`onLoad` 触发 `checkGateAndLoad()`,未写 `onShow` 无条件刷新(无 `_onLoadFired` 因为本页非 Tab 页;onShow 仅处理购买返回),符合 AGENTS.md 规则 1/4c。
+4. **profile 来源**:`getHealthStatus` 响应若无 gender/age 字段,执行时保留 `gender: '', age: ''` 兜底(条件渲染区全隐藏,已填 lifestyle 值仍保留)。
+
+### 步骤 2.2:`pages.json` 注册
+
+在 `cfc-frontend/pages.json` 的 health 分包(`"root": "pages/health"`)的 `pages` 数组末尾追加(对齐 `health-status-form` 条目格式):
+
+```json
+{
+  "path": "health-survey",
+  "style": {
+    "navigationBarTitleText": "综合健康调研",
+    "navigationStyle": "default"
+  }
+}
+```
+
+```bash
+# 校验 pages.json 仍是合法 JSON
+cd /sc-data/cfc/cfc-frontend
+node -e "JSON.parse(require('fs').readFileSync('pages.json','utf8')); console.log('pages.json VALID')"
+```
+
+### 步骤 2.3:改造 `index-home/index.vue` goSelfTest
+
+文件 `cfc-frontend/pages/index-home/index.vue`,把 `goSelfTest` 的 else 分支(L1646-1649)从跳 `health-plan-summary` 改为跳新调研页(全量 8 模块):
+
+```js
+      } else {
+        // 无待填调研 → 进入独立综合调研(仅调研不生成方案;会员门槛由调研页 onLoad 把关)
+        uni.navigateTo({ url: '/pages/health/health-survey?modules=basic,status,diet,disease,bowel,sleep,med,allergy' })
+      }
+```
+
+> 注意保留 if 分支(`survey-questionnaire`)不变,仅改 else 分支 3 行。`goSelfTest` 上方注释同步更新。
+
+### 步骤 2.4:改造 `health-main/index.vue` goSelfTest
+
+文件 `cfc-frontend/pages/health-main/index.vue` L518:
+
+```js
+    goSelfTest: function() {
+      uni.navigateTo({ url: '/pages/health/health-survey?modules=basic,status,diet,disease,bowel,sleep,med,allergy' })
+    },
+```
+
+### 步骤 2.5:API 去重审计
+
+```bash
+cd /sc-data/cfc/cfc-frontend
+node scripts/audit-duplicate-api-calls.js
+```
+
+要求退出码 0。若 `health-survey.vue` 被审计出 onLoad/onShow 双发告警,按 AGENTS.md 规则 1 加 `_onLoadFired` 标志修正后重跑。
+
+### 步骤 2.6:语法校验
+
+```bash
+cd /sc-data/cfc/cfc-frontend
+node -e "
+var fs = require('fs')
+var src = fs.readFileSync('pages/health/health-survey.vue', 'utf8')
+var m = src.match(/<script>([\s\S]*?)<\/script>/)
+require('fs').writeFileSync('/tmp/opencode/_survey_page_check.js', m[1])
+"
+node --check /tmp/opencode/_survey_page_check.js && echo "SYNTAX OK"
+```
+
+### 步骤 2.7:提交
+
+```bash
+git add cfc-frontend/pages/health/health-survey.vue cfc-frontend/pages.json \
+        cfc-frontend/pages/index-home/index.vue cfc-frontend/pages/health-main/index.vue
+git commit -m "feat(health): 新增独立综合调研页 health-survey + pages.json 注册 + 两处首页入口改造"
+```
+
+---
+
+## 任务 3:方案制定页 Step2 组件替换 + 旧向导标记 DEPRECATED
+
+**文件:**
+- 修改:`cfc-frontend/pages/health/health-plan-summary.vue`
+- 标记:`cfc-frontend/pages/health/components/HealthQuestionnaireWizard.vue` + 5 个子 step
+
+**目标:** 方案制定页 Step2 改用新组件,多成员 tab/快照/保存流程语义不变。
+
+---
+
+### 步骤 3.1:模板替换
+
+`health-plan-summary.vue` 模板 L54-67 的 `<health-questionnaire-wizard>` 整块替换为:
+
+```html
+      <!-- 综合调研(8 模块 step:组件内部管理步骤,多成员 tab 由父页切换传快照) -->
+      <comprehensive-survey
+        :modules="surveyModules"
+        :form="currentMemberSnapshot ? currentMemberSnapshot.form : {}"
+        :diet="currentDiet"
+        :profile="currentMemberProfile"
+        :submitting="loading"
+        @update-form="onSurveyForm"
+        @update-diet="onSurveyDiet"
+        @submit="onSurveySubmit"
+      />
+```
+
+**注意**:`memberFormMap` 每项结构为 `{form:{...}, lipidCholesterol, selectedDiseases, medRows, noMedication, lifestyle}`——设计文档 props 契约写的是 `form: Object` 平铺结构。**执行时按本页快照实际结构传 `:form="currentMemberSnapshot.form"`**,即把快照的 `form` 子对象(含 lifestyle/selectedDiseases/medRows 等)整体作为组件 form prop。这要求组件侧 `buildFormPatch` 读取的是 `innerForm.selectedDiseases/medRows/noMedication/lifestyle`——与任务 1 代码一致。
+
+> **一致性修正说明**:任务 1 组件的 `form` prop 语义为「快照的 form 子对象」(含 `selectedDiseases`/`medRows`/`noMedication`/`lifestyle`/`lipidCholesterol`/扁平体征 6 字段),design spec §5.5 的字段清单已覆盖这些 key,**不需要改 spec**。
+
+### 步骤 3.2:script 修改(import + components + 新增 computed + 事件方法)
+
+**import 区(L188-193)**:新增 ComprehensiveSurvey import,保留 HealthQuestionnaireWizard import 暂不删(用于确认无引用后):
+
+```js
+import MemberChipPicker from '@/pages/health/components/MemberChipPicker.vue'
+import HealthQuestionnaireWizard from '@/pages/health/components/HealthQuestionnaireWizard.vue'
+import ComprehensiveSurvey from '@/pages/health/components/ComprehensiveSurvey.vue'
+
+export default {
+  components: { MemberChipPicker, HealthQuestionnaireWizard, ComprehensiveSurvey },
+```
+
+**data 区**:新增 `surveyModules` 常量(全量 8 模块):
+
+```js
+      // Step2 综合调研模块(方案制定前完整调研,全量 8 模块)
+      surveyModules: ['basic', 'status', 'diet', 'disease', 'bowel', 'sleep', 'med', 'allergy'],
+```
+
+**computed 区新增 3 个**:
+
+```js
+    // 当前填写成员的 health 快照
+    currentMemberSnapshot: function() {
+      return this.memberFormMap[String(this.activeFormMemberId || '__account')] || null
+    },
+    // 当前成员的 form 子对象(组件 form prop)
+    currentMemberForm: function() {
+      var s = this.currentMemberSnapshot
+      return (s && s.form) || {
+        heightCm: '', weightKg: '', bloodPressure: '', bloodGlucose: '', notes: '',
+        lipidCholesterol: '', selectedDiseases: [], medRows: [], noMedication: false,
+        lifestyle: {}
+      }
+    },
+    // 当前成员的 diet 快照
+    currentDiet: function() {
+      return this.memberDietMap[String(this.activeFormMemberId || '__account')] || null
+    },
+    // 当前成员画像(性别/年龄,供组件条件渲染;数据源见 selectedMembers/pickerMembers)
+    currentMemberProfile: function() {
+      var m = this.selectedMembers.filter(function(x) { return String(x.memberId) === String(this.activeFormMemberId) }, this)
+      var one = m.length > 0 ? m[0] : null
+      return { gender: (one && (one.gender || one.sex)) || '', age: (one && one.age != null) ? one.age : '' }
+    },
+```
+
+> 执行时需核对 `selectedMembers` computed 与 `pickerMembers` 里成员的字段名(`gender`/`sex`/`age`/`birthday`),若成员列表无 age 字段而有 birthday,用 `utils/format.js` 的 `parseDate` 计算年龄;若两者都取不到,保持空串兜底(条件渲染隐藏)。
+
+**methods 区新增/替换(把旧 onWizard* 系列替换为 onSurvey* 系列)**:
+
+```js
+    // ===== 综合调研事件:patch 合并到当前成员快照 =====
+    // form patch:可能是扁平体征(heightCm 等)、JSON 串(diseaseHistory/medications)、
+    // 或结构化 key(selectedDiseases/medRows/noMedication)。统一处理两类:
+    onSurveyForm: function(patch) {
+      var key = String(this.activeFormMemberId || '__account')
+      var prev = this.memberFormMap[key] || { form: {}, lifestyle: {} }
+      var newForm = Object.assign({}, prev.form)
+      // 扁平/JSON 字段直接进 form
+      var formKeys = ['heightCm', 'weightKg', 'bloodPressure', 'bloodGlucose', 'notes', 'diseaseHistory', 'medications', 'bloodLipids', 'noMedication', 'selectedDiseases', 'medRows', 'lipidCholesterol']
+      var self = this
+      formKeys.forEach(function(k) {
+        if (patch[k] != null) newForm[k] = patch[k]
+      })
+      this.$set(this.memberFormMap, key, Object.assign({}, prev, { form: newForm }))
+    },
+    // diet patch 合并到当前成员 diet 快照
+    onSurveyDiet: function(patch) {
+      var key = String(this.activeFormMemberId || '__account')
+      var prev = this.memberDietMap[key] || this.normalizeDiet(null)
+      this.$set(this.memberDietMap, key, Object.assign({}, prev, patch))
+    },
+    // lifestyle merge:把 diff 合并进当前成员 lifestyle 后整体回写快照
+    onSurveyLifestyle: function(diff) {
+      var key = String(this.activeFormMemberId || '__account')
+      var prev = this.memberFormMap[key] || { form: {}, lifestyle: {} }
+      var merged = Object.assign({}, prev.lifestyle || {}, diff || {})
+      this.$set(this.memberFormMap, key, Object.assign({}, prev, { lifestyle: merged }))
+    },
+    // 提交:走原 saveAndGenerate(血压校验 + Promise.all 保存 + generatePlan 不变)
+    onSurveySubmit: function(payload) {
+      var self = this
+      // 组件 emit 前已做空提交拦截,此处把 lifestyle diff 合并后触发保存
+      if (payload && payload.lifestyleDiff) self.onSurveyLifestyle(payload.lifestyleDiff)
+      // 同步 form/diet 快照的最终值(组件工作副本可能比 patch 更新)
+      if (payload && payload.form) self.onSurveyForm(payload.form)
+      if (payload && payload.diet) self.onSurveyDiet(payload.form ? payload.form : null)  // 见下方修正
+      self.saveAndGenerate()
+    },
+```
+
+> **⚠️ 执行修正(任务1 组件 emit 契约落地)**:任务 1 组件的 `submit` emit 的 `payload.form` 是完整工作副本(含所有结构化 key),不是 patch。所以 `onSurveySubmit` 不需要拆 patch,直接:
+> ```js
+> onSurveySubmit: function(payload) {
+>   var key = String(this.activeFormMemberId || '__account')
+>   var prev = this.memberFormMap[key] || { form: {}, lifestyle: {} }
+>   var mergedLifestyle = Object.assign({}, prev.lifestyle || {}, payload.lifestyleDiff || {})
+>   this.$set(this.memberFormMap, key, {
+>     form: payload.form || prev.form,
+>     lifestyle: mergedLifestyle
+>   })
+>   if (payload.diet) this.$set(this.memberDietMap, key, payload.diet)
+>   this.saveAndGenerate()
+> },
+> ```
+> **同时**:`buildHealthPayload`(L552-593)的 `lifestyleKeys` 数组必须补 3 个新 key:
+> ```js
+> var lifestyleKeys = ['mood', 'sleepDuration', 'bedtime', 'bowelFrequency', 'bowelForm', 'antibioticHistory', 'allergySymptoms', 'menstrualCycle', 'specialPeriod', 'growthDevelopment', 'pickyEating', 'osteoporosisRisk', 'fallRisk', 'screenTime', 'teenMood', 'currentStatus', 'exerciseFrequency', 'exerciseDuration']
+> ```
+> 且 `parseLifestyle`(L607-630)同样补这 3 个 key 的解析(否则回填丢失)。
+
+**删除/保留旧的 onWizard\* 方法**:`onWizardVitals`/`onWizardMedical`/`onWizardLifestyle`/`onWizardDiet`/`onWizardSubmit`/`mergeWizardForm` 共 6 个方法(L512-551)**全部删除**(新组件不再 emit 旧事件)。执行时逐个删除,确保无残留引用:
+
+```bash
+grep -n 'onWizard\|mergeWizardForm' cfc-frontend/pages/health/health-plan-summary.vue
+```
+
+预期无输出。若模板或 `$refs.wizard` 仍有引用,一并清理。
+
+### 步骤 3.3:旧向导标记 DEPRECATED(6 个文件)
+
+给以下 6 个文件**文件头第一行**(`<template>` 之前)插入同一句注释:
+
+```html
+<!-- DEPRECATED: 已由 ComprehensiveSurvey.vue 替代,待确认无引用后清理(2026-10-16 标记) -->
+```
+
+1. `cfc-frontend/pages/health/components/HealthQuestionnaireWizard.vue`
+2. `cfc-frontend/pages/health/components/BasicVitalsStep.vue`
+3. `cfc-frontend/pages/health/components/MedicalHistoryStep.vue`
+4. `cfc-frontend/pages/health/components/LifestyleStep.vue`
+5. `cfc-frontend/pages/health/components/DietHabitsStep.vue`
+6. `cfc-frontend/pages/health/components/ConfirmStep.vue`(以实际文件列表为准,执行时 `ls cfc-frontend/pages/health/components/` 核对旧 wizard 的子组件集合)
+
+> 执行时先确认第 6 个文件名:`ls cfc-frontend/pages/health/components/`;旧 wizard import 了哪些子 step,就标记哪些。**只加注释,不删任何代码行**。
+
+### 步骤 3.4:全量引用检查(旧 wizard 必须零引用)
+
+```bash
+cd /sc-data/cfc/cfc-frontend
+grep -rn 'HealthQuestionnaireWizard\|health-questionnaire-wizard' pages/ components/ utils/ || echo "PASS: no wizard reference"
+```
+
+预期 `PASS: no wizard reference`。若有残留(如 `$refs.wizard`),清理后重跑。
+
+### 步骤 3.5:语法校验 + 审计
+
+```bash
+cd /sc-data/cfc/cfc-frontend
+for f in pages/health/health-plan-summary.vue pages/health/components/HealthQuestionnaireWizard.vue pages/health/components/BasicVitalsStep.vue pages/health/components/MedicalHistoryStep.vue pages/health/components/LifestyleStep.vue pages/health/components/DietHabitsStep.vue; do
+  node -e "
+  var fs = require('fs')
+  var src = fs.readFileSync('$f', 'utf8')
+  var m = src.match(/<script>([\s\S]*?)<\/script>/)
+  require('fs').writeFileSync('/tmp/opencode/_chk.js', m[1])
+  "
+  node --check /tmp/opencode/_chk.js && echo "OK: $f" || echo "FAIL: $f"
+done
+node scripts/audit-duplicate-api-calls.js && echo "AUDIT PASS"
+```
+
+### 步骤 3.6:提交
+
+```bash
+git add cfc-frontend/pages/health/health-plan-summary.vue \
+        cfc-frontend/pages/health/components/HealthQuestionnaireWizard.vue \
+        cfc-frontend/pages/health/components/BasicVitalsStep.vue \
+        cfc-frontend/pages/health/components/MedicalHistoryStep.vue \
+        cfc-frontend/pages/health/components/LifestyleStep.vue \
+        cfc-frontend/pages/health/components/DietHabitsStep.vue
+git commit -m "refactor(health): 方案制定页 Step2 替换 ComprehensiveSurvey,旧向导标记 DEPRECATED"
+```
+
+---
+
+## 四、验证清单(全部完成后逐项跑)
+
+> **验证前提**:HBuilderX 重新打包(改代码后必须重新运行,否则旧产物白板/旧行为)。Agent 只做语法/结构校验,不打包。
+
+### A. 语法与静态检查
+
+1. `node --check` 对 3 个改动/新增页(health-survey.vue / ComprehensiveSurvey.vue / health-plan-summary.vue)全部通过
+2. `pages.json` JSON.parse 通过
+3. `node scripts/audit-duplicate-api-calls.js` 退出码 0
+4. 全仓 grep 无 `HealthQuestionnaireWizard` 引用
+
+### B. 手工验证(对照设计 §九)
+
+| # | 场景 | 预期 |
+|---|------|------|
+| 1 | 首页 `index-home` 无待填调研点自测大卡 | 跳 `/pages/health/health-survey?modules=basic,status,diet,disease,bowel,sleep,med,allergy`,8 step 流 |
+| 2 | `health-main` 自检卡点击 | 同上 |
+| 3 | 首页有待填调研(pendingSurveys 非空) | 仍跳 `survey-questionnaire`(现状保留) |
+| 4 | URL `modules=diet,bowel` | 仅 2 个 step(第三步完成后底部显示「完成」) |
+| 5 | 每个模块的交互(chip/滑块/动态行/分组折叠/自定义添加/无特殊偏好一键清空) | 与 preferences.vue / 旧 wizard 体验一致,无卡死 |
+| 6 | 保存独立调研 | health_status + diet_preferences 正确落库;回填一致 |
+| 7 | **增量验证** | 先填疾病史+用药保存 → 再只调研「运动」(`modules=bowel`)保存 → 疾病史/用药数据仍在(未被 null 覆盖) |
+| 8 | 过敏源模块 | `diet_preferences.allergies` 与 `lifestyle.allergySymptoms` 双写均有数据 |
+| 9 | 饮食结构 3 项 | vegetableFruit/dairy/sugaryDrinks 落库且回填 |
+| 10 | 方案制定页 Step2 | 多成员 tab 切换、各成员快照独立、生成方案成功、跳过问卷路径不变 |
+| 11 | 空提交 | 所有启用模块均未填 → toast「请至少填写一项」 |
+| 12 | 非法值 | 身高 999 / 血压 abc 被前端拦截 |
+| 13 | 会员门槛 | 非会员进独立调研页 → 弹付费墙 → 去开通返回后自动恢复 |
+
+### C. 后端零改动确认
+
+```bash
+cd /sc-data/cfc && git diff --stat HEAD~5 -- cfc-backend/ && echo "--- 若上面为空即零后端改动 ---"
+```
+
+---
+
+## 五、风险与回滚
+
+| 风险 | 应对 |
+|------|------|
+| `memberFormMap` 快照结构与组件 `form` prop 契约不符导致 Step2 空白 | 任务 3.1 已明确传 `form` 子对象;验证清单 B10 覆盖 |
+| lifestyle 3 个新 key 在 LangGraph 侧无消费导致报错 | 设计已确认后端/ LangGraph 读不到新 key 时静默跳过(JSON 自由 key 无 schema 约束);不新增 LangGraph 改动 |
+| 旧 wizard 删除过早导致回归 | 仅标记 DEPRECATED,不删文件;后续单独清理任务 |
+| 会员字段判定与 health-plan-summary 不一致 | 步骤 2.1 说明已要求逐字段对齐;验证 B13 覆盖 |
+| 增量保存被 null 覆盖 | 依赖 MP `updateById` 非 null 更新语义;验证 B7 专项覆盖 |
+
+**回滚**:每个任务独立 commit。回滚 `git revert <commit>` 即可,无 DDL、无后端改动,数据层无损。
+
+---
+
+## 六、文档同步(计划完成后)
+
+1. `docs/superpowers/PROJECT-OVERVIEW.md`:把综合调研特性从"设计中"改为"已实现"条目,登记计划文档索引
+2. 设计文档 `2026-10-16-comprehensive-health-survey-design.md` 状态从「待审查」改为「已实现」