Explorar o código

docs(plans): 健康现状档案 v2 实现计划 — 疾病史两源预置清单+确诊时间+服药时长

- 改动收敛为单文件 cfc-frontend/pages/health/health-status-form.vue
- 疾病史:DISEASE_PRESETS(10项) → DISEASE_GROUPS(8组54项★菌群源),分类折叠多选,★角标说明,已选区设确诊时间(年月选择器)
- 用药:startDate年月 + durationText自动计算,记不清手动兜底
- 后端零变更(JSON列透传),v1旧数据回显兼容
- 7个任务:常量/数据模型/模板/逻辑/用药/保存/验证提交,含完整代码与验证命令
iwt hai 1 mes
pai
achega
92354f6a2e

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

@@ -84,7 +84,7 @@
 | 新用户注册引导 + 个人信息按需补充/回写 | Phase 1 | ✅ 已实施(注册解耦最小集 + 个人中心卡片 + 功能场景就地补充自动回写 + 无家庭引导组件) | `specs/2026-08-04-registration-profile-backfill-design.md` + `plans/2026-08-04-registration-profile-backfill.md` |
 | 通关解锁(测试门禁+引导解锁) | Phase 1 | 🟡 设计稿 v2.1(三关递进:自检→个人沙盘 / 邀请家人→全家沙盘 / 创建家人→孩子微行动;沙盘加个人/全家切换开关;增值关卡含购物/会员/上传报告,各关均有奖励;后台可配置) | `specs/2026-08-10-unlock-gates-design.md` |
 | 首页重构(主动健康漏斗) | Phase 5 | ✅ 已实施(四段式漏斗:了解区自测卡/发现区沙盘+上传报告/行动区旅程卡+今日任务+我的任务+家庭挑战+会员/内容区快捷栏+动态+阅读+商品;未登录态零改动) | `specs/2026-08-18-homepage-health-funnel-design.md` + `plans/2026-08-18-homepage-health-funnel.md` |
-| 当前状态调研(健康现状档案) | Phase 5 | 🔄 v2 设计待评审(v1 已实施:health_status 表+档案页+出方案弹窗;v2 重设计:疾病史改两源预置清单约50项分类折叠多选+确诊时间,用药加开始日期自动计算时长) | `specs/2026-08-13-health-status-survey-design.md` |
+| 当前状态调研(健康现状档案 v2) | Phase 5 | 🔄 v2 计划待执行(v1 已实施:health_status 表+档案页+出方案弹窗;v2 重设计:疾病史改两源预置清单 8 组 54 项分类折叠多选+确诊时间年月,用药加开始年月自动计算时长+记不清兜底;后端零变更) | `specs/2026-08-13-health-status-survey-design.md` + `plans/2026-08-18-health-status-survey-v2.md` |
 
 ---
 
@@ -355,7 +355,8 @@
 | `2026-08-10-unlock-gates-design.md` | 🟡 设计稿 v2.1 | 通关解锁功能(三关递进:自检→个人沙盘、邀请家人→全家沙盘、创建家人→孩子微行动;沙盘个人/全家切换;增值关卡含购物/会员/上传报告;每关有奖;管理端关卡配置,数据驱动) |
 | `2026-08-15-sku-price-cents-and-spec-selector-design.md` | ✅ 已实施(7 Tasks:实体/DTO/订单/购物车/迁移/管理端/小程序,5 commits) | SKU 规格价格统一分单位 + 详情页规格按钮修复设计 |
 | `2026-08-18-homepage-health-funnel-design.md` | ✅ 已实施(四段式漏斗:了解区自测卡/发现区沙盘+上传报告/行动区聚合/内容区降级;未登录态零改动) | 首页重构 — 主动健康漏斗(了解→发现→改变) |
-| `2026-08-13-health-status-survey-design.md` | 🔄 v2 设计待评审(v1 已实施;v2 疾病史两源预置清单+确诊时间+服药时长) | 当前状态调研 — 健康现状档案 |
+| `2026-08-13-health-status-survey-design.md` | 🔄 v2 设计已定稿(2026-08-18;v1 已实施;v2 疾病史两源预置清单+确诊时间+服药时长) | 当前状态调研 — 健康现状档案 |
+| `2026-08-18-health-status-survey-v2.md` | 🔄 计划待执行(单文件改 health-status-form.vue;8 组 54 项预置清单分类多选+确诊时间;用药年月+时长自动计算;后端零变更) | 当前状态调研 — 健康现状档案 v2 |
 
 ### 实施计划(plans/)
 

