Просмотр исходного кода

docs: report-confirm 被检测人选择面板整合实现计划

asus 1 месяц назад
Родитель
Сommit
0c588156f1

+ 2 - 0
docs/superpowers/PROJECT-OVERVIEW.md

@@ -391,6 +391,8 @@
 | `2026-08-02-virtual-payment-implementation.md` | 🟢 已实施 | 小程序虚拟支付接入实施计划(12 Tasks:建表/签名/推送/三链路改造/前端/管理端) |
 | `2026-08-04-virtual-payment-refund-followup.md` | 🔵 pending | 虚拟支付退款闭环 follow-up(会员退款ORD分支 + iOS退款问询按官方协议接入) |
 | `2026-08-04-registration-profile-backfill.md` | ✅ 已实施(12 Tasks:注册解耦最小集 + 个人中心卡片 + 功能场景就地补充自动回写 + 无家庭引导组件) | 新用户注册引导实施计划 |
+| `2026-08-04-report-confirm-member-selector.md` | 🟡 计划中 | 被检测人选择面板整合实现计划(report-confirm.vue 单文件) |
+| `2026-08-04-family-challenge-custom.md` | 🟢 已实施 | 家庭挑战自定义管理(创建/编辑/删除/模板) |
 
 ### 计划与设计文档(specs/)
 

+ 458 - 0
docs/superpowers/plans/2026-08-04-report-confirm-member-selector.md

@@ -0,0 +1,458 @@
+# 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 (`- [ ]`) 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)`。
+
+- [ ] **Step 1: 修改 data() — 新增两个字段**
+
+在 `showCandidatesOnly: false,`(约 L308)之后插入:
+
+```js
+      showCandidatesOnly: false,
+      // 被检测人选择面板
+      showMemberPanel: false,
+      selectedMemberObj: null,
+```
+
+- [ ] **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)分组,保持解析顺序,统计需处理(非正常)数量
+```
+
+- [ ] **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
+      }
+    }
+```
+
+- [ ] **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 = []
+      })
+    },
+```
+
+- [ ] **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: 候选匹配与新建成员 ===` 注释块位置不变。
+
+- [ ] **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' })
+          }
+```
+
+- [ ] **Step 7: 修改模板 — 被检测人行可点击 + 内联面板**
+
+将 L30-34 的被检测人字段行:
+
+```html
+        <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>
+```
+
+替换为:
+
+```html
+        <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>
+```
+
+- [ ] **Step 8: 修改模板 — 删除独立"选择家庭成员"卡片与"新建家庭成员表单"卡片**
+
+删除整个 L57-107(`<!-- 家庭成员绑定(含候选推荐) -->` 卡片,`v-if="needBind"`)与 L109-131(`<!-- 新建家庭成员表单 -->` 卡片,`v-if="showAutoCreateForm"`)。删除后,`<!-- 舌诊指标(只读展示) -->` 注释成为基本信息卡片后的下一块。
+
+- [ ] **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 中是正常的),逐个确认属于新增逻辑或保留逻辑。
+
+- [ ] **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/`。
+
+- [ ] **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` 不出现。
+
+- [ ] **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`(`<style scoped>` 块)
+
+**Interfaces:**
+- Consumes: Task 1 新增的模板类 `member-select-row` / `member-select-arrow` / `member-panel` / `auto-create-inline`
+- Produces: 以上 4 个类的基础样式;移除死样式 `.auto-create-card`
+
+- [ ] **Step 1: 新增被检测人行与面板样式**
+
+在 `.field-value.binding-required` 规则(约 L833-835)之后插入:
+
+```css
+.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;
+}
+```
+
+- [ ] **Step 2: 移除死样式 .auto-create-card**
+
+删除(约 L1365-1368):
+
+```css
+/* ===== Phase 2: 新建成员表单 ===== */
+.auto-create-card {
+  border: 2rpx solid #E3F2FD;
+}
+```
+
+将注释改为 `/* ===== Phase 2: 新建成员表单(面板内嵌) ===== */`,保留其后的 `.form-item` 等规则。
+
+- [ ] **Step 3: 构建验证**
+
+```bash
+cd cfc-frontend
+npm run build:mp-weixin
+```
+
+Expected: 退出码 0,无 `ERROR`。
+
+- [ ] **Step 4: Commit**
+
+```bash
+cd D:\workspace\cfc
+git add cfc-frontend/pages/health/report-confirm.vue
+git commit -m "feat: report-confirm 面板样式 + 清理 auto-create-card 死样式"
+```
+
+---
+
+### Task 3: 终验与提交
+
+**Files:**
+- Verify: `cfc-frontend/pages/health/report-confirm.vue`、构建产物
+
+**Interfaces:**
+- Consumes: Task 1-2 全部改动
+
+- [ ] **Step 1: 全量残留检查**
+
+```bash
+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 次)。
+
+- [ ] **Step 2: 逻辑核对(人工阅读)**
+
+阅读 `cfc-frontend/pages/health/report-confirm.vue` 确认:
+1. `canConfirm` 计算属性未改动(`needBind && !selectedMemberId && !createdMemberId` → false)
+2. `doConfirm` 的 subjectId 优先级未改动(selectedMemberId > createdMemberId > matchedMemberId > memberId)
+3. 模板中不再有独立的选择/新建卡片,面板仅在 `showMemberPanel` 为 true 时渲染
+4. 面板内新建表单提交走 `doAutoCreate`(非 `addFamilyMember` 直调)
+5. 无中文 CSS 类名、无可选链 `?.`、无 `:class` 方法调用
+
+- [ ] **Step 3: 最终构建**
+
+```bash
+cd cfc-frontend
+npm run build:mp-weixin
+```
+
+Expected: 退出码 0,无 `ERROR`。
+
+- [ ] **Step 4: 更新 PROJECT-OVERVIEW.md 计划索引**
+
+在 `docs/superpowers/PROJECT-OVERVIEW.md` 的"实施计划(plans/)"表格末尾新增:
+
+```markdown
+| `2026-08-04-report-confirm-member-selector.md` | 🟡 计划中 | 被检测人选择面板整合实现计划(report-confirm.vue 单文件) |
+```
+
+并提交:
+
+```bash
+cd D:\workspace\cfc
+git add docs/superpowers/PROJECT-OVERVIEW.md
+git commit -m "docs: PROJECT-OVERVIEW 登记 report-confirm 选择面板实现计划"
+```
+
+- [ ] **Step 5: 汇总提交状态**
+
+```bash
+cd D:\workspace\cfc
+git log --oneline -5
+git status --short
+```
+
+Expected: 最近 3 个 commit 为本计划的 Task 1/2/3 提交;`git status --short` 无未提交改动(除本计划外的既有改动除外)。