2026-08-18-health-status-survey-v2.md 30 KB

「当前状态调研」健康现状档案 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:替换常量区

将:

var DISEASE_PRESETS = ['高血压', '糖尿病', '高血脂', '过敏性鼻炎', '哮喘', '贫血', '胃炎', '甲状腺疾病', '心脏疾病', '肾脏疾病']

替换为(8 组 54 项,★ = 菌群报告病症风险源,与设计 6.1.1 表一致):

// 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:语法校验

运行:

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 行):

  data() {
    return {
      form: {
        heightCm: '',
        weightKg: '',
        bloodPressure: '',
        bloodGlucose: '',
        bloodLipids: '',
        diseaseHistory: '',
        medications: '',
        notes: ''
      },
      lipidCholesterol: '',
      diseaseTags: DISEASE_PRESETS.slice(),
      diseaseNames: [],
      customDiseases: [],
      diseaseInput: '',
      medRows: [],
      saving: false
    }
  },

替换为:

  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 行):

  onLoad: function() {
    this.load()
  },

替换为:

  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:语法校验

运行:

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:替换疾病史区模板

将:

    <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>

替换为:

    <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:模板结构校验

运行:

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 方法

将:

    getCustomKey: function(i) { return 'c' + i },

替换为:

    getSelKey: function(i) { return 's' + i },
  • 步骤 2:替换 loadDiseases(v2 结构 + v1 兼容)

将:

    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) {}
    },

替换为:

    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:替换疾病相关方法组

将:

    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)
    },

替换为:

    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:语法校验

运行:

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:替换用药区模板

将:

    <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>

替换为:

    <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 兼容)

将:

    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
    },

替换为:

    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 + 新增用药方法

将:

    addMed: function() {
      this.medRows.push({ name: '', time: '' })
    },

替换为:

    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:语法校验

运行:

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 构造

将:

      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 || '' })
        }
      })

替换为:

      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() {...} 之前插入:

  computed: {
    selectedNames: function() {
      return this.selectedDiseases.map(function(d) { return d.name })
    }
  },
  • 步骤 3:样式补充

在 <style scoped> 内、.form-title 规则之后追加:

.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:语法校验

运行:

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:完整语法与结构校验

    # 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

在「首页重构(主动健康漏斗)」行之后新增一行:

| 当前状态调研(健康现状档案 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

    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 匹配。✓