+ 800 - 0
docs/superpowers/plans/2026-08-18-health-status-survey-v2.md

@@ -0,0 +1,800 @@
+# 「当前状态调研」健康现状档案 v2 实现计划(疾病史/用药输入重设计)
+
+> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
+
+**目标:** 将 `health-status-form.vue` 的疾病史区重写为「菌群报告病症风险(20 项 ★)+ 互联网统计常见病」两源合并去重约 54 项的分类折叠多选清单,每条疾病支持设置确诊时间(年月);用药区每行新增「开始服药年月选择器 → 自动计算已服时长」,记不清时可用时长文本兜底。后端零变更,无 DDL。
+
+**架构:** 仅修改前端单文件 `cfc-frontend/pages/health/health-status-form.vue`。疾病清单改为 `DISEASE_GROUPS` 分组常量(`{key, title, items:[{name, risk}]}`),数据模型 `diseaseNames`(字符串数组)重构为 `selectedDiseases`(`[{name, diagnosedAt, custom}]` 对象数组);`medRows` 由 `{name, time}` 扩展为 `{name, startDate, durationText, time}`。保存 JSON 结构与 v1 兼容(旧字段保留、新字段缺省),后端 `HealthStatus` 实体 String 透传不变。
+
+**技术栈:** uni-app Vue 2 小程序(Options API)。限制:禁止可选链 `?.`、禁止 CSS Grid、禁止 `:key` 表达式(用方法调用)、禁止 `:class` 中调用自定义方法(用内联表达式 `selectedNames.indexOf(...) >= 0`)、禁止 `new Date(string)` 解析。
+
+**规格:** `docs/superpowers/specs/2026-08-13-health-status-survey-design.md`(v2,2026-08-18 更新)
+
+---
+
+## 文件结构
+
+**前端(cfc-frontend/):**
+- 修改 `pages/health/health-status-form.vue` — 唯一代码改动文件:
+  - 常量区:`DISEASE_PRESETS`(10 项字符串)→ `DISEASE_GROUPS`(8 组 54 项,`{name, risk}`)
+  - 模板:疾病史区重写(分组折叠 + 多选 + ★ 角标 + 已选区 + 确诊时间选择器);用药区增强(开始年月 + 时长展示 + 记不清)
+  - 数据:`diseaseNames`/`customDiseases`/`diseaseTags` → `selectedDiseases` + `expandedGroups` + `currentMonth`/`monthEnd`;`medRows` 扩展字段
+  - 方法:新增 `toggleGroup`/`toggleDisease`/`showRiskTip`/`addCustomDisease`/`removeSelected`/`onDiagChange`/`onMedStartChange`/`onForgetClick`/`calcDuration`/`findGroupKey`;改造 `loadDiseases`/`parseMeds`/`buildPayload`
+
+**文档:**
+- 创建 `docs/superpowers/plans/2026-08-18-health-status-survey-v2.md`(本文件)
+- 修改 `docs/superpowers/PROJECT-OVERVIEW.md` — 同步计划条目与状态
+
+**验证方式(本模块无单测框架):**
+- 语法校验:`sed -n '/<script>/,/<\/script>/p' cfc-frontend/pages/health/health-status-form.vue | sed '1d;$d' > /tmp/hs-check.mjs && node --check /tmp/hs-check.mjs`
+- 结构校验:模板标签闭合、`:key`/`:class` 符合小程序限制
+- 手工验证清单(见任务 6)
+
+---
+
+### 任务 1:疾病清单常量重构(DISEASE_PRESETS → DISEASE_GROUPS)
+
+**文件:**
+- 修改:`cfc-frontend/pages/health/health-status-form.vue`(script 顶部常量区,替换第 82 行 `var DISEASE_PRESETS = [...]`)
+
+- [ ] **步骤 1:替换常量区**
+
+将:
+
+```js
+var DISEASE_PRESETS = ['高血压', '糖尿病', '高血脂', '过敏性鼻炎', '哮喘', '贫血', '胃炎', '甲状腺疾病', '心脏疾病', '肾脏疾病']
+```
+
+替换为(8 组 54 项,★ = 菌群报告病症风险源,与设计 6.1.1 表一致):
+
+```js
+// v2: 预置疾病清单(菌群报告病症风险 ★ + 互联网统计常见病 两源合并去重,8 组 54 项)
+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 }
+  ]}
+]
+```
+
+- [ ] **步骤 2:语法校验**
+
+运行:
+```bash
+sed -n '/<script>/,/<\/script>/p' cfc-frontend/pages/health/health-status-form.vue | sed '1d;$d' > /tmp/hs-check.mjs && node --check /tmp/hs-check.mjs
+```
+预期:无输出(通过)。此步仅替换常量,其余代码仍引用 `DISEASE_PRESETS` 会暂时不工作——**步骤 2 与后续任务须一并完成后再验证整体**,此处先确认常量区语法无误即可。
+
+---
+
+### 任务 2:疾病史区数据模型重构(selectedDiseases + expandedGroups + 年月初始化)
+
+**文件:**
+- 修改:`cfc-frontend/pages/health/health-status-form.vue`(script 的 `data()` 与 `onLoad`)
+
+- [ ] **步骤 1:替换 `data()`**
+
+将(原 84-105 行):
+
+```js
+  data() {
+    return {
+      form: {
+        heightCm: '',
+        weightKg: '',
+        bloodPressure: '',
+        bloodGlucose: '',
+        bloodLipids: '',
+        diseaseHistory: '',
+        medications: '',
+        notes: ''
+      },
+      lipidCholesterol: '',
+      diseaseTags: DISEASE_PRESETS.slice(),
+      diseaseNames: [],
+      customDiseases: [],
+      diseaseInput: '',
+      medRows: [],
+      saving: false
+    }
+  },
+```
+
+替换为:
+
+```js
+  data() {
+    return {
+      form: {
+        heightCm: '',
+        weightKg: '',
+        bloodPressure: '',
+        bloodGlucose: '',
+        bloodLipids: '',
+        diseaseHistory: '',
+        medications: '',
+        notes: ''
+      },
+      lipidCholesterol: '',
+      diseaseGroups: DISEASE_GROUPS,
+      expandedGroups: DISEASE_GROUPS.slice(0, 3).map(function(g) { return g.key }),
+      selectedDiseases: [],
+      diseaseInput: '',
+      medRows: [],
+      currentMonth: '',
+      monthEnd: '',
+      saving: false
+    }
+  },
+```
+
+- [ ] **步骤 2:替换 `onLoad` 初始化年月**
+
+将(原 106-108 行):
+
+```js
+  onLoad: function() {
+    this.load()
+  },
+```
+
+替换为:
+
+```js
+  onLoad: 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, m, 0).getDate()
+    this.monthEnd = y + '-' + mStr + '-' + (lastDay < 10 ? '0' + lastDay : '' + lastDay)
+    this.load()
+  },
+```
+
+> 说明:`currentMonth` = `yyyy-MM`(年月选择器 value/end 基准),`monthEnd` = 当前月最后一天 `yyyy-MM-DD`(picker `end` 上限,防止选择未来年月)。`new Date(y, m, 0)` 为数字构造,非字符串解析,iOS 安全。
+
+- [ ] **步骤 3:语法校验**
+
+运行:
+```bash
+sed -n '/<script>/,/<\/script>/p' cfc-frontend/pages/health/health-status-form.vue | sed '1d;$d' > /tmp/hs-check.mjs && node --check /tmp/hs-check.mjs
+```
+预期:无输出(通过)。
+
+---
+
+### 任务 3:疾病史区模板重写(分类折叠 + 多选 + ★ 角标 + 已选区 + 确诊时间)
+
+**文件:**
+- 修改:`cfc-frontend/pages/health/health-status-form.vue`(template 疾病史区,原 35-56 行)
+
+- [ ] **步骤 1:替换疾病史区模板**
+
+将:
+
+```html
+    <view class="form-section">
+      <view class="form-title">疾病史</view>
+      <view class="tag-wrap">
+        <view
+          v-for="(tag, idx) in diseaseTags"
+          :key="idx"
+          class="tag-item"
+          :class="{ 'tag-active': form.diseaseNames.indexOf(tag) >= 0 }"
+          @click="toggleDisease(tag)"
+        >
+          <text>{{ tag }}</text>
+        </view>
+        <view v-for="(custom, idx) in customDiseases" :key="getCustomKey(idx)" class="tag-item tag-active">
+          <text>{{ custom }}</text>
+          <text class="tag-del" @click="removeCustomDisease(idx)">×</text>
+        </view>
+      </view>
+      <view class="custom-row">
+        <input class="custom-input" type="text" v-model="diseaseInput" placeholder="输入其他疾病" />
+        <text class="add-link" @click="addCustomDisease">添加</text>
+      </view>
+    </view>
+```
+
+替换为:
+
+```html
+    <view class="form-section">
+      <view class="form-title">疾病史</view>
+      <view v-for="(group, gi) in diseaseGroups" :key="group.key" class="disease-group">
+        <view class="group-head" @click="toggleGroup(group.key)">
+          <text class="group-title">{{ group.title }}</text>
+          <text class="group-arrow">{{ expandedGroups.indexOf(group.key) >= 0 ? '收起' : '展开' }}</text>
+        </view>
+        <view v-if="expandedGroups.indexOf(group.key) >= 0" class="tag-wrap">
+          <view
+            v-for="(item, ii) in group.items"
+            :key="item.name"
+            class="tag-item"
+            :class="{ 'tag-active': selectedNames.indexOf(item.name) >= 0 }"
+            @click="toggleDisease(item.name)"
+          >
+            <text>{{ item.name }}</text>
+            <text v-if="item.risk" class="risk-badge" @click.stop="showRiskTip(item.name)">★</text>
+          </view>
+        </view>
+      </view>
+      <view class="custom-row">
+        <input class="custom-input" type="text" v-model="diseaseInput" placeholder="输入其他疾病" />
+        <text class="add-link" @click="addCustomDisease">添加</text>
+      </view>
+      <view v-if="selectedDiseases.length > 0" class="selected-area">
+        <view class="selected-title">已选疾病({{ selectedDiseases.length }})</view>
+        <view v-for="(item, si) in 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" @click="removeSelected(si)">×</text>
+        </view>
+      </view>
+    </view>
+```
+
+> 说明:
+> - `:class` 用内联 `selectedNames.indexOf(item.name) >= 0`(`selectedNames` 为 computed),符合「禁止 :class 调用自定义方法」限制
+> - `:key` 用 `group.key`/`item.name`(均为普通变量,非表达式);已选区用方法 `getSelKey(si)`
+> - `fields="month"` 让 picker 只选年月,`end` 限制未来;`@click.stop` 防止点 ★ 触发选中
+
+- [ ] **步骤 2:模板结构校验**
+
+运行:
+```bash
+cd /sc-data/cfc && grep -c '<view' cfc-frontend/pages/health/health-status-form.vue && grep -c '</view>' cfc-frontend/pages/health/health-status-form.vue
+```
+预期:两数字相等(标签闭合)。同时确认无 `?.`、无 `style="display:grid"`。
+
+---
+
+### 任务 4:疾病史区逻辑方法(toggleGroup/toggleDisease/showRiskTip/已选区操作)
+
+**文件:**
+- 修改:`cfc-frontend/pages/health/health-status-form.vue`(script `methods`,替换 `getCustomKey`/`loadDiseases` 之外的方法区)
+
+- [ ] **步骤 1:替换 `getCustomKey` 方法**
+
+将:
+
+```js
+    getCustomKey: function(i) { return 'c' + i },
+```
+
+替换为:
+
+```js
+    getSelKey: function(i) { return 's' + i },
+```
+
+- [ ] **步骤 2:替换 `loadDiseases`(v2 结构 + v1 兼容)**
+
+将:
+
+```js
+    loadDiseases: function(json) {
+      var self = this
+      try {
+        var arr = JSON.parse(json || '[]')
+        if (arr instanceof Array) {
+          arr.forEach(function(item) {
+            if (item.name && self.diseaseNames.indexOf(item.name) < 0) {
+              self.diseaseNames.push(item.name)
+            }
+          })
+        }
+      } catch (e) {}
+    },
+```
+
+替换为:
+
+```js
+    loadDiseases: function(json) {
+      var self = this
+      try {
+        var arr = JSON.parse(json || '[]')
+        if (arr instanceof Array) {
+          arr.forEach(function(item) {
+            if (item.name && self.selectedNames.indexOf(item.name) < 0) {
+              self.selectedDiseases.push({
+                name: item.name,
+                diagnosedAt: item.diagnosedAt || '',
+                custom: false
+              })
+              var gk = self.findGroupKey(item.name)
+              if (gk && self.expandedGroups.indexOf(gk) < 0) {
+                self.expandedGroups.push(gk)
+              }
+            }
+          })
+        }
+      } catch (e) {}
+    },
+```
+
+> 兼容:v1 旧数据 `{name, note}` 无 `diagnosedAt` → 默认 `''` 不报错;加载时自动展开已选疾病所在组。
+
+- [ ] **步骤 3:替换疾病相关方法组**
+
+将:
+
+```js
+    toggleDisease: function(tag) {
+      var idx = this.diseaseNames.indexOf(tag)
+      if (idx >= 0) this.diseaseNames.splice(idx, 1)
+      else this.diseaseNames.push(tag)
+    },
+    addCustomDisease: function() {
+      var name = (this.diseaseInput || '').trim()
+      if (!name) return
+      if (this.diseaseNames.indexOf(name) < 0) {
+        this.diseaseNames.push(name)
+        this.customDiseases.push(name)
+      }
+      this.diseaseInput = ''
+    },
+    removeCustomDisease: function(idx) {
+      var name = this.customDiseases[idx]
+      this.customDiseases.splice(idx, 1)
+      var i = this.diseaseNames.indexOf(name)
+      if (i >= 0) this.diseaseNames.splice(i, 1)
+    },
+```
+
+替换为:
+
+```js
+    toggleGroup: function(key) {
+      var idx = this.expandedGroups.indexOf(key)
+      if (idx >= 0) this.expandedGroups.splice(idx, 1)
+      else this.expandedGroups.push(key)
+    },
+    findGroupKey: function(name) {
+      for (var i = 0; i < this.diseaseGroups.length; i++) {
+        var items = this.diseaseGroups[i].items
+        for (var j = 0; j < items.length; j++) {
+          if (items[j].name === name) return this.diseaseGroups[i].key
+        }
+      }
+      return ''
+    },
+    toggleDisease: function(name) {
+      var idx = -1
+      for (var i = 0; i < this.selectedDiseases.length; i++) {
+        if (this.selectedDiseases[i].name === name) { idx = i; break }
+      }
+      if (idx >= 0) {
+        this.selectedDiseases.splice(idx, 1)
+      } else {
+        this.selectedDiseases.push({ name: name, diagnosedAt: '', custom: false })
+        var gk = this.findGroupKey(name)
+        if (gk && this.expandedGroups.indexOf(gk) < 0) {
+          this.expandedGroups.push(gk)
+        }
+      }
+    },
+    showRiskTip: function(name) {
+      uni.showModal({
+        title: '菌群报告关联',
+        content: '「' + name + '」与肠道菌群报告相关,勾选后出方案时可结合菌群报告风险值评估。',
+        showCancel: false
+      })
+    },
+    addCustomDisease: function() {
+      var name = (this.diseaseInput || '').trim()
+      if (!name) return
+      if (this.selectedNames.indexOf(name) < 0) {
+        this.selectedDiseases.push({ name: name, diagnosedAt: '', custom: true })
+      }
+      this.diseaseInput = ''
+    },
+    removeSelected: function(idx) {
+      this.selectedDiseases.splice(idx, 1)
+    },
+    onDiagChange: function(idx, e) {
+      this.selectedDiseases[idx].diagnosedAt = e.detail.value
+    },
+```
+
+> `e.detail.value` 在 `fields="month"` 下返回 `yyyy-MM`,直接存入 `diagnosedAt`。
+
+- [ ] **步骤 4:语法校验**
+
+运行:
+```bash
+sed -n '/<script>/,/<\/script>/p' cfc-frontend/pages/health/health-status-form.vue | sed '1d;$d' > /tmp/hs-check.mjs && node --check /tmp/hs-check.mjs
+```
+预期:无输出(通过)。
+
+---
+
+### 任务 5:用药区增强(startDate 年月选择器 + durationText 自动计算 + 记不清兜底)
+
+**文件:**
+- 修改:`cfc-frontend/pages/health/health-status-form.vue`(template 用药区 + script 用药方法)
+
+- [ ] **步骤 1:替换用药区模板**
+
+将:
+
+```html
+    <view class="form-section">
+      <view class="form-title">在服药物/治疗</view>
+      <view v-for="(med, idx) in medRows" :key="idx" class="med-row">
+        <input class="med-name" type="text" v-model="med.name" placeholder="药物品牌,如 立普妥" />
+        <input class="med-time" type="text" v-model="med.time" placeholder="服药时间,如 早饭后一次" />
+        <text class="med-del" @click="removeMed(idx)">删除</text>
+      </view>
+      <text class="add-link" @click="addMed">+ 添加药物/治疗</text>
+    </view>
+```
+
+替换为:
+
+```html
+    <view class="form-section">
+      <view class="form-title">在服药物/治疗</view>
+      <view v-for="(med, idx) in medRows" :key="idx" class="med-row">
+        <view class="med-line">
+          <input class="med-name" type="text" v-model="med.name" placeholder="药物品牌,如 立普妥" />
+          <text class="med-del" @click="removeMed(idx)">删除</text>
+        </view>
+        <view class="med-line">
+          <picker mode="date" fields="month" :value="med.startDate || currentMonth" :end="monthEnd" @change="onMedStartChange(idx, $event)">
+            <view class="med-picker">{{ med.startDate ? med.startDate : '选择开始日期' }}</view>
+          </picker>
+          <text class="med-forget" @click="onForgetClick(idx)">记不清</text>
+        </view>
+        <view class="med-line" v-if="med.durationText">
+          <text class="med-duration">已服时长:{{ med.durationText }}</text>
+        </view>
+        <view class="med-line">
+          <input class="med-time" type="text" v-model="med.time" placeholder="服药时间,如 早饭后一次" />
+        </view>
+      </view>
+      <text class="add-link" @click="addMed">+ 添加药物/治疗</text>
+    </view>
+```
+
+- [ ] **步骤 2:替换 `parseMeds`(v2 字段 + v1 兼容)**
+
+将:
+
+```js
+    parseMeds: function(json) {
+      var list = []
+      try {
+        var arr = JSON.parse(json || '[]')
+        if (arr instanceof Array) {
+          arr.forEach(function(item) {
+            list.push({ name: item.name || '', time: item.time || '' })
+          })
+        }
+      } catch (e) {}
+      if (list.length === 0) list.push({ name: '', time: '' })
+      return list
+    },
+```
+
+替换为:
+
+```js
+    parseMeds: function(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) {}
+      if (list.length === 0) list.push({ name: '', startDate: '', durationText: '', time: '' })
+      return list
+    },
+```
+
+> 兼容:v1 旧数据 `{name, time}` → `startDate`/`durationText` 默认 `''` 不报错。
+
+- [ ] **步骤 3:替换 `addMed` + 新增用药方法**
+
+将:
+
+```js
+    addMed: function() {
+      this.medRows.push({ name: '', time: '' })
+    },
+```
+
+替换为:
+
+```js
+    addMed: function() {
+      this.medRows.push({ name: '', startDate: '', durationText: '', time: '' })
+    },
+    onMedStartChange: function(idx, e) {
+      var start = e.detail.value
+      this.medRows[idx].startDate = start
+      this.medRows[idx].durationText = this.calcDuration(start)
+    },
+    onForgetClick: function(idx) {
+      var self = this
+      uni.showModal({
+        title: '记不清开始日期',
+        editable: true,
+        placeholderText: '如:约 3 个月',
+        success: function(res) {
+          if (res.confirm && res.content) {
+            self.medRows[idx].durationText = res.content.trim()
+          }
+        }
+      })
+    },
+    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) + ' 年'
+    },
+```
+
+> 时长规则(设计 6.1.2):`当前年月 - startDate`,<12 个月 →「已服 X 个月」,≥12 →「已服 X 年」(取整)。`uni.showModal({editable:true})` 为微信 2.17.1+ 能力,用于「记不清」时输入时长文本兜底。
+
+- [ ] **步骤 4:语法校验**
+
+运行:
+```bash
+sed -n '/<script>/,/<\/script>/p' cfc-frontend/pages/health/health-status-form.vue | sed '1d;$d' > /tmp/hs-check.mjs && node --check /tmp/hs-check.mjs
+```
+预期:无输出(通过)。
+
+---
+
+### 任务 6:保存逻辑改造(buildPayload v2 JSON)+ 样式补充
+
+**文件:**
+- 修改:`cfc-frontend/pages/health/health-status-form.vue`(script `buildPayload` + style)
+
+- [ ] **步骤 1:替换 `buildPayload` 的疾病与用药 JSON 构造**
+
+将:
+
+```js
+      var diseases = []
+      this.diseaseNames.forEach(function(name) {
+        diseases.push({ name: name, note: '' })
+      })
+      var meds = []
+      this.medRows.forEach(function(item) {
+        if (item.name) {
+          meds.push({ name: item.name, time: item.time || '' })
+        }
+      })
+```
+
+替换为:
+
+```js
+      var diseases = []
+      this.selectedDiseases.forEach(function(item) {
+        var d = { name: item.name }
+        if (item.diagnosedAt) d.diagnosedAt = item.diagnosedAt
+        diseases.push(d)
+      })
+      var meds = []
+      this.medRows.forEach(function(item) {
+        if (item.name) {
+          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
+          meds.push(m)
+        }
+      })
+```
+
+> v2 疾病 JSON 为 `[{name, diagnosedAt}]`(`diagnosedAt` 有值才写,兼容 v1 无该字段的读取路径);用药 JSON 为 `[{name, startDate, durationText, time}]`(各字段有值才写)。
+
+- [ ] **步骤 2:在 `data()` 后新增 `computed.selectedNames`**
+
+在 `onLoad: function() {...}` 之前插入:
+
+```js
+  computed: {
+    selectedNames: function() {
+      return this.selectedDiseases.map(function(d) { return d.name })
+    }
+  },
+```
+
+- [ ] **步骤 3:样式补充**
+
+在 `<style scoped>` 内、`.form-title` 规则之后追加:
+
+```css
+.disease-group { margin-bottom: 8rpx; }
+.group-head { display: flex; align-items: center; justify-content: space-between; padding: 12rpx 0; }
+.group-title { font-size: 28rpx; font-weight: bold; color: #333; }
+.group-arrow { font-size: 24rpx; color: #999; }
+.risk-badge { margin-left: 6rpx; color: #E64340; font-size: 24rpx; }
+.selected-area { margin-top: 16rpx; padding: 16rpx; background: #FFF7E6; border-radius: 12rpx; }
+.selected-title { font-size: 26rpx; color: #8A6D3B; margin-bottom: 8rpx; }
+.sel-row { display: flex; align-items: center; justify-content: space-between; padding: 10rpx 0; border-bottom: 1rpx solid #F0E6D0; }
+.sel-name { font-size: 28rpx; color: #333; flex: 1; }
+.sel-diag { font-size: 26rpx; color: #F97316; }
+.sel-del { font-size: 30rpx; color: #E64340; margin-left: 16rpx; }
+.med-row { border: 1rpx solid #F0F0F0; border-radius: 12rpx; padding: 16rpx; margin-bottom: 16rpx; }
+.med-line { display: flex; align-items: center; margin-bottom: 12rpx; }
+.med-line:last-child { margin-bottom: 0; }
+.med-picker { font-size: 26rpx; color: #F97316; padding: 8rpx 16rpx; background: #F5F7FA; border-radius: 10rpx; }
+.med-forget { font-size: 26rpx; color: #999; margin-left: 16rpx; }
+.med-duration { font-size: 26rpx; color: #8A6D3B; }
+```
+
+> 原 `.med-row { display: flex; ... }` 单行布局已不再适用(多行卡片),保留原规则并叠加卡片样式;如与 `med-line` 冲突,删除原 `.med-row` 中的 `display:flex` 规则。
+
+- [ ] **步骤 4:语法校验**
+
+运行:
+```bash
+sed -n '/<script>/,/<\/script>/p' cfc-frontend/pages/health/health-status-form.vue | sed '1d;$d' > /tmp/hs-check.mjs && node --check /tmp/hs-check.mjs
+```
+预期:无输出(通过)。
+
+---
+
+### 任务 7:整体验证 + 文档同步 + 提交
+
+**文件:**
+- 修改:`docs/superpowers/PROJECT-OVERVIEW.md`
+
+- [ ] **步骤 1:完整语法与结构校验**
+
+```bash
+# 1) script 语法
+sed -n '/<script>/,/<\/script>/p' cfc-frontend/pages/health/health-status-form.vue | sed '1d;$d' > /tmp/hs-check.mjs && node --check /tmp/hs-check.mjs
+# 2) 模板标签闭合
+grep -c '<view' cfc-frontend/pages/health/health-status-form.vue; grep -c '</view>' cfc-frontend/pages/health/health-status-form.vue
+# 3) 禁用的残留检查:可选链 / 中文类名 / grid
+grep -n '?\.' cfc-frontend/pages/health/health-status-form.vue; grep -n 'display: *grid' cfc-frontend/pages/health/health-status-form.vue
+# 4) 确认无残留旧引用
+grep -n 'DISEASE_PRESETS\|diseaseNames\|customDiseases\|diseaseTags\|getCustomKey\|removeCustomDisease' cfc-frontend/pages/health/health-status-form.vue
+```
+预期:步骤 3/4 无输出;步骤 2 两数字相等。
+
+- [ ] **步骤 2:手工验证清单(HBuilderX 打包后微信开发者工具)**
+
+按设计文档第 8 节:
+
+| # | 验证项 | 预期 |
+|---|--------|------|
+| 1 | 档案页首次填写 → 保存 → 重新进入回显 | 疾病/确诊时间/服药时长正确回显 |
+| 2 | 疾病预置分类折叠展开、多选、★ 角标点击说明 | 默认展开前 3 组;★ 弹窗说明 |
+| 3 | 选中的疾病设确诊时间(年月选择器)→ 保存回显 | `diagnosedAt` 以 `yyyy-MM` 展示 |
+| 4 | 自定义补充疾病 → 设确诊时间 → 保存回显 | 自定义病在已选区可删可设时间 |
+| 5 | 用药行:选择开始日期 → 时长自动计算展示 | 「已服 X 个月/年」随当前年月计算 |
+| 6 | 「记不清」→ 弹窗输入时长文本 → 保存回显 | `durationText` 兜底生效 |
+| 7 | v1 旧数据(`{name,note}` / `{name,time}`)回显兼容 | 不报错,`diagnosedAt`/`startDate`/`durationText` 置空 |
+| 8 | 年月选择器不可选未来月份 | `end` 限制到当前月 |
+| 9 | 校验非法值(身高 999、血压 abc)→ 拦截 | 前端校验提示 |
+| 10 | 空提交 | 提示「请至少填写一项」 |
+
+- [ ] **步骤 3:同步 PROJECT-OVERVIEW.md**
+
+在「首页重构(主动健康漏斗)」行之后新增一行:
+
+```markdown
+| 当前状态调研(健康现状档案 v2) | Phase 5 | 🟢 已实施(v2 疾病史:8 组 54 项两源预置清单分类折叠多选 + 确诊时间年月;用药:开始年月自动计算时长 + 记不清兜底;后端零变更) | `specs/2026-08-13-health-status-survey-design.md` + `plans/2026-08-18-health-status-survey-v2.md` |
+```
+
+并将上一行「当前状态调研(健康现状档案)」标记为已并入 v2(或直接以 v2 行替代)。
+
+- [ ] **步骤 4:Commit**
+
+```bash
+git add cfc-frontend/pages/health/health-status-form.vue docs/superpowers/plans/2026-08-18-health-status-survey-v2.md docs/superpowers/PROJECT-OVERVIEW.md
+git commit -m "feat(health): 健康现状档案 v2 — 疾病史两源预置清单分类多选+确诊时间,用药开始年月自动计算时长
+- 疾病史:DISEASE_PRESETS(10项) → DISEASE_GROUPS(8组54项,★菌群源),分类折叠多选,★角标说明
+- 确诊时间:年月选择器(fields=month, end限当前月),存 diagnosedAt(yyyy-MM)
+- 用药:startDate年月 + durationText自动计算(<12月→X个月, ≥12→X年),记不清可手动兜底
+- 后端零变更(JSON列透传),v1旧数据({name,note}/{name,time})回显兼容"
+```
+
+---
+
+## 自检
+
+**1. 规格覆盖度:**
+- 6.1.1 疾病史区:预置清单(任务 1 常量)、分类折叠(任务 3 模板 + 任务 4 toggleGroup)、多选(toggleDisease)、★ 角标说明(showRiskTip)、已选区 + 设确诊时间(任务 3 已选区 + onDiagChange)、自定义补充(addCustomDisease)✓
+- 6.1.2 用药区:startDate 年月选择器(任务 5 onMedStartChange)、时长自动计算(calcDuration)、记不清兜底(onForgetClick)✓
+- 4.2 JSON 结构(buildPayload 任务 6)✓
+- 边界:年月不可选未来(monthEnd)、v1 旧数据兼容(loadDiseases/parseMeds)、空提交校验(v1 保留)✓
+- 非目标确认:无后端变更、无 api.js 变更、无 plan-summary 变更(v1 已完成)✓
+
+**2. 占位符扫描:** 无 TODO/待定/占位代码;所有方法含完整实现。✓
+
+**3. 类型一致性:** `selectedDiseases` 元素为 `{name, diagnosedAt, custom}`(toggleDisease/loadDiseases/buildPayload 三处一致);`medRows` 元素为 `{name, startDate, durationText, time}`(addMed/parseMeds/onMedStartChange/buildPayload 四处一致);computed `selectedNames` 为字符串数组,模板 `:class` 用 `indexOf` 匹配。✓