Sfoglia il codice sorgente

feat(health): 新增 ComprehensiveSurvey 综合调研组件(8模块step+合并式增量保存)(任务1/3)

iwt 1 settimana fa
parent
commit
b5578f5d2
1 ha cambiato i file con 1108 aggiunte e 0 eliminazioni
  1. 1108 0
      cfc-frontend/pages/health/components/ComprehensiveSurvey.vue

+ 1108 - 0
cfc-frontend/pages/health/components/ComprehensiveSurvey.vue

@@ -0,0 +1,1108 @@
+<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="m"
+              class="progress-label" :class="{ 'progress-label-active': currentStep > i }">{{ moduleTitles[m] }}</text>
+      </view>
+    </view>
+
+    <!-- 步骤内容 -->
+    <view class="content">
+      <!-- ===== Step: 基本情况 basic ===== -->
+      <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-head"><text class="card-title">身高(cm)</text>
+            <view class="quick-chips">
+              <view class="quick-chip" :class="{ 'quick-chip-on': heightQuick === 'normal' }" @tap="setHeightQuick('normal')"><text>正常</text></view>
+              <view class="quick-chip" :class="{ 'quick-chip-on': heightQuick === 'high' }" @tap="setHeightQuick('high')"><text>偏高</text></view>
+              <view class="quick-chip" :class="{ 'quick-chip-on': heightQuick === 'low' }" @tap="setHeightQuick('low')"><text>偏低</text></view>
+            </view>
+          </view>
+          <input class="num-input" type="digit" v-model="innerForm.heightCm" placeholder="如 170" />
+          <text class="unit-hint">单位:厘米</text>
+        </view>
+        <view class="card">
+          <view class="card-head"><text class="card-title">体重(kg)</text>
+            <view class="quick-chips">
+              <view class="quick-chip" :class="{ 'quick-chip-on': weightQuick === 'normal' }" @tap="setWeightQuick('normal')"><text>正常</text></view>
+              <view class="quick-chip" :class="{ 'quick-chip-on': weightQuick === 'high' }" @tap="setWeightQuick('high')"><text>偏重</text></view>
+              <view class="quick-chip" :class="{ 'quick-chip-on': weightQuick === 'low' }" @tap="setWeightQuick('low')"><text>偏轻</text></view>
+            </view>
+          </view>
+          <input class="num-input" type="digit" v-model="innerForm.weightKg" placeholder="如 60" />
+          <text class="unit-hint">单位:公斤</text>
+        </view>
+        <view class="card">
+          <view class="card-head"><text class="card-title">血压</text>
+            <view class="quick-chips">
+              <view class="quick-chip" :class="{ 'quick-chip-on': bpQuick === 'normal' }" @tap="setBpQuick('normal')"><text>正常</text></view>
+              <view class="quick-chip" :class="{ 'quick-chip-on': bpQuick === 'high' }" @tap="setBpQuick('high')"><text>偏高</text></view>
+              <view class="quick-chip" :class="{ 'quick-chip-on': bpQuick === 'low' }" @tap="setBpQuick('low')"><text>偏低</text></view>
+            </view>
+          </view>
+          <input class="num-input" type="text" v-model="innerForm.bloodPressure" placeholder="如 120/80" />
+          <text class="unit-hint">格式:收缩压/舒张压(mmHg)</text>
+        </view>
+        <view class="card">
+          <view class="card-head"><text class="card-title">空腹血糖(mmol/L)</text>
+            <view class="quick-chips">
+              <view class="quick-chip" :class="{ 'quick-chip-on': glucoseQuick === 'normal' }" @tap="setGlucoseQuick('normal')"><text>正常</text></view>
+              <view class="quick-chip" :class="{ 'quick-chip-on': glucoseQuick === 'high' }" @tap="setGlucoseQuick('high')"><text>偏高</text></view>
+              <view class="quick-chip" :class="{ 'quick-chip-on': glucoseQuick === 'low' }" @tap="setGlucoseQuick('low')"><text>偏低</text></view>
+            </view>
+          </view>
+          <input class="num-input" type="digit" v-model="innerForm.bloodGlucose" placeholder="如 5.0" />
+        </view>
+        <view class="card">
+          <view class="card-head"><text class="card-title">总胆固醇(mmol/L)</text>
+            <view class="quick-chips">
+              <view class="quick-chip" :class="{ 'quick-chip-on': lipidQuick === 'normal' }" @tap="setLipidQuick('normal')"><text>正常</text></view>
+              <view class="quick-chip" :class="{ 'quick-chip-on': lipidQuick === 'high' }" @tap="setLipidQuick('high')"><text>偏高</text></view>
+            </view>
+          </view>
+          <input class="num-input" type="digit" v-model="innerForm.lipidCholesterol" placeholder="如 4.5" />
+        </view>
+        <view class="card">
+          <view class="card-head"><text class="card-title">其他补充(选填)</text></view>
+          <textarea class="notes-area" v-model="innerForm.notes" placeholder="其他想补充的健康信息" :maxlength="500" />
+        </view>
+      </view>
+
+      <!-- ===== Step: 当前状态 status ===== -->
+      <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">
+          <view class="card-head"><text class="card-title">特殊状态</text><text class="card-hint">单选</text></view>
+          <view class="chip-row-wrap">
+            <view v-for="opt in currentStatusOptions" :key="opt" class="chip-item-wrap"
+                  :class="{ 'chip-active': lifestyle.currentStatus === opt }"
+                  @tap="setLifestyle('currentStatus', opt)"><text>{{ opt }}</text></view>
+          </view>
+        </view>
+        <!-- 女性专属 -->
+        <view v-if="isFemale">
+          <view class="card">
+            <view class="card-head"><text class="card-title">月经周期</text><text class="card-hint">单选</text></view>
+            <view class="chip-row-wrap">
+              <view v-for="opt in menstrualOptions" :key="opt" class="chip-item-wrap"
+                    :class="{ 'chip-active': lifestyle.menstrualCycle === opt }"
+                    @tap="setLifestyle('menstrualCycle', opt)"><text>{{ opt }}</text></view>
+            </view>
+          </view>
+          <view class="card">
+            <view class="card-head"><text class="card-title">特殊时期</text><text class="card-hint">单选</text></view>
+            <view class="chip-row-wrap">
+              <view v-for="opt in specialPeriodOptions" :key="opt" class="chip-item-wrap"
+                    :class="{ 'chip-active': lifestyle.specialPeriod === opt }"
+                    @tap="setLifestyle('specialPeriod', opt)"><text>{{ opt }}</text></view>
+            </view>
+          </view>
+        </view>
+        <!-- 儿童专属 -->
+        <view v-if="isChild">
+          <view class="card">
+            <view class="card-head"><text class="card-title">生长发育</text><text class="card-hint">单选</text></view>
+            <view class="chip-row-wrap">
+              <view v-for="opt in growthOptions" :key="opt" class="chip-item-wrap"
+                    :class="{ 'chip-active': lifestyle.growthDevelopment === opt }"
+                    @tap="setLifestyle('growthDevelopment', opt)"><text>{{ opt }}</text></view>
+            </view>
+          </view>
+          <view class="card">
+            <view class="card-head"><text class="card-title">挑食情况</text><text class="card-hint">单选</text></view>
+            <view class="chip-row-wrap">
+              <view v-for="opt in pickyEatingOptions" :key="opt" class="chip-item-wrap"
+                    :class="{ 'chip-active': lifestyle.pickyEating === opt }"
+                    @tap="setLifestyle('pickyEating', opt)"><text>{{ opt }}</text></view>
+            </view>
+          </view>
+        </view>
+        <!-- 老年专属 -->
+        <view v-if="isElderly">
+          <view class="card">
+            <view class="card-head"><text class="card-title">骨质疏松风险</text><text class="card-hint">单选</text></view>
+            <view class="chip-row-wrap">
+              <view v-for="opt in osteoporosisOptions" :key="opt" class="chip-item-wrap"
+                    :class="{ 'chip-active': lifestyle.osteoporosisRisk === opt }"
+                    @tap="setLifestyle('osteoporosisRisk', opt)"><text>{{ opt }}</text></view>
+            </view>
+          </view>
+          <view class="card">
+            <view class="card-head"><text class="card-title">跌倒风险</text><text class="card-hint">单选</text></view>
+            <view class="chip-row-wrap">
+              <view v-for="opt in fallRiskOptions" :key="opt" class="chip-item-wrap"
+                    :class="{ 'chip-active': lifestyle.fallRisk === opt }"
+                    @tap="setLifestyle('fallRisk', opt)"><text>{{ opt }}</text></view>
+            </view>
+          </view>
+        </view>
+        <!-- 青少年专属 -->
+        <view v-if="isTeenager">
+          <view class="card">
+            <view class="card-head"><text class="card-title">屏幕时间</text><text class="card-hint">单选</text></view>
+            <view class="chip-row-wrap">
+              <view v-for="opt in screenTimeOptions" :key="opt" class="chip-item-wrap"
+                    :class="{ 'chip-active': lifestyle.screenTime === opt }"
+                    @tap="setLifestyle('screenTime', opt)"><text>{{ opt }}</text></view>
+            </view>
+          </view>
+          <view class="card">
+            <view class="card-head"><text class="card-title">情绪状态</text><text class="card-hint">单选</text></view>
+            <view class="chip-row-wrap">
+              <view v-for="opt in teenMoodOptions" :key="opt" class="chip-item-wrap"
+                    :class="{ 'chip-active': lifestyle.teenMood === opt }"
+                    @tap="setLifestyle('teenMood', opt)"><text>{{ opt }}</text></view>
+            </view>
+          </view>
+        </view>
+      </view>
+
+      <!-- ===== Step: 饮食偏好 diet ===== -->
+      <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': innerDiet.noPreference }"
+                  @tap="setNoPreference">{{ innerDiet.noPreference ? '已选:无特殊偏好' : '无特殊偏好,直接下一步' }}</button>
+        </view>
+        <view class="card">
+          <view class="card-head"><text class="card-title">绝对忌口</text><text class="card-hint">多选</text></view>
+          <view class="chip-row-wrap">
+            <view v-for="item in presetAvoid" :key="item" class="chip-item-wrap"
+                  :class="{ 'chip-active': innerDiet.absoluteAvoid.indexOf(item) >= 0 }"
+                  @tap="toggleDietChip('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">
+          <view class="card-head"><text class="card-title">必吃食物</text><text class="card-hint">多选</text></view>
+          <view class="chip-row-wrap">
+            <view v-for="item in presetMustEat" :key="item" class="chip-item-wrap"
+                  :class="{ 'chip-active': innerDiet.mustEat.indexOf(item) >= 0 }"
+                  @tap="toggleDietChip('mustEat', item)"><text>{{ item }}</text></view>
+          </view>
+        </view>
+        <view class="card">
+          <view class="card-head"><text class="card-title">口味偏好</text><text class="card-hint">多选</text></view>
+          <view class="chip-row-wrap">
+            <view v-for="item in presetFlavor" :key="item" class="chip-item-wrap"
+                  :class="{ 'chip-active': innerDiet.flavorPref.indexOf(item) >= 0 }"
+                  @tap="toggleDietChip('flavorPref', item)"><text>{{ item }}</text></view>
+          </view>
+        </view>
+        <view class="card">
+          <view class="card-head"><text class="card-title">菜系偏好</text><text class="card-hint">多选</text></view>
+          <view class="chip-row-wrap">
+            <view v-for="item in presetCuisine" :key="item" class="chip-item-wrap"
+                  :class="{ 'chip-active': innerDiet.cuisinePref.indexOf(item) >= 0 }"
+                  @tap="toggleDietChip('cuisinePref', item)"><text>{{ item }}</text></view>
+          </view>
+        </view>
+        <view class="card">
+          <view class="card-head"><text class="card-title">烹饪方式</text><text class="card-hint">多选</text></view>
+          <view class="chip-row-wrap">
+            <view v-for="item in presetCooking" :key="item" class="chip-item-wrap"
+                  :class="{ 'chip-active': innerDiet.cookingMethods.indexOf(item) >= 0 }"
+                  @tap="toggleDietChip('cookingMethods', item)"><text>{{ item }}</text></view>
+          </view>
+        </view>
+        <view class="card">
+          <view class="card-head"><text class="card-title">健康目标</text><text class="card-hint">多选</text></view>
+          <view class="chip-row-wrap">
+            <view v-for="item in presetGoals" :key="item" class="chip-item-wrap"
+                  :class="{ 'chip-active': innerDiet.healthGoals.indexOf(item) >= 0 }"
+                  @tap="toggleDietChip('healthGoals', item)"><text>{{ item }}</text></view>
+          </view>
+        </view>
+        <view class="card">
+          <view class="card-head"><text class="card-title">辣度</text><text class="card-hint">{{ spiceLabels[innerDiet.spiceLevel || 0] }}</text></view>
+          <view class="slider-row">
+            <slider :value="innerDiet.spiceLevel || 0" min="0" max="5" step="1" activeColor="#FF8C42"
+                    @changing="onSpiceChanging" @change="onSpiceChange" />
+          </view>
+        </view>
+        <view class="card">
+          <view class="card-head"><text class="card-title">宗教饮食</text></view>
+          <view class="chip-row-wrap">
+            <view v-for="opt in religiousOptions" :key="opt.value" class="chip-item-wrap"
+                  :class="{ 'chip-active': innerDiet.religiousDiet === opt.value }"
+                  @tap="setDietValue('religiousDiet', opt.value)"><text>{{ opt.label }}</text></view>
+          </view>
+        </view>
+        <view class="card">
+          <view class="card-head"><text class="card-title">饮食结构现状</text><text class="card-hint">单选</text></view>
+          <view class="struct-row"><text class="struct-label">每日蔬果量</text>
+            <view class="chip-row-wrap">
+              <view v-for="opt in vegFruitOptions" :key="opt" class="chip-item-wrap"
+                    :class="{ 'chip-active': innerDiet.vegetableFruit === opt }"
+                    @tap="setDietValue('vegetableFruit', opt)"><text>{{ opt }}</text></view>
+            </view>
+          </view>
+          <view class="struct-row"><text class="struct-label">乳制品摄入</text>
+            <view class="chip-row-wrap">
+              <view v-for="opt in dairyOptions" :key="opt" class="chip-item-wrap"
+                    :class="{ 'chip-active': innerDiet.dairy === opt }"
+                    @tap="setDietValue('dairy', opt)"><text>{{ opt }}</text></view>
+            </view>
+          </view>
+          <view class="struct-row"><text class="struct-label">含糖饮料频率</text>
+            <view class="chip-row-wrap">
+              <view v-for="opt in sugaryDrinkOptions" :key="opt" class="chip-item-wrap"
+                    :class="{ 'chip-active': innerDiet.sugaryDrinks === opt }"
+                    @tap="setDietValue('sugaryDrinks', opt)"><text>{{ opt }}</text></view>
+            </view>
+          </view>
+        </view>
+      </view>
+
+      <!-- ===== Step: 疾病史 disease ===== -->
+      <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">选择过往确诊疾病,可设确诊年月(★ 为重点关注项)</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-row-wrap">
+            <view v-for="(item, ii) in group.items" :key="getDiseaseKey(gi, ii)" class="chip-item-wrap"
+                  :class="{ 'chip-active': selectedDiseaseNames.indexOf(item.name) >= 0 }"
+                  @tap="toggleDisease(item.name)">
+              <text>{{ item.name }}</text>
+              <text v-if="item.risk" class="risk-badge">★</text>
+            </view>
+          </view>
+        </view>
+        <view class="card">
+          <view class="card-head"><text class="card-title">自定义疾病</text></view>
+          <view class="custom-row">
+            <input class="custom-input" type="text" v-model="diseaseInput" placeholder="输入其他疾病" />
+            <view class="custom-confirm" @tap="addCustomDisease"><text>添加</text></view>
+          </view>
+        </view>
+        <view class="card" v-if="innerForm.selectedDiseases.length > 0">
+          <view class="card-head"><text class="card-title">已选疾病({{ innerForm.selectedDiseases.length }})</text></view>
+          <view v-for="(item, si) in innerForm.selectedDiseases" :key="getSelKey(si)" class="sel-row">
+            <text class="sel-name">{{ item.name }}</text>
+            <picker mode="date" fields="month" :value="item.diagnosedAt || currentMonth" :end="monthEnd" @change="onDiagChange(si, $event)">
+              <view class="sel-diag">{{ item.diagnosedAt ? item.diagnosedAt : '设确诊时间' }}</view>
+            </picker>
+            <text class="sel-del" @tap="removeSelected(si)">×</text>
+          </view>
+        </view>
+      </view>
+
+      <!-- ===== Step: 排便及运动 bowel ===== -->
+      <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">
+          <view class="card-head"><text class="card-title">排便频率</text><text class="card-hint">单选</text></view>
+          <view class="chip-row-wrap">
+            <view v-for="opt in bowelFreqOptions" :key="opt" class="chip-item-wrap"
+                  :class="{ 'chip-active': lifestyle.bowelFrequency === opt }"
+                  @tap="setLifestyle('bowelFrequency', opt)"><text>{{ opt }}</text></view>
+          </view>
+        </view>
+        <view class="card">
+          <view class="card-head"><text class="card-title">粪便性状</text><text class="card-hint">单选</text></view>
+          <view class="chip-row-wrap">
+            <view v-for="opt in bowelFormOptions" :key="opt" class="chip-item-wrap"
+                  :class="{ 'chip-active': lifestyle.bowelForm === opt }"
+                  @tap="setLifestyle('bowelForm', opt)"><text>{{ opt }}</text></view>
+          </view>
+        </view>
+        <view class="card">
+          <view class="card-head"><text class="card-title">运动频率</text><text class="card-hint">单选</text></view>
+          <view class="chip-row-wrap">
+            <view v-for="opt in exerciseFreqOptions" :key="opt" class="chip-item-wrap"
+                  :class="{ 'chip-active': lifestyle.exerciseFrequency === opt }"
+                  @tap="setLifestyle('exerciseFrequency', opt)"><text>{{ opt }}</text></view>
+          </view>
+        </view>
+        <view class="card">
+          <view class="card-head"><text class="card-title">每次运动时长</text><text class="card-hint">单选</text></view>
+          <view class="chip-row-wrap">
+            <view v-for="opt in exerciseDurationOptions" :key="opt" class="chip-item-wrap"
+                  :class="{ 'chip-active': lifestyle.exerciseDuration === opt }"
+                  @tap="setLifestyle('exerciseDuration', opt)"><text>{{ opt }}</text></view>
+          </view>
+        </view>
+      </view>
+
+      <!-- ===== Step: 睡眠情况 sleep ===== -->
+      <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">
+          <view class="card-head"><text class="card-title">睡眠时长</text><text class="card-hint">单选</text></view>
+          <view class="chip-row-wrap">
+            <view v-for="opt in sleepDurationOptions" :key="opt" class="chip-item-wrap"
+                  :class="{ 'chip-active': lifestyle.sleepDuration === opt }"
+                  @tap="setLifestyle('sleepDuration', opt)"><text>{{ opt }}</text></view>
+          </view>
+        </view>
+        <view class="card">
+          <view class="card-head"><text class="card-title">入睡时间</text><text class="card-hint">单选</text></view>
+          <view class="chip-row-wrap">
+            <view v-for="opt in bedtimeOptions" :key="opt" class="chip-item-wrap"
+                  :class="{ 'chip-active': lifestyle.bedtime === opt }"
+                  @tap="setLifestyle('bedtime', opt)"><text>{{ opt }}</text></view>
+          </view>
+        </view>
+      </view>
+
+      <!-- ===== Step: 用药情况 med ===== -->
+      <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="quick-chip" :class="{ 'quick-chip-on': innerForm.noMedication }" @tap="toggleNoMedication">
+            <text>{{ innerForm.noMedication ? '✓ 已选' : '无长期用药' }}</text>
+          </view>
+          <text class="unit-hint" v-if="innerForm.noMedication">已标记无长期用药,可跳过药物填写</text>
+        </view>
+        <view v-if="!innerForm.noMedication" v-for="(row, idx) in innerForm.medRows" :key="getMedKey(idx)" class="card">
+          <view class="card-head"><text class="card-title">用药 {{ idx + 1 }}</text>
+            <text class="card-del" @tap="removeMed(idx)">删除</text>
+          </view>
+          <view class="card-row"><text class="card-label">药名</text>
+            <input class="card-input" type="text" v-model="row.name" placeholder="药物品牌,如 立普妥" /></view>
+          <view class="card-row"><text class="card-label">开始年月</text>
+            <picker mode="date" fields="month" :value="row.startDate || currentMonth" :end="monthEnd" @change="onMedStartChange(idx, $event)">
+              <view class="picker-val">{{ row.startDate ? row.startDate : '选择开始日期' }}</view>
+            </picker>
+            <text class="link-forget" @tap="onForgetClick(idx)">记不清</text>
+          </view>
+          <view class="card-row" v-if="row.durationText"><text class="card-label">已服时长</text>
+            <text class="card-value">{{ row.durationText }}</text></view>
+          <view class="card-row"><text class="card-label">服药时间</text>
+            <input class="card-input" type="text" v-model="row.time" placeholder="如 早饭后一次" /></view>
+        </view>
+        <view class="add-row" v-if="!innerForm.noMedication" @tap="addMed"><text class="add-icon">+</text><text class="add-text">添加药物/治疗</text></view>
+      </view>
+
+      <!-- ===== Step: 过敏源 allergy ===== -->
+      <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">
+          <view class="card-head"><text class="card-title">过敏原</text><text class="card-hint">多选</text></view>
+          <view class="chip-row-wrap">
+            <view v-for="item in presetAllergies" :key="item" class="chip-item-wrap"
+                  :class="{ 'chip-active': innerDiet.allergies.indexOf(item) >= 0 }"
+                  @tap="toggleDietChip('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">
+          <view class="card-head"><text class="card-title">过敏表现</text><text class="card-hint">多选</text></view>
+          <view class="chip-row-wrap">
+            <view v-for="opt in allergyOptions" :key="opt" class="chip-item-wrap"
+                  :class="{ 'chip-active': lifestyle.allergySymptoms.indexOf(opt) >= 0 }"
+                  @tap="toggleLifestyleArray('allergySymptoms', opt)"><text>{{ opt }}</text></view>
+          </view>
+        </view>
+      </view>
+    </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>
+</template>
+
+<script>
+// ===== 预设常量(与旧子组件 / preferences.vue 保持一致) =====
+var PRESET_ALLERGIES = ['海鲜', '花生', '乳糖', '鸡蛋', '牛奶', '豆类', '麸质', '坚果', '芒果', '其他']
+var PRESET_AVOID = ['牛肉', '羊肉', '猪肉', '内脏', '油腻', '辛辣', '生冷', '腌制']
+var PRESET_FLAVOR = ['酸', '甜', '咸', '鲜', '苦', '清淡', '浓郁']
+var PRESET_CUISINE = ['川', '粤', '鲁', '淮扬', '日料', '西餐', '东南亚', '韩餐']
+var PRESET_COOKING = ['蒸', '煮', '炒', '烤', '炖', '凉拌', '煎', '炸']
+var PRESET_MUSTEAT = ['蔬菜', '水果', '粗粮', '鱼类', '鸡蛋', '豆制品', '菌菇']
+var PRESET_GOALS = ['减脂', '增肌', '控糖', '肠道调理', '增强免疫']
+var VALID_MODULES = ['basic', 'status', 'diet', 'disease', 'bowel', 'sleep', 'med', 'allergy']
+// 8 组 53 项预置疾病(与旧 MedicalHistoryStep.vue 一致,risk=true 为重点关注项)
+var DISEASE_GROUPS = [
+  { key: 'cardio', title: '心血管代谢', items: [
+    { name: '高血压', risk: false },
+    { name: '糖尿病', risk: false },
+    { name: '血脂异常', risk: false },
+    { name: '冠心病', risk: false },
+    { name: '脑卒中', risk: false },
+    { name: '心力衰竭', risk: false },
+    { name: '心房颤动', risk: false },
+    { name: '慢阻肺', risk: false },
+    { name: '心脑血管疾病', risk: true },
+    { name: 'II型糖尿病', risk: true },
+    { name: '肥胖', risk: true },
+    { name: '超重', risk: false },
+    { name: '高尿酸血症', risk: false },
+    { name: '骨质疏松', risk: false }
+  ]},
+  { key: 'digest', title: '消化系统', items: [
+    { name: '幽门螺杆菌感染', risk: false },
+    { name: '脂肪肝', risk: false },
+    { name: '慢性胃炎', risk: false },
+    { name: '胃食管反流', risk: false },
+    { name: '肠易激综合征', risk: true },
+    { name: '炎症性肠炎', risk: true },
+    { name: '慢性便秘', risk: true },
+    { name: '感染性腹泻', risk: true },
+    { name: '胆结石', risk: false },
+    { name: '炎症性肠病', risk: false },
+    { name: '肝病', risk: true },
+    { name: '胃病', risk: true },
+    { name: '胆病', risk: true },
+    { name: '肾病', risk: true },
+    { name: '腹胀', risk: true }
+  ]},
+  { key: 'immune', title: '过敏免疫', items: [
+    { name: '过敏性鼻炎', risk: true },
+    { name: '哮喘', risk: false },
+    { name: '湿疹', risk: false },
+    { name: '食物过敏', risk: false },
+    { name: '自体免疫病', risk: true }
+  ]},
+  { key: 'neuro', title: '精神神经', items: [
+    { name: '抑郁症', risk: true },
+    { name: '失眠', risk: true },
+    { name: '焦虑障碍', risk: false },
+    { name: '偏头痛', risk: false },
+    { name: '自闭症', risk: true }
+  ]},
+  { key: 'resp', title: '呼吸系统', items: [
+    { name: '肺部感染或疾病', risk: true },
+    { name: '肺部疾病', risk: true }
+  ]},
+  { key: 'tumor', title: '肿瘤', items: [
+    { name: '结直肠癌', risk: true },
+    { name: '肺癌', risk: false },
+    { name: '甲状腺癌', risk: false },
+    { name: '肝癌', risk: false },
+    { name: '胃癌', risk: false },
+    { name: '乳腺癌', risk: false },
+    { name: '食管癌', risk: false }
+  ]},
+  { key: 'child', title: '儿童青少年', items: [
+    { name: '近视', risk: false },
+    { name: '龋齿', risk: false },
+    { name: '注意缺陷多动障碍', risk: false },
+    { name: '甲状腺疾病', risk: true }
+  ]},
+  { key: 'other', title: '其他', items: [
+    { name: '缺铁性贫血', risk: false },
+    { name: '贫血', risk: false }
+  ]}
+]
+// 新采集的 lifestyle key(旧数据里可能没有,默认空值)
+var LIFESTYLE_KEYS = [
+  'currentStatus', 'menstrualCycle', 'specialPeriod', 'growthDevelopment', 'pickyEating',
+  'osteoporosisRisk', 'fallRisk', 'screenTime', 'teenMood',
+  'bowelFrequency', 'bowelForm', 'exerciseFrequency', 'exerciseDuration',
+  'sleepDuration', 'bedtime', 'allergySymptoms',
+  'mood', 'antibioticHistory'
+]
+
+export default {
+  name: 'ComprehensiveSurvey',
+  props: {
+    // 启用模块 key 列表(顺序决定步骤顺序;空/全非法 → 全量 8 模块)
+    modules: { type: Array, default: function() { return [] } },
+    // health_status 快照(结构化字段,lifestyle 为对象非 JSON 串)
+    form: { type: Object, default: function() { return {} } },
+    // diet_preferences 快照(13 key)
+    diet: { type: Object, default: function() { return {} } },
+    // 成员画像(gender/age 驱动条件渲染)
+    profile: { type: Object, default: function() { return {} } },
+    // 外部提交态
+    submitting: { type: Boolean, default: false }
+  },
+  data: function() {
+    return {
+      currentStep: 1,
+      innerForm: {},
+      innerDiet: {},
+      lifestyle: {},
+      currentMonth: '',
+      monthEnd: '',
+      expandedGroups: [],
+      // 自定义输入
+      customField: '',
+      customInput: '',
+      diseaseInput: '',
+      // 快捷标签选中态(仅 UI 提示,值仍以输入框为准)
+      heightQuick: '', weightQuick: '', bpQuick: '', glucoseQuick: '', lipidQuick: '',
+      // 选项常量
+      presetAllergies: PRESET_ALLERGIES,
+      presetAvoid: PRESET_AVOID,
+      presetFlavor: PRESET_FLAVOR,
+      presetCuisine: PRESET_CUISINE,
+      presetCooking: PRESET_COOKING,
+      presetMustEat: PRESET_MUSTEAT,
+      presetGoals: PRESET_GOALS,
+      diseaseGroups: DISEASE_GROUPS,
+      moduleTitles: {
+        basic: '基本', status: '状态', diet: '饮食', disease: '疾病',
+        bowel: '排便', sleep: '睡眠', med: '用药', allergy: '过敏'
+      },
+      currentStatusOptions: ['正常', '哺乳期', '备孕', '怀孕', '住院期', '恢复期', '其他'],
+      menstrualOptions: ['规律(28-32天)', '不规律', '已停经', '尚未开始'],
+      specialPeriodOptions: ['无', '备孕中', '怀孕', '哺乳期'],
+      growthOptions: ['正常(在生长曲线内)', '偏慢', '偏快', '不清楚'],
+      pickyEatingOptions: ['无挑食', '轻微挑食', '明显挑食', '严重挑食'],
+      osteoporosisOptions: ['无风险', '有风险(曾骨折/骨密度低)', '不清楚'],
+      fallRiskOptions: ['近1年无跌倒', '近1年跌倒1次', '近1年跌倒2次及以上'],
+      screenTimeOptions: ['<1小时/天', '1-3小时/天', '3-5小时/天', '>5小时/天'],
+      teenMoodOptions: ['开朗', '一般', '易怒', '低落'],
+      bowelFreqOptions: ['每天1次(正常)', '隔天1次', '每周2-3次', '每周<2次'],
+      bowelFormOptions: ['干硬颗粒', '条状干硬', '正常香蕉状', '软便', '水样'],
+      exerciseFreqOptions: ['不运动', '每周1-2次', '每周3-4次', '每周5次以上'],
+      exerciseDurationOptions: ['30分钟以内', '30-60分钟', '60分钟以上'],
+      sleepDurationOptions: ['<6小时', '6-7小时', '7-8小时', '>8小时'],
+      bedtimeOptions: ['22:00前', '22:00-24:00', '24:00后'],
+      vegFruitOptions: ['<2份', '2-3份', '4份以上'],
+      dairyOptions: ['每天', '每周3-5次', '很少', '不喝'],
+      sugaryDrinkOptions: ['几乎不喝', '每周1-2次', '每周3-5次', '每天1次以上'],
+      allergyOptions: ['无', '反复皮疹', '鼻痒/打喷嚏', '眼痒/流泪', '哮喘样症状', '食物过敏'],
+      spiceLabels: ['不辣', '微辣', '中辣', '偏辣', '很辣', '特辣'],
+      religiousOptions: [
+        { value: 'none', label: '无限制' },
+        { value: 'halal', label: '清真' },
+        { value: 'vegetarian', label: '素食' },
+        { value: 'vegan', label: '纯素' }
+      ]
+    }
+  },
+  computed: {
+    // 过滤非法 key + 兜底全量 + 去重(保持顺序)
+    activeModules: function() {
+      var seen = {}
+      var list = (this.modules || []).filter(function(m) {
+        if (VALID_MODULES.indexOf(m) < 0 || seen[m]) return false
+        seen[m] = true
+        return true
+      })
+      if (list.length === 0) list = VALID_MODULES.slice()
+      return list
+    },
+    currentModule: function() {
+      return this.activeModules[this.currentStep - 1] || ''
+    },
+    progressPercent: function() {
+      return Math.round((this.currentStep / this.activeModules.length) * 100)
+    },
+    // 已选疾病名列表(供模板内联 indexOf,避免 :class 带参方法调用)
+    selectedDiseaseNames: function() {
+      return (this.innerForm.selectedDiseases || []).map(function(d) { return d.name })
+    },
+    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: {
+    // 仅在 prop 对象身份变化时重建工作副本(父页换快照/切成员 tab);
+    // 不用 deep:v-model 逐键输入会变更共享行对象,deep 会每次按键全量重建
+    form: {
+      handler: function(v) { this.initFromProps() },
+      immediate: true
+    },
+    diet: {
+      handler: function(v) { this.syncDiet(v) },
+      immediate: true
+    }
+  },
+  created: function() {
+    var now = new Date()
+    var y = now.getFullYear()
+    var m = now.getMonth() + 1
+    var mStr = m < 10 ? '0' + m : '' + m
+    this.currentMonth = y + '-' + mStr
+    var lastDay = new Date(y, now.getMonth(), 0).getDate()
+    this.monthEnd = y + '-' + mStr + '-' + (lastDay < 10 ? '0' + lastDay : '' + lastDay)
+    this.initFromProps()
+    this.syncDiet(this.diet)
+    // 默认展开前 3 组 + 已选疾病所在组
+    this.expandedGroups = DISEASE_GROUPS.slice(0, 3).map(function(g) { return g.key })
+    var self = this
+    ;(this.innerForm.selectedDiseases || []).forEach(function(d) {
+      var gk = self.findGroupKey(d.name)
+      if (gk && self.expandedGroups.indexOf(gk) < 0) self.expandedGroups.push(gk)
+    })
+  },
+  methods: {
+    // ===== 初始化 =====
+    initFromProps: function() {
+      var f = this.form || {}
+      this.innerForm = {
+        heightCm: f.heightCm != null ? f.heightCm : '',
+        weightKg: f.weightKg != null ? f.weightKg : '',
+        bloodPressure: f.bloodPressure != null ? f.bloodPressure : '',
+        bloodGlucose: f.bloodGlucose != null ? f.bloodGlucose : '',
+        notes: f.notes != null ? f.notes : '',
+        lipidCholesterol: f.lipidCholesterol != null ? f.lipidCholesterol : '',
+        selectedDiseases: Array.isArray(f.selectedDiseases) ? f.selectedDiseases.slice() : [],
+        medRows: Array.isArray(f.medRows) ? f.medRows.slice() : [],
+        noMedication: !!f.noMedication
+      }
+      var ls = f.lifestyle
+      var obj = {}
+      if (ls && typeof ls === 'object') {
+        LIFESTYLE_KEYS.forEach(function(k) {
+          obj[k] = ls[k] != null ? (ls[k] instanceof Array ? ls[k].slice() : ls[k]) : (k === 'allergySymptoms' ? [] : '')
+        })
+      } else {
+        LIFESTYLE_KEYS.forEach(function(k) {
+          obj[k] = k === 'allergySymptoms' ? [] : ''
+        })
+      }
+      this.lifestyle = obj
+      // 快捷标签回显
+      this.matchQuickLabels()
+    },
+    syncDiet: function(v) {
+      var d = v || {}
+      this.innerDiet = {
+        allergies: Array.isArray(d.allergies) ? d.allergies.slice() : [],
+        absoluteAvoid: Array.isArray(d.absoluteAvoid) ? d.absoluteAvoid.slice() : [],
+        mustEat: Array.isArray(d.mustEat) ? d.mustEat.slice() : [],
+        religiousDiet: d.religiousDiet || 'none',
+        spiceLevel: d.spiceLevel != null ? d.spiceLevel : 0,
+        flavorPref: Array.isArray(d.flavorPref) ? d.flavorPref.slice() : [],
+        cuisinePref: Array.isArray(d.cuisinePref) ? d.cuisinePref.slice() : [],
+        cookingMethods: Array.isArray(d.cookingMethods) ? d.cookingMethods.slice() : [],
+        healthGoals: Array.isArray(d.healthGoals) ? d.healthGoals.slice() : [],
+        noPreference: !!d.noPreference,
+        vegetableFruit: d.vegetableFruit || '',
+        dairy: d.dairy || '',
+        sugaryDrinks: d.sugaryDrinks || ''
+      }
+    },
+    matchQuickLabels: function() {
+      this.heightQuick = this.matchQuick('heightCm', 'height')
+      this.weightQuick = this.matchQuick('weightKg', 'weight')
+      this.bpQuick = this.matchQuick('bloodPressure', 'bloodPressure')
+      this.glucoseQuick = this.matchQuick('bloodGlucose', 'bloodGlucose')
+      this.lipidQuick = this.matchQuick('lipidCholesterol', 'lipid')
+    },
+    matchQuick: function(field, quickKey) {
+      var QUICK = {
+        height: { normal: '170', high: '185', low: '155' },
+        weight: { normal: '60', high: '80', low: '45' },
+        bloodPressure: { normal: '120/80', high: '140/90', low: '90/60' },
+        bloodGlucose: { normal: '5.0', high: '7.0', low: '3.5' },
+        lipid: { normal: '4.5', high: '6.2' }
+      }
+      var map = QUICK[quickKey]
+      var val = String(this.innerForm[field] || '')
+      var keys = Object.keys(map)
+      for (var i = 0; i < keys.length; i++) {
+        if (String(map[keys[i]]) === val) return keys[i]
+      }
+      return ''
+    },
+    // ===== 步骤导航(前进步时同步快照到父页) =====
+    prevStep: function() { if (this.currentStep > 1) this.currentStep-- },
+    nextStep: function() {
+      if (this.currentStep < this.activeModules.length) {
+        this.$emit('update-form', this.formCopy())
+        this.$emit('update-diet', this.dietCopy())
+        this.currentStep++
+      }
+    },
+    // ===== 提交 =====
+    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 formCopy = this.formCopy()
+      var dietCopy = this.dietCopy()
+      var diff = this.buildLifestyleDiff()
+      if (!this.hasAnyContent(formCopy, dietCopy, diff)) {
+        uni.showToast({ title: '请至少填写一项', icon: 'none' }); return
+      }
+      this.$emit('submit', { form: formCopy, diet: dietCopy, lifestyleDiff: diff })
+    },
+    hasAnyContent: function(form, diet, diff) {
+      var keys = ['heightCm', 'weightKg', 'bloodPressure', 'bloodGlucose', 'notes']
+      for (var i = 0; i < keys.length; i++) {
+        if (form[keys[i]] != null && form[keys[i]] !== '') return true
+      }
+      if (form.lipidCholesterol != null && form.lipidCholesterol !== '') return true
+      if (form.selectedDiseases && form.selectedDiseases.length > 0) return true
+      if (form.medRows && form.medRows.length > 0) return true
+      if (form.noMedication) return true
+      var dkeys = ['allergies', 'absoluteAvoid', 'mustEat', 'flavorPref', 'cuisinePref', 'cookingMethods', 'healthGoals', 'vegetableFruit', 'dairy', 'sugaryDrinks']
+      for (var j = 0; j < dkeys.length; j++) {
+        var dv = diet[dkeys[j]]
+        if (dv instanceof Array) { if (dv.length > 0) return true } else if (dv != null && dv !== '' && dv !== 'none') return true
+      }
+      if (diet.religiousDiet && diet.religiousDiet !== 'none') return true
+      if (diet.spiceLevel && Number(diet.spiceLevel) > 0) return true
+      if (diet.noPreference) return true
+      return Object.keys(diff).length > 0
+    },
+    // 结构化 form 副本(不含 lifestyle;JSON 转换由父页负责)
+    formCopy: function() {
+      return {
+        heightCm: this.innerForm.heightCm,
+        weightKg: this.innerForm.weightKg,
+        bloodPressure: this.innerForm.bloodPressure,
+        bloodGlucose: this.innerForm.bloodGlucose,
+        notes: this.innerForm.notes,
+        lipidCholesterol: this.innerForm.lipidCholesterol,
+        selectedDiseases: this.innerForm.selectedDiseases || [],
+        medRows: this.innerForm.medRows || [],
+        noMedication: !!this.innerForm.noMedication
+      }
+    },
+    dietCopy: function() {
+      return {
+        allergies: this.innerDiet.allergies || [],
+        absoluteAvoid: this.innerDiet.absoluteAvoid || [],
+        mustEat: this.innerDiet.mustEat || [],
+        religiousDiet: this.innerDiet.religiousDiet || 'none',
+        spiceLevel: this.innerDiet.spiceLevel || 0,
+        flavorPref: this.innerDiet.flavorPref || [],
+        cuisinePref: this.innerDiet.cuisinePref || [],
+        cookingMethods: this.innerDiet.cookingMethods || [],
+        healthGoals: this.innerDiet.healthGoals || [],
+        noPreference: !!this.innerDiet.noPreference,
+        vegetableFruit: this.innerDiet.vegetableFruit || '',
+        dairy: this.innerDiet.dairy || '',
+        sugaryDrinks: this.innerDiet.sugaryDrinks || ''
+      }
+    },
+    // lifestyle diff:仅本次修改过的 key(未变化的 key 剔除,交由父页 merge 旧值)
+    buildLifestyleDiff: function() {
+      var diff = {}
+      var orig = (this.form && this.form.lifestyle) || {}
+      var self = this
+      LIFESTYLE_KEYS.forEach(function(k) {
+        var val = self.lifestyle[k]
+        if (val == null) return
+        var a = JSON.stringify(val)
+        var b = JSON.stringify(orig[k] != null ? orig[k] : (k === 'allergySymptoms' ? [] : null))
+        if (a === b) return
+        if (val instanceof Array) { if (val.length > 0) diff[k] = val.slice() }
+        else if (val !== '') diff[k] = val
+      })
+      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 工具 =====
+    setDietValue: function(key, val) {
+      this.$set(this.innerDiet, key, val)
+    },
+    toggleDietChip: function(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)
+      this.$set(this.innerDiet, 'noPreference', false)
+    },
+    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)
+      this.$set(this.innerDiet, 'noPreference', false)
+    },
+    // ===== 自定义添加 =====
+    openCustom: function(field) { this.customField = field; this.customInput = '' },
+    addCustom: function() {
+      var val = (this.customInput || '').trim()
+      if (!val) { this.customField = ''; return }
+      var arr = (this.innerDiet[this.customField] || []).slice()
+      if (arr.indexOf(val) < 0) {
+        arr.push(val)
+        this.$set(this.innerDiet, this.customField, arr)
+      }
+      this.$set(this.innerDiet, 'noPreference', false)
+      this.customInput = ''
+      this.customField = ''
+    },
+    // ===== 体征快捷标签 =====
+    setHeightQuick: function(key) {
+      var v = { normal: '170', high: '185', low: '155' }[key]
+      this.$set(this.innerForm, 'heightCm', v); this.heightQuick = key
+    },
+    setWeightQuick: function(key) {
+      var v = { normal: '60', high: '80', low: '45' }[key]
+      this.$set(this.innerForm, 'weightKg', v); this.weightQuick = key
+    },
+    setBpQuick: function(key) {
+      var v = { normal: '120/80', high: '140/90', low: '90/60' }[key]
+      this.$set(this.innerForm, 'bloodPressure', v); this.bpQuick = key
+    },
+    setGlucoseQuick: function(key) {
+      var v = { normal: '5.0', high: '7.0', low: '3.5' }[key]
+      this.$set(this.innerForm, 'bloodGlucose', v); this.glucoseQuick = key
+    },
+    setLipidQuick: function(key) {
+      var v = { normal: '4.5', high: '6.2' }[key]
+      this.$set(this.innerForm, 'lipidCholesterol', v); this.lipidQuick = key
+    },
+    // ===== v-for key 方法(微信小程序 :key 不支持表达式,须用方法调用) =====
+    getDiseaseKey: function(gi, ii) { return 'g' + gi + '-' + ii },
+    getSelKey: function(si) { return 's' + si },
+    getMedKey: function(idx) { return 'm' + idx },
+    // ===== 疾病 =====
+    findGroupKey: function(name) {
+      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 DISEASE_GROUPS[i].key
+        }
+      }
+      return ''
+    },
+    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)
+    },
+    addCustomDisease: function() {
+      var name = (this.diseaseInput || '').trim()
+      if (!name) return
+      var list = (this.innerForm.selectedDiseases || []).slice()
+      if (!list.some(function(d) { return d.name === name })) {
+        list.push({ name: name, diagnosedAt: '', custom: true })
+        this.$set(this.innerForm, 'selectedDiseases', list)
+      }
+      this.diseaseInput = ''
+    },
+    removeSelected: function(idx) {
+      var list = (this.innerForm.selectedDiseases || []).slice()
+      list.splice(idx, 1)
+      this.$set(this.innerForm, 'selectedDiseases', list)
+    },
+    onDiagChange: function(idx, e) {
+      var list = (this.innerForm.selectedDiseases || []).slice()
+      list[idx] = Object.assign({}, list[idx], { diagnosedAt: e.detail.value })
+      this.$set(this.innerForm, 'selectedDiseases', list)
+    },
+    // ===== 用药 =====
+    toggleNoMedication: function() {
+      this.$set(this.innerForm, 'noMedication', !this.innerForm.noMedication)
+    },
+    addMed: function() {
+      var rows = (this.innerForm.medRows || []).slice()
+      rows.push({ name: '', startDate: '', durationText: '', time: '' })
+      this.$set(this.innerForm, 'medRows', rows)
+    },
+    removeMed: function(idx) {
+      var rows = (this.innerForm.medRows || []).slice()
+      rows.splice(idx, 1)
+      this.$set(this.innerForm, 'medRows', rows)
+    },
+    onMedStartChange: function(idx, e) {
+      var start = e.detail.value
+      var rows = (this.innerForm.medRows || []).slice()
+      rows[idx] = Object.assign({}, rows[idx], { startDate: start, durationText: this.calcDuration(start) })
+      this.$set(this.innerForm, 'medRows', rows)
+    },
+    onForgetClick: function(idx) {
+      var self = this
+      uni.showModal({
+        title: '记不清开始日期',
+        editable: true,
+        placeholderText: '如:约 3 个月',
+        success: function(res) {
+          if (res.confirm && res.content) {
+            var rows = (self.innerForm.medRows || []).slice()
+            rows[idx] = Object.assign({}, rows[idx], { durationText: res.content.trim() })
+            self.$set(self.innerForm, 'medRows', rows)
+          }
+        }
+      })
+    },
+    calcDuration: function(startDate) {
+      if (!startDate) return ''
+      var parts = startDate.split('-')
+      if (parts.length !== 2) return ''
+      var sy = parseInt(parts[0], 10)
+      var sm = parseInt(parts[1], 10)
+      var now = new Date()
+      var months = (now.getFullYear() - sy) * 12 + (now.getMonth() + 1 - sm)
+      if (months < 1) months = 1
+      if (months < 12) return '已服 ' + months + ' 个月'
+      return '已服 ' + Math.floor(months / 12) + ' 年'
+    }
+  }
+}
+</script>
+
+<style scoped>
+.survey-wrap { min-height: 100vh; background: linear-gradient(180deg, #FFF7ED, #FFF3E6); }
+.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 { padding: 24rpx 32rpx 220rpx; }
+.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: 22rpx; color: #B08968; margin-top: 8rpx; line-height: 1.5; }
+.card { background: #FFFFFF; border-radius: 24rpx; padding: 28rpx; margin-bottom: 24rpx; box-shadow: 0 4rpx 16rpx rgba(255, 140, 66, 0.08); }
+.card-head { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; }
+.card-title { display: block; font-size: 28rpx; font-weight: 600; color: #4A3728; margin-bottom: 16rpx; }
+.card-head .card-title { margin-bottom: 0; }
+.card-hint { font-size: 22rpx; color: #B08968; margin-bottom: 16rpx; }
+.card-del { font-size: 24rpx; color: #E53E3E; padding: 8rpx 16rpx; }
+.card-value { font-size: 26rpx; color: #4A3728; }
+.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; }
+.picker-val { font-size: 26rpx; color: #FF8C42; }
+.num-input { display: block; width: 100%; background: #FFF3E6; border-radius: 16rpx; padding: 16rpx 20rpx; font-size: 28rpx; color: #4A3728; margin-top: 16rpx; }
+.unit-hint { display: block; font-size: 20rpx; color: #C4A98E; margin-top: 8rpx; }
+.notes-area { width: 100%; min-height: 120rpx; background: #FFF3E6; border-radius: 16rpx; padding: 16rpx 20rpx; font-size: 26rpx; color: #4A3728; margin-top: 16rpx; }
+.quick-chips { display: flex; gap: 12rpx; }
+.quick-chip { padding: 8rpx 20rpx; background: #FFF3E6; color: #6B5744; border-radius: 999rpx; font-size: 20rpx; border: 2rpx solid #FFE6D2; }
+.quick-chip-on { background: #FF8C42; color: #FFFFFF; border-color: #FF8C42; }
+.chip-row-wrap { display: flex; flex-wrap: wrap; gap: 16rpx; }
+.chip-item-wrap { display: flex; align-items: center; gap: 4rpx; padding: 12rpx 28rpx; background: #FFF3E6; color: #6B5744; border-radius: 999rpx; font-size: 24rpx; border: 2rpx solid transparent; }
+.chip-item-wrap.chip-active { background: #FF8C42; color: #FFFFFF; border-color: #FF8C42; font-weight: 600; }
+.risk-badge { color: #E53E3E; font-size: 20rpx; }
+.chip-active .risk-badge { color: #FFE6D2; }
+.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-add { margin-top: 16rpx; font-size: 24rpx; color: #FF8C42; }
+.struct-row { margin-bottom: 24rpx; }
+.struct-row:last-child { margin-bottom: 0; }
+.struct-label { display: block; font-size: 24rpx; color: #6B5744; margin-bottom: 12rpx; }
+.slider-row { display: flex; align-items: center; gap: 24rpx; }
+.sel-row { display: flex; align-items: center; justify-content: space-between; padding: 20rpx 0; border-bottom: 1rpx solid #F7EFE6; }
+.sel-row:last-child { border-bottom: none; }
+.sel-name { flex: 1; font-size: 26rpx; color: #4A3728; }
+.sel-diag { font-size: 24rpx; color: #FF8C42; padding: 8rpx 20rpx; background: #FFF3E6; border-radius: 999rpx; margin: 0 16rpx; }
+.sel-del { font-size: 32rpx; color: #B08968; padding: 0 12rpx; }
+.link-forget { font-size: 24rpx; color: #FF8C42; padding: 8rpx 16rpx; }
+.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; }
+.add-row { display: flex; align-items: center; justify-content: center; padding: 24rpx; border: 2rpx dashed #FFD4B8; border-radius: 24rpx; color: #FF8C42; font-size: 26rpx; background: #FFFFFF; }
+.add-icon { margin-right: 8rpx; }
+.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; line-height: 1.6; }
+.btn-next, .btn-done { background: #FF8C42; color: #FFFFFF; }
+.btn-done[disabled] { opacity: 0.6; }
+</style>