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
cfc-frontend/pages/health/report-confirm.vue,禁止改动后端?.(用 && 链式判断):class 绑定中调用方法(用内联表达式或计算属性):key 表达式(用 idx/mIdx 下标)@PostMapping(本任务无接口变更)feat: / fix: / docs: 前缀,中文描述)Files:
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)之后插入:
showCandidatesOnly: false,
// 被检测人选择面板
showMemberPanel: false,
selectedMemberObj: null,
在 confidenceDesc()(约 L351 结束)与 indicatorGroups() 之间插入:
},
memberDisplayText() {
var m = this.selectedMemberObj
if (m && (m.nickname || m.name)) return m.nickname || m.name
if (this.needBind) return '需手动绑定'
return this.extractedName || '未识别'
},
// 指标按类型(category)分组,保持解析顺序,统计需处理(非正常)数量
将 L427-438 的:
// 有候选时优先展示候选列表
if (this.candidates.length > 0) {
this.showCandidatesOnly = true
}
if (this.needBind) {
this.loadFamilyMembers()
// 高置信度且已匹配 → 直接选中
if (this.matchedMemberId && this.matchConfidence >= 0.8) {
this.selectedMemberId = this.matchedMemberId
}
}
替换为:
// 有候选时优先展示候选列表
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
}
}
在 self.familyMembers = parents.concat(children) 之后、.catch 之前插入 self.syncSelectedMemberObj():
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 = []
})
},
删除 selectMember(member)(L458-460)与 selectCandidate(candidate)(L467-470)两个方法,在原 selectMember 位置插入:
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: 候选匹配与新建成员 === 注释块位置不变。
将成功分支(约 L495-502):
if (memberId) {
self.createdMemberId = memberId
self.selectedMemberId = memberId
self.showAutoCreateForm = false
uni.showToast({ title: '创建成功', icon: 'success' })
}
替换为:
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' })
}
将 L30-34 的被检测人字段行:
<view class="field-row">
<text class="field-label">被检测人</text>
<text class="field-value" v-if="!needBind">{{ extractedName || '未识别' }}</text>
<text class="field-value binding-required" v-else>需手动绑定</text>
</view>
替换为:
<view class="field-row member-select-row" @click="toggleMemberPanel">
<text class="field-label">被检测人</text>
<text class="field-value" :class="{ 'binding-required': needBind && !selectedMemberObj }">{{ memberDisplayText }}</text>
<text class="member-select-arrow">{{ selectedMemberObj ? '修改 ›' : '选择 ›' }}</text>
</view>
<!-- 被检测人选择面板(内联展开) -->
<view class="member-panel" v-if="showMemberPanel">
<!-- 候选匹配列表(按置信度排序) -->
<view class="candidate-section" v-if="candidates.length > 0">
<text class="candidate-section-hint">以下成员与报告信息匹配,请选择:</text>
<view
class="candidate-item"
v-for="(cand, idx) in candidates"
:key="idx"
:class="{ 'candidate-selected': selectedMemberId === (cand.id || cand.userId) }"
@click="selectAndClose(cand)"
>
<view class="candidate-left">
<text class="candidate-avatar">{{ (cand.nickname || cand.name || '?').substring(0, 1) }}</text>
<view class="candidate-info">
<text class="candidate-name">{{ cand.nickname || cand.name || '未知' }}</text>
<text class="candidate-detail" v-if="cand.gender || cand.age">{{ genderText(cand.gender) }}{{ cand.age ? ' · ' + cand.age + '岁' : '' }}</text>
<text class="candidate-fields" v-if="cand.matchFields">匹配: {{ cand.matchFields }}</text>
</view>
</view>
<view class="candidate-right">
<view class="score-bar-track">
<view class="score-bar-fill" :style="'width:' + (cand.similarityScore * 100) + '%'"></view>
</view>
<text class="score-text">{{ Math.round(cand.similarityScore * 100) }}%</text>
</view>
</view>
</view>
<!-- 全量家庭成员列表(后备) -->
<view class="member-list" v-if="familyMembers.length > 0 && (candidates.length === 0 || !showCandidatesOnly)">
<view
class="member-item"
v-for="(member, mIdx) in familyMembers"
:key="mIdx"
:class="{ 'member-selected': selectedMemberId === (member.userId || member.id) }"
@click="selectAndClose(member)"
>
<text class="member-avatar">{{ (member.nickname || '?').substring(0, 1) }}</text>
<text class="member-name">{{ member.nickname || '未知' }}</text>
</view>
</view>
<!-- 新建成员入口 -->
<view class="create-member-entry" @click="toggleAutoCreate">
<text class="create-member-icon">+</text>
<text class="create-member-text">{{ showAutoCreateForm ? '收起新建表单' : '不在列表中?新建家庭成员' }}</text>
</view>
<!-- 新建成员表单(面板内嵌) -->
<view class="auto-create-inline" v-if="showAutoCreateForm">
<view class="form-item">
<text class="form-item-label">姓名</text>
<input class="form-item-input" v-model="newMember.name" placeholder="请输入姓名" />
</view>
<view class="form-item">
<text class="form-item-label">性别</text>
<view class="gender-selector">
<text class="gender-option" :class="{ 'gender-selected': newMember.gender === 'male' }" @click="newMember.gender = 'male'">男</text>
<text class="gender-option" :class="{ 'gender-selected': newMember.gender === 'female' }" @click="newMember.gender = 'female'">女</text>
</view>
</view>
<view class="form-item">
<text class="form-item-label">年龄</text>
<input class="form-item-input" type="number" v-model="newMember.age" placeholder="请输入年龄" />
</view>
<view class="btn-create-submit" :class="{ 'btn-disabled': creatingMember }" @click="doAutoCreate">
<text v-if="creatingMember">创建中...</text>
<text v-else>确认创建</text>
</view>
</view>
</view>
删除整个 L57-107(<!-- 家庭成员绑定(含候选推荐) --> 卡片,v-if="needBind")与 L109-131(<!-- 新建家庭成员表单 --> 卡片,v-if="showAutoCreateForm")。删除后,<!-- 舌诊指标(只读展示) --> 注释成为基本信息卡片后的下一块。
[x] Step 9: 验证脚本层无残留引用
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: 构建验证
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 类绑定
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
cd D:\workspace\cfc
git add cfc-frontend/pages/health/report-confirm.vue
git commit -m "feat: report-confirm 被检测人选择整合进基本信息卡片(内联面板+移除独立卡片)"
Files:
cfc-frontend/pages/health/report-confirm.vue(<style scoped> 块)Interfaces:
member-select-row / member-select-arrow / member-panel / auto-create-inlineProduces: 以上 4 个类的基础样式;移除死样式 .auto-create-card
[x] Step 1: 新增被检测人行与面板样式
在 .field-value.binding-required 规则(约 L833-835)之后插入:
.member-select-row {
cursor: pointer;
}
.member-select-row:active {
opacity: 0.7;
}
.member-select-arrow {
font-size: 24rpx;
color: #5B9BD5;
margin-left: 12rpx;
flex-shrink: 0;
}
.member-panel {
background: #F8FAFB;
border-radius: 12rpx;
padding: 16rpx;
margin: 8rpx 0 16rpx;
}
删除(约 L1365-1368):
/* ===== Phase 2: 新建成员表单 ===== */
.auto-create-card {
border: 2rpx solid #E3F2FD;
}
将注释改为 /* ===== Phase 2: 新建成员表单(面板内嵌) ===== */,保留其后的 .form-item 等规则。
[x] Step 3: 构建验证
cd cfc-frontend
npm run build:mp-weixin
Expected: 退出码 0,无 ERROR。
[x] Step 4: Commit
cd D:\workspace\cfc
git add cfc-frontend/pages/health/report-confirm.vue
git commit -m "feat: report-confirm 面板样式 + 清理 auto-create-card 死样式"
Files:
cfc-frontend/pages/health/report-confirm.vue、构建产物Interfaces:
Consumes: Task 1-2 全部改动
[x] Step 1: 全量残留检查
cd cfc-frontend
grep -n "selectCandidate\|selectMember\|auto-create-card\|选择家庭成员" pages/health/report-confirm.vue
grep -c "showMemberPanel\|selectedMemberObj\|selectAndClose\|toggleMemberPanel\|syncSelectedMemberObj\|memberDisplayText" pages/health/report-confirm.vue
Expected: 第一个命令 0 匹配;第二个命令 ≥ 6(每个新符号至少出现 1 次)。
阅读 cfc-frontend/pages/health/report-confirm.vue 确认:
canConfirm 计算属性未改动(needBind && !selectedMemberId && !createdMemberId → false)doConfirm 的 subjectId 优先级未改动(selectedMemberId > createdMemberId > matchedMemberId > memberId)showMemberPanel 为 true 时渲染doAutoCreate(非 addFamilyMember 直调)?.、无 :class 方法调用[x] Step 3: 最终构建
cd cfc-frontend
npm run build:mp-weixin
Expected: 退出码 0,无 ERROR。
在 docs/superpowers/PROJECT-OVERVIEW.md 的"实施计划(plans/)"表格末尾新增:
| `2026-08-04-report-confirm-member-selector.md` | 🟡 计划中 | 被检测人选择面板整合实现计划(report-confirm.vue 单文件) |
并提交:
cd D:\workspace\cfc
git add docs/superpowers/PROJECT-OVERVIEW.md
git commit -m "docs: PROJECT-OVERVIEW 登记 report-confirm 选择面板实现计划"
[x] Step 5: 汇总提交状态
cd D:\workspace\cfc
git log --oneline -5
git status --short
Expected: 最近 3 个 commit 为本计划的 Task 1/2/3 提交;git status --short 无未提交改动(除本计划外的既有改动除外)。