2026-08-04-report-confirm-member-selector.md 17 KB

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)之后插入:

      showCandidatesOnly: false,
      // 被检测人选择面板
      showMemberPanel: false,
      selectedMemberObj: null,
  • Step 2: 修改 computed — 新增 memberDisplayText

在 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)分组,保持解析顺序,统计需处理(非正常)数量
  • Step 3: 修改 onLoad — loadFamilyMembers 改为始终调用

将 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
      }
    }
  • Step 4: 修改 loadFamilyMembers — 成功回调同步选中成员

在 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 = []
      })
    },
  • Step 5: 替换 selectMember/selectCandidate — 新增 toggleMemberPanel / selectAndClose / syncSelectedMemberObj

删除 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: 候选匹配与新建成员 === 注释块位置不变。

  • Step 6: 修改 doAutoCreate — 成功分支回填 + 收起面板

将成功分支(约 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' })
          }
  • Step 7: 修改模板 — 被检测人行可点击 + 内联面板

将 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>
  • Step 8: 修改模板 — 删除独立"选择家庭成员"卡片与"新建家庭成员表单"卡片

删除整个 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 被检测人选择整合进基本信息卡片(内联面板+移除独立卡片)"
    

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

  • [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;
}
  • Step 2: 移除死样式 .auto-create-card

删除(约 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 死样式"
    

Task 3: 终验与提交

Files:

  • Verify: 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 次)。

  • 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 方法调用
  • [x] Step 3: 最终构建

    cd cfc-frontend
    npm run build:mp-weixin
    

Expected: 退出码 0,无 ERROR。

  • Step 4: 更新 PROJECT-OVERVIEW.md 计划索引

在 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 无未提交改动(除本计划外的既有改动除外)。