# report-confirm 被检测人选择面板整合 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax for tracking.
**Goal:** 将 `report-confirm.vue` 中被检测人的选择/添加功能整合进"基本信息"卡片:点击被检测人字段原地展开选择面板(候选匹配 + 全量成员 + 新建入口),移除独立"选择家庭成员"卡片与"新建家庭成员表单"卡片。
**Architecture:** 单文件前端改造(`cfc-frontend/pages/health/report-confirm.vue`)。模板层把原独立卡片(L57-131)的内容移入基本信息卡片的被检测人字段下方,包在 `v-if="showMemberPanel"` 面板容器内;逻辑层新增 `showMemberPanel`/`selectedMemberObj`/`selectAndClose`/`toggleMemberPanel`/`syncSelectedMemberObj`/`memberDisplayText`,移除 `selectMember`/`selectCandidate`(其逻辑并入 `selectAndClose`),`loadFamilyMembers` 改为始终调用。`canConfirm`/`doConfirm` 完全不变。无后端改动。
**Tech Stack:** uni-app Vue 2 (Options API) / 微信小程序 mp-weixin
## Global Constraints
- 仅前端:`cfc-frontend/pages/health/report-confirm.vue`,禁止改动后端
- Vue 2 Options API,禁止 Composition API
- 禁止可选链 `?.`(用 `&&` 链式判断)
- 禁止在 `:class` 绑定中调用方法(用内联表达式或计算属性)
- 禁止中文 CSS 类名/选择器
- 小程序用 flexbox,禁止 CSS Grid
- 禁止 `:key` 表达式(用 `idx`/`mIdx` 下标)
- 统一 `@PostMapping`(本任务无接口变更)
- 提交信息遵循仓库风格(`feat:` / `fix:` / `docs:` 前缀,中文描述)
---
### Task 1: 核心改造 — 模板 + 脚本
**Files:**
- Modify: `cfc-frontend/pages/health/report-confirm.vue`(模板 L28-55 基本信息卡片、移除 L57-131、脚本 data/computed/onLoad/methods)
**Interfaces:**
- Produces: 新 data 字段 `showMemberPanel: false`、`selectedMemberObj: null`;新计算属性 `memberDisplayText`;新方法 `toggleMemberPanel()`、`selectAndClose(member)`、`syncSelectedMemberObj()`;`loadFamilyMembers()` 成功回调内新增 `syncSelectedMemberObj()` 调用;`doAutoCreate()` 成功分支新增回填+收起逻辑;移除 `selectMember(member)`、`selectCandidate(candidate)`。
- [x] **Step 1: 修改 data() — 新增两个字段**
在 `showCandidatesOnly: false,`(约 L308)之后插入:
```js
showCandidatesOnly: false,
// 被检测人选择面板
showMemberPanel: false,
selectedMemberObj: null,
```
- [x] **Step 2: 修改 computed — 新增 memberDisplayText**
在 `confidenceDesc()`(约 L351 结束)与 `indicatorGroups()` 之间插入:
```js
},
memberDisplayText() {
var m = this.selectedMemberObj
if (m && (m.nickname || m.name)) return m.nickname || m.name
if (this.needBind) return '需手动绑定'
return this.extractedName || '未识别'
},
// 指标按类型(category)分组,保持解析顺序,统计需处理(非正常)数量
```
- [x] **Step 3: 修改 onLoad — loadFamilyMembers 改为始终调用**
将 L427-438 的:
```js
// 有候选时优先展示候选列表
if (this.candidates.length > 0) {
this.showCandidatesOnly = true
}
if (this.needBind) {
this.loadFamilyMembers()
// 高置信度且已匹配 → 直接选中
if (this.matchedMemberId && this.matchConfidence >= 0.8) {
this.selectedMemberId = this.matchedMemberId
}
}
```
替换为:
```js
// 有候选时优先展示候选列表
if (this.candidates.length > 0) {
this.showCandidatesOnly = true
}
// 始终加载家庭成员(面板始终可点击,needBind=false 也允许改选)
this.loadFamilyMembers()
if (this.needBind) {
// 高置信度且已匹配 → 直接选中
if (this.matchedMemberId && this.matchConfidence >= 0.8) {
this.selectedMemberId = this.matchedMemberId
}
}
```
- [x] **Step 4: 修改 loadFamilyMembers — 成功回调同步选中成员**
在 `self.familyMembers = parents.concat(children)` 之后、`.catch` 之前插入 `self.syncSelectedMemberObj()`:
```js
loadFamilyMembers() {
var self = this
// 从后端获取真实家庭成员(家长 + 孩子),storage 中并无该数据
getFamilyMembers().then(function(res) {
var data = (res && res.data) || {}
var parents = (data.parents || []).map(function(p) {
return { id: p.id, nickname: p.realName || p.nickname, gender: p.gender, role: 'parent' }
})
var children = (data.children || []).map(function(c) {
return { id: c.id, nickname: c.nickname, gender: c.gender, age: c.age, role: 'child' }
})
self.familyMembers = parents.concat(children)
self.syncSelectedMemberObj()
}).catch(function(e) {
console.log('加载家庭成员失败', e)
self.familyMembers = []
})
},
```
- [x] **Step 5: 替换 selectMember/selectCandidate — 新增 toggleMemberPanel / selectAndClose / syncSelectedMemberObj**
删除 `selectMember(member)`(L458-460)与 `selectCandidate(candidate)`(L467-470)两个方法,在原 `selectMember` 位置插入:
```js
toggleMemberPanel() {
this.showMemberPanel = !this.showMemberPanel
if (!this.showMemberPanel) {
this.showAutoCreateForm = false
}
},
selectAndClose(member) {
var memberId = member.id || member.userId
this.selectedMemberId = memberId
this.selectedMemberObj = member
this.extractedName = member.nickname || member.name || this.extractedName
this.extractedGender = member.gender || this.extractedGender
this.extractedAge = member.age || this.extractedAge
this.showAutoCreateForm = false
this.showMemberPanel = false
},
syncSelectedMemberObj() {
if (!this.selectedMemberId) {
this.selectedMemberObj = null
return
}
var all = this.familyMembers.concat(this.candidates)
for (var i = 0; i < all.length; i++) {
var m = all[i]
if ((m.id || m.userId) === this.selectedMemberId) {
this.selectedMemberObj = m
return
}
}
},
```
注意:删除 `selectCandidate` 时保留其后的 `// === Phase 2: 候选匹配与新建成员 ===` 注释块位置不变。
- [x] **Step 6: 修改 doAutoCreate — 成功分支回填 + 收起面板**
将成功分支(约 L495-502):
```js
if (memberId) {
self.createdMemberId = memberId
self.selectedMemberId = memberId
self.showAutoCreateForm = false
uni.showToast({ title: '创建成功', icon: 'success' })
}
```
替换为:
```js
if (memberId) {
self.createdMemberId = memberId
self.selectedMemberId = memberId
self.selectedMemberObj = {
id: memberId,
nickname: self.newMember.name,
gender: self.newMember.gender,
age: self.newMember.age
}
self.extractedName = self.newMember.name
self.extractedGender = self.newMember.gender
self.extractedAge = self.newMember.age
self.showAutoCreateForm = false
self.showMemberPanel = false
uni.showToast({ title: '创建成功', icon: 'success' })
}
```
- [x] **Step 7: 修改模板 — 被检测人行可点击 + 内联面板**
将 L30-34 的被检测人字段行:
```html
被检测人
{{ extractedName || '未识别' }}
需手动绑定
```
替换为:
```html
被检测人
{{ memberDisplayText }}
{{ selectedMemberObj ? '修改 ›' : '选择 ›' }}
以下成员与报告信息匹配,请选择:
{{ (cand.nickname || cand.name || '?').substring(0, 1) }}
{{ cand.nickname || cand.name || '未知' }}
{{ genderText(cand.gender) }}{{ cand.age ? ' · ' + cand.age + '岁' : '' }}
匹配: {{ cand.matchFields }}
{{ Math.round(cand.similarityScore * 100) }}%
{{ (member.nickname || '?').substring(0, 1) }}
{{ member.nickname || '未知' }}
+
{{ showAutoCreateForm ? '收起新建表单' : '不在列表中?新建家庭成员' }}
姓名
性别
男
女
年龄
创建中...
确认创建
```
- [x] **Step 8: 修改模板 — 删除独立"选择家庭成员"卡片与"新建家庭成员表单"卡片**
删除整个 L57-107(`` 卡片,`v-if="needBind"`)与 L109-131(`` 卡片,`v-if="showAutoCreateForm"`)。删除后,`` 注释成为基本信息卡片后的下一块。
- [x] **Step 9: 验证脚本层无残留引用**
```bash
cd cfc-frontend
grep -n "selectCandidate\|selectMember(" pages/health/report-confirm.vue
grep -n "选择家庭成员\|auto-create-card" pages/health/report-confirm.vue
```
Expected: 无输出(0 匹配)。若有残留(如 `showCandidatesOnly` 定义仍在 data 中是正常的),逐个确认属于新增逻辑或保留逻辑。
- [x] **Step 10: 构建验证**
```bash
cd cfc-frontend
npm run build:mp-weixin
```
Expected: 退出码 0,输出无 `ERROR`(允许 3 个既有 `"export 'components' was not found"` warning)。构建产物出现在 `dist/build/mp-weixin/pages/health/`。
- [x] **Step 11: 检查编译产物 — 无方法调用泄漏进 WXML 类绑定**
```bash
cd cfc-frontend
Select-String -Path "dist/build/mp-weixin/pages/health/report-confirm.wxml" -Pattern "selectAndClose|selectCandidate|selectMember|memberDisplayText|toggleMemberPanel" | ForEach-Object { $_.Line.Substring(0, [Math]::Min(200, $_.Line.Length)) }
```
Expected: `selectAndClose`/`toggleMemberPanel` 以 `data-event-opts` 事件绑定形式存在;`memberDisplayText` 被编译为预计算值(如 `{{...mN}}` 形式,非函数调用);`selectCandidate`/`selectMember` 不出现。
- [x] **Step 12: Commit**
```bash
cd D:\workspace\cfc
git add cfc-frontend/pages/health/report-confirm.vue
git commit -m "feat: report-confirm 被检测人选择整合进基本信息卡片(内联面板+移除独立卡片)"
```
---
### Task 2: 样式补充与死样式清理
**Files:**
- Modify: `cfc-frontend/pages/health/report-confirm.vue`(`