面向 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 符合小程序限制文件:
修改: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 }
]}
]
运行:
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 与后续任务须一并完成后再验证整体,此处先确认常量区语法无误即可。
文件:
修改: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
}
},
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(pickerend上限,防止选择未来年月)。new Date(y, m, 0)为数字构造,非字符串解析,iOS 安全。
运行:
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
预期:无输出(通过)。
文件:
修改: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防止点 ★ 触发选中
运行:
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"。
文件:
修改: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 },
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→ 默认''不报错;加载时自动展开已选疾病所在组。
将:
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。
运行:
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
预期:无输出(通过)。
文件:
修改: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>
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默认''不报错。
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+ 能力,用于「记不清」时输入时长文本兜底。
运行:
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
预期:无输出(通过)。
文件:
修改: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}](各字段有值才写)。
data() 后新增 computed.selectedNames在 onLoad: function() {...} 之前插入:
computed: {
selectedNames: function() {
return this.selectedDiseases.map(function(d) { return d.name })
}
},
在 <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规则。
运行:
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
预期:无输出(通过)。
文件:
修改: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 两数字相等。
按设计文档第 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 | 空提交 | 提示「请至少填写一项」 |
在「首页重构(主动健康漏斗)」行之后新增一行:
| 当前状态调研(健康现状档案 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. 规格覆盖度:
2. 占位符扫描: 无 TODO/待定/占位代码;所有方法含完整实现。✓
3. 类型一致性: selectedDiseases 元素为 {name, diagnosedAt, custom}(toggleDisease/loadDiseases/buildPayload 三处一致);medRows 元素为 {name, startDate, durationText, time}(addMed/parseMeds/onMedStartChange/buildPayload 四处一致);computed selectedNames 为字符串数组,模板 :class 用 indexOf 匹配。✓