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