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

refactor(member): T25 P2 — 戒备/饮食记录/测评页内联选择器改用统一组件

批次 2(P2 单选内联选择器):
- diet/records、growth/caution-detail、health/gut-flora-foods-detail:
  内联 member-bar 换成 MemberSelector(minimal),主键由 (item.id || item.userId)
  改为 canonical memberId;「全部」由合成伪项改为组件的 overview 聚合项
- psych-assessment/index、cognitive-hall:
  内联 member-tabs 换成 MemberSelector(minimal, embedded),保留
  「选择测评对象:」文案;成员列表先经 normalizeMembers 折叠
- MemberSelector:showSelf=false 时保留本人行(按昵称展示),
  避免该形态下本人被静默丢弃
- memberNormalize:新增 resolveMemberId,把 URL/缓存里的历史标识
  (users.id、遗留 children.id)归一到 familyMemberId

顺带修正:原先「全量成员」列表用 userId 优先、而选中写入用 id 优先,
两者不一致导致选中项不高亮。
Sisyphus 2 недель назад
Родитель
Сommit
11d50306df

+ 4 - 2
cfc-frontend/components/MemberSelector.vue

@@ -320,9 +320,11 @@ export default {
       }
       }
       return out
       return out
     },
     },
-    // minimal 变体:其余成员(不含本人行,同一用户至多出现一次)
+    // minimal 变体:各成员 chip 列表。
+    // showSelf=true 时排除本人行(本人已由「个人」项承载,避免同一人出现两次);
+    // showSelf=false 时保留全部成员(含本人,按昵称展示,保持原页面语义)。
     otherMembers: function() {
     otherMembers: function() {
-      return membersExceptSelf(this.decoratedMembers)
+      return this.showSelf ? membersExceptSelf(this.decoratedMembers) : this.decoratedMembers
     },
     },
     // minimal 变体:「个人」项选中态(selfActive 兜底「未显式选择即视为本人」)
     // minimal 变体:「个人」项选中态(selfActive 兜底「未显式选择即视为本人」)
     selfChipActive: function() {
     selfChipActive: function() {

+ 28 - 37
cfc-frontend/pages/cognitive-hall.vue

@@ -7,17 +7,13 @@
 
 
     <view class="member-selector" v-if="members.length > 1">
     <view class="member-selector" v-if="members.length > 1">
       <text class="selector-label">选择测评对象:</text>
       <text class="selector-label">选择测评对象:</text>
-      <view class="member-tabs">
-        <view
-          class="member-tab"
-          :class="{ 'member-tab-active': selectedMemberId === m.id }"
-          v-for="m in members"
-          :key="m.id"
-          @click="selectMember(m.id)"
-        >
-          <text>{{ m.nickname }}</text>
-        </view>
-      </view>
+      <MemberSelector
+        variant="minimal"
+        embedded
+        :members="members"
+        :selected-member-id="selectedMemberId"
+        :show-self="false"
+        @select="onMemberSelect" />
     </view>
     </view>
 
 
     <view class="loading" v-if="loading">
     <view class="loading" v-if="loading">
@@ -57,12 +53,15 @@
 </template>
 </template>
 
 
 <script>
 <script>
+import MemberSelector from '@/components/MemberSelector.vue'
+import { normalizeMembers, memberIdOf, resolveMemberId } from '@/utils/memberNormalize.js'
 import { getCognitiveChildScores } from '../utils/api.js'
 import { getCognitiveChildScores } from '../utils/api.js'
 import { getFamilyMemberList } from '../utils/api.js'
 import { getFamilyMemberList } from '../utils/api.js'
 import fontScaleMixin from '@/mixins/font-scale.js'
 import fontScaleMixin from '@/mixins/font-scale.js'
 
 
 export default {
 export default {
   mixins: [fontScaleMixin],
   mixins: [fontScaleMixin],
+  components: { MemberSelector },
   data() {
   data() {
     return {
     return {
       memberId: '',
       memberId: '',
@@ -108,10 +107,18 @@ export default {
       getFamilyMemberList({ visibleOnly: true })
       getFamilyMemberList({ visibleOnly: true })
         .then(function(res) {
         .then(function(res) {
           if (res && res.code === 200 && res.data) {
           if (res && res.code === 200 && res.data) {
-            self.members = res.data
-            if (!self.memberId && res.data.length > 0) {
-              self.memberId = res.data[0].id
-              self.selectedMemberId = res.data[0].id
+            // 前端防御层:按 user_id 折叠同用户多行,isSelf 唯一
+            self.members = normalizeMembers(res.data, { userId: uni.getStorageSync('userId') })
+            // 历史标识(users.id / 遗留 children.id)归一到统一身份键 memberId
+            var mid = resolveMemberId(self.members, self.memberId) || self.memberId
+            if (mid && String(mid) !== String(self.memberId)) {
+              self.memberId = mid
+              self.selectedMemberId = mid
+              self.loadScores()
+            } else if (!self.memberId && self.members.length > 0) {
+              var first = memberIdOf(self.members[0])
+              self.memberId = first
+              self.selectedMemberId = first
               self.loadScores()
               self.loadScores()
             }
             }
           }
           }
@@ -120,8 +127,11 @@ export default {
           // 静默失败
           // 静默失败
         })
         })
     },
     },
-    selectMember: function(id) {
-      this.selectedMemberId = id
+    // 统一组件选中回调:member 已由 memberNormalize 规范化,携带 canonical memberId
+    onMemberSelect: function(member) {
+      var mid = memberIdOf(member)
+      this.memberId = mid
+      this.selectedMemberId = mid
       this.loadScores()
       this.loadScores()
     },
     },
     loadScores: function() {
     loadScores: function() {
@@ -194,6 +204,7 @@ $c-ink-night:    $app-ink-night;   // 深夜蓝黑(本页深色文字)
   padding: 24rpx 30rpx;
   padding: 24rpx 30rpx;
   background: $app-surface;
   background: $app-surface;
   margin: 20rpx 0;
   margin: 20rpx 0;
+.member-selector .member-minimal { margin-top: 16rpx; }
   border-radius: 16rpx;
   border-radius: 16rpx;
   margin-left: 30rpx;
   margin-left: 30rpx;
   margin-right: 30rpx;
   margin-right: 30rpx;
@@ -204,26 +215,6 @@ $c-ink-night:    $app-ink-night;   // 深夜蓝黑(本页深色文字)
   color: $facade-text-body;
   color: $facade-text-body;
 }
 }
 
 
-.member-tabs {
-  display: flex;
-  flex-direction: row;
-  flex-wrap: wrap;
-  margin-top: 16rpx;
-}
-
-.member-tab {
-  padding: 10rpx 28rpx;
-  border-radius: 30rpx;
-  margin: 8rpx 8rpx 0 0;
-  background: $c-border-hair;
-  font-size: calc(26rpx * var(--font-scale, 1));
-  color: $facade-text-body;
-}
-
-.member-tab-active {
-  background: $dim-wisdom;
-  color: $app-surface;
-}
 
 
 .game-list {
 .game-list {
   padding: 0 30rpx;
   padding: 0 30rpx;

+ 26 - 21
cfc-frontend/pages/diet/records.vue

@@ -1,12 +1,17 @@
 <template>
 <template>
   <view class="page" :style="{ '--font-scale': fontScale }">
   <view class="page" :style="{ '--font-scale': fontScale }">
-    <view class="member-bar" v-if="memberList.length > 1">
-      <view class="member-tab" :class="{'active': currentMemberId === (item.id || item.userId)}"
-        v-for="(item, mi) in memberList" :key="mi"
-        @tap="switchMember(item)">
-        <text class="member-name">{{ item.nickname || item.roleLabel || '全部' }}</text>
-      </view>
-    </view>
+    <!-- 成员切换条(统一组件 MemberSelector:同一用户只出现一次,主键为 canonical memberId) -->
+    <MemberSelector
+      variant="minimal"
+      v-if="memberList.length > 0"
+      :members="memberList"
+      :selected-member-id="currentMemberId"
+      :show-overview="true"
+      overview-label="全部"
+      :overview-active="!currentMemberId || currentMemberId < 0"
+      :show-self="false"
+      @overview="selectAllMembers"
+      @select="onMemberSelect" />
 
 
     <!-- 周历 -->
     <!-- 周历 -->
     <view class="week-strip">
     <view class="week-strip">
@@ -175,6 +180,8 @@
 </template>
 </template>
 
 
 <script>
 <script>
+import MemberSelector from '@/components/MemberSelector.vue'
+import { memberIdOf } from '@/utils/memberNormalize.js'
 import { getMealList, getMealDaily, getMealWeekly, getMealAlerts, getCautionFamilyMembers, createMealRecord } from '../../utils/api.js'
 import { getMealList, getMealDaily, getMealWeekly, getMealAlerts, getCautionFamilyMembers, createMealRecord } from '../../utils/api.js'
 import { parseDate } from '../../utils/format.js'
 import { parseDate } from '../../utils/format.js'
 import shareMixin from '../../components/share-mixin.js'
 import shareMixin from '../../components/share-mixin.js'
@@ -182,6 +189,7 @@ import fontScaleMixin from '../../mixins/font-scale.js'
 
 
 export default {
 export default {
   mixins: [shareMixin, fontScaleMixin],
   mixins: [shareMixin, fontScaleMixin],
+  components: { MemberSelector },
   data() {
   data() {
     return {
     return {
       memberId: null,
       memberId: null,
@@ -213,8 +221,16 @@ export default {
       var m = { breakfast: '早餐', lunch: '午餐', dinner: '晚餐', snack: '加餐' }
       var m = { breakfast: '早餐', lunch: '午餐', dinner: '晚餐', snack: '加餐' }
       return m[t] || t || '饮食'
       return m[t] || t || '饮食'
     },
     },
-    switchMember: function(item) {
-      this.currentMemberId = item.id || item.userId
+    // 统一组件选中回调:member 已由 memberNormalize 规范化,携带 canonical memberId
+    onMemberSelect: function(member) {
+      this.currentMemberId = memberIdOf(member)
+      this.loadWeekly()
+      this.loadAlerts()
+      this.loadDayRecords()
+    },
+    // 「全部」聚合项:不传 memberId,后端按全家庭返回
+    selectAllMembers: function() {
+      this.currentMemberId = -1
       this.loadWeekly()
       this.loadWeekly()
       this.loadAlerts()
       this.loadAlerts()
       this.loadDayRecords()
       this.loadDayRecords()
@@ -223,8 +239,7 @@ export default {
       try {
       try {
         var res = await getCautionFamilyMembers()
         var res = await getCautionFamilyMembers()
         if (res && res.code === 200 && res.data && res.data.length > 0) {
         if (res && res.code === 200 && res.data && res.data.length > 0) {
-          var allItem = { id: -1, userId: -1, nickname: '全部', roleLabel: '全部' }
-          this.memberList = [allItem].concat(res.data)
+          this.memberList = res.data
           if (!this.currentMemberId) this.currentMemberId = -1
           if (!this.currentMemberId) this.currentMemberId = -1
         }
         }
       } catch(e) {}
       } catch(e) {}
@@ -377,16 +392,6 @@ export default {
 <style scoped lang="scss">
 <style scoped lang="scss">
 .page { min-height: 100vh; background: $uni-primary-pale; display: flex; flex-direction: column; }
 .page { min-height: 100vh; background: $uni-primary-pale; display: flex; flex-direction: column; }
 
 
-/* 成员切换栏 */
-.member-bar {
-  display: flex; overflow-x: auto; padding: 16rpx 30rpx 8rpx; background: $app-surface;
-  border-bottom: 1rpx solid $facade-fill; gap: 16rpx; -webkit-overflow-scrolling: touch;
-}
-.member-tab {
-  flex-shrink: 0; padding: 8rpx 20rpx; border-radius: 20rpx; background: $facade-fill-2; color: $facade-text-body;
-  font-size: 26rpx; font-weight: 500;
-}
-.member-tab.active { background: linear-gradient(135deg, $dim-body, $dim-body-light); color: $app-surface; }
 
 
 /* 周历条 */
 /* 周历条 */
 .week-strip {
 .week-strip {

+ 24 - 34
cfc-frontend/pages/growth/caution-detail.vue

@@ -6,13 +6,18 @@
       <text class="banner-text">以下食材推荐指数低于 0,建议谨慎食用或避免</text>
       <text class="banner-text">以下食材推荐指数低于 0,建议谨慎食用或避免</text>
     </view>
     </view>
 
 
-    <view class="member-bar" v-if="memberList.length > 1">
-      <view class="member-tab" :class="{'active': currentMemberId === (item.id || item.userId)}"
-        v-for="(item, mi) in memberList" :key="mi"
-        @tap="switchMember(item)">
-        <text class="member-name">{{ item.nickname || item.roleLabel || '全部' }}</text>
-      </view>
-    </view>
+    <!-- 成员切换条(统一组件 MemberSelector:同一用户只出现一次,主键为 canonical memberId) -->
+    <MemberSelector
+      variant="minimal"
+      v-if="memberList.length > 0"
+      :members="memberList"
+      :selected-member-id="currentMemberId"
+      :show-overview="true"
+      overview-label="全部"
+      :overview-active="!currentMemberId || currentMemberId < 0"
+      :show-self="false"
+      @overview="selectAllMembers"
+      @select="onMemberSelect" />
 
 
     <scroll-view class="content" scroll-y :refresher-enabled="true" :refresher-triggered="refreshing" @refresherrefresh="onRefresh">
     <scroll-view class="content" scroll-y :refresher-enabled="true" :refresher-triggered="refreshing" @refresherrefresh="onRefresh">
       <view class="food-card" v-for="(item, idx) in foods" :key="idx">
       <view class="food-card" v-for="(item, idx) in foods" :key="idx">
@@ -62,13 +67,15 @@
 </template>
 </template>
 
 
 <script>
 <script>
+import MemberSelector from '@/components/MemberSelector.vue'
+import { memberIdOf } from '@/utils/memberNormalize.js'
 import { getFoodCautionList, getFoodCautionTrend, getCautionFamilyMembers } from '../../utils/api.js'
 import { getFoodCautionList, getFoodCautionTrend, getCautionFamilyMembers } from '../../utils/api.js'
 import MiniTrend from './mini-trend.vue'
 import MiniTrend from './mini-trend.vue'
 import fontScaleMixin from '@/mixins/font-scale.js'
 import fontScaleMixin from '@/mixins/font-scale.js'
 
 
 export default {
 export default {
   mixins: [fontScaleMixin],
   mixins: [fontScaleMixin],
-  components: { MiniTrend },
+  components: { MiniTrend, MemberSelector },
   data() {
   data() {
     return {
     return {
       foods: [],
       foods: [],
@@ -94,16 +101,21 @@ export default {
     goBack: function() {
     goBack: function() {
       uni.navigateBack()
       uni.navigateBack()
     },
     },
-    switchMember: function(item) {
-      this.currentMemberId = item.id || item.userId
+    // 统一组件选中回调:member 已由 memberNormalize 规范化,携带 canonical memberId
+    onMemberSelect: function(member) {
+      this.currentMemberId = memberIdOf(member)
+      this.loadFoods()
+    },
+    // 「全部」聚合项:不传 memberId,后端按全家庭返回
+    selectAllMembers: function() {
+      this.currentMemberId = -1
       this.loadFoods()
       this.loadFoods()
     },
     },
     async loadMemberList() {
     async loadMemberList() {
       try {
       try {
         var res = await getCautionFamilyMembers()
         var res = await getCautionFamilyMembers()
         if (res && res.code === 200 && res.data && res.data.length > 0) {
         if (res && res.code === 200 && res.data && res.data.length > 0) {
-          var allItem = { id: -1, userId: -1, nickname: '全部', roleLabel: '全部' }
-          this.memberList = [allItem].concat(res.data)
+          this.memberList = res.data
           if (!this.currentMemberId) {
           if (!this.currentMemberId) {
             this.currentMemberId = -1
             this.currentMemberId = -1
           }
           }
@@ -257,28 +269,6 @@ export default {
 .banner-icon { font-size: 32rpx; }
 .banner-icon { font-size: 32rpx; }
 .banner-text { font-size: calc(24rpx * var(--font-scale, 1)); color: #856404; flex: 1; }
 .banner-text { font-size: calc(24rpx * var(--font-scale, 1)); color: #856404; flex: 1; }
 
 
-.member-bar {
-  display: flex;
-  overflow-x: auto;
-  padding: 16rpx 30rpx 8rpx;
-  background: $app-surface;
-  border-bottom: 1rpx solid $facade-fill;
-  gap: 16rpx;
-  -webkit-overflow-scrolling: touch;
-}
-.member-tab {
-  flex-shrink: 0;
-  padding: 8rpx 20rpx;
-  border-radius: 20rpx;
-  background: $facade-fill-2;
-  color: $facade-text-body;
-  font-size: calc(26rpx * var(--font-scale, 1));
-  font-weight: 500;
-}
-.member-tab.active {
-  background: linear-gradient(135deg, $status-danger, $status-danger-deep);
-  color: $app-surface;
-}
 
 
 .content {
 .content {
   padding: 20rpx 30rpx;
   padding: 20rpx 30rpx;

+ 24 - 35
cfc-frontend/pages/health/gut-flora-foods-detail.vue

@@ -1,13 +1,18 @@
 <template>
 <template>
   <view class="page" :style="{ '--font-scale': fontScale }">
   <view class="page" :style="{ '--font-scale': fontScale }">
 
 
-    <view class="member-bar" v-if="memberList.length > 1">
-      <view class="member-tab" :class="{'active': currentMemberId === (item.id || item.userId)}"
-        v-for="(item, mi) in memberList" :key="mi"
-        @tap="switchMember(item)">
-        <text class="member-name">{{ item.nickname || item.roleLabel || '全部' }}</text>
-      </view>
-    </view>
+    <!-- 成员切换条(统一组件 MemberSelector:同一用户只出现一次,主键为 canonical memberId) -->
+    <MemberSelector
+      variant="minimal"
+      v-if="memberList.length > 0"
+      :members="memberList"
+      :selected-member-id="currentMemberId"
+      :show-overview="true"
+      overview-label="全部"
+      :overview-active="!currentMemberId || currentMemberId < 0"
+      :show-self="false"
+      @overview="selectAllMembers"
+      @select="onMemberSelect" />
 
 
     <view class="search-bar">
     <view class="search-bar">
       <view class="search-input-wrap">
       <view class="search-input-wrap">
@@ -134,13 +139,15 @@
 </template>
 </template>
 
 
 <script>
 <script>
+import MemberSelector from '@/components/MemberSelector.vue'
+import { memberIdOf } from '@/utils/memberNormalize.js'
 import { getHealthFoods, editHealthReport, queryFoodKnowledge, getCautionFamilyMembers, getFoodCautionTrend } from '../../utils/api.js'
 import { getHealthFoods, editHealthReport, queryFoodKnowledge, getCautionFamilyMembers, getFoodCautionTrend } from '../../utils/api.js'
 import MiniTrend from '../growth/mini-trend.vue'
 import MiniTrend from '../growth/mini-trend.vue'
 import fontScaleMixin from '@/mixins/font-scale.js'
 import fontScaleMixin from '@/mixins/font-scale.js'
 
 
 export default {
 export default {
   mixins: [fontScaleMixin],
   mixins: [fontScaleMixin],
-  components: { MiniTrend },
+  components: { MiniTrend, MemberSelector },
   data() {
   data() {
     return {
     return {
       reportId: null,
       reportId: null,
@@ -204,16 +211,21 @@ export default {
   },
   },
 methods: {
 methods: {
     goBack: function() { uni.navigateBack() },
     goBack: function() { uni.navigateBack() },
-    switchMember: function(item) {
-      this.currentMemberId = item.id || item.userId
+    // 统一组件选中回调:member 已由 memberNormalize 规范化,携带 canonical memberId
+    onMemberSelect: function(member) {
+      this.currentMemberId = memberIdOf(member)
+      this.loadData()
+    },
+    // 「全部」聚合项:不传 memberId,后端按全家庭返回
+    selectAllMembers: function() {
+      this.currentMemberId = -1
       this.loadData()
       this.loadData()
     },
     },
     async loadMemberList() {
     async loadMemberList() {
       try {
       try {
         var res = await getCautionFamilyMembers()
         var res = await getCautionFamilyMembers()
         if (res && res.code === 200 && res.data && res.data.length > 0) {
         if (res && res.code === 200 && res.data && res.data.length > 0) {
-          var allItem = { id: -1, userId: -1, nickname: '全部', roleLabel: '全部' }
-          this.memberList = [allItem].concat(res.data)
+          this.memberList = res.data
           if (!this.currentMemberId) this.currentMemberId = -1
           if (!this.currentMemberId) this.currentMemberId = -1
         }
         }
       } catch(e) {}
       } catch(e) {}
@@ -384,29 +396,6 @@ methods: {
 <style scoped lang="scss">
 <style scoped lang="scss">
 .page { min-height: 100vh; background: $uni-primary-pale; }
 .page { min-height: 100vh; background: $uni-primary-pale; }
 
 
-/* 成员切换栏 */
-.member-bar {
-  display: flex;
-  overflow-x: auto;
-  padding: 16rpx 30rpx 8rpx;
-  background: $app-surface;
-  border-bottom: 1rpx solid $facade-fill;
-  gap: 16rpx;
-  -webkit-overflow-scrolling: touch;
-}
-.member-tab {
-  flex-shrink: 0;
-  padding: 8rpx 20rpx;
-  border-radius: 20rpx;
-  background: $facade-fill-2;
-  color: $facade-text-body;
-  font-size: calc(26rpx * var(--font-scale, 1));
-  font-weight: 500;
-}
-.member-tab.active {
-  background: linear-gradient(135deg, $dim-body, $dim-body-light);
-  color: $app-surface;
-}
 
 
 /* 搜索框 */
 /* 搜索框 */
 .search-bar { padding: 16rpx 30rpx 8rpx; background: $app-surface; }
 .search-bar { padding: 16rpx 30rpx 8rpx; background: $app-surface; }

+ 23 - 36
cfc-frontend/pages/psych-assessment/index.vue

@@ -7,17 +7,13 @@
 
 
     <view class="member-selector" v-if="members.length > 1">
     <view class="member-selector" v-if="members.length > 1">
       <text class="selector-label">选择测评对象:</text>
       <text class="selector-label">选择测评对象:</text>
-      <view class="member-tabs">
-        <view
-          class="member-tab"
-          :class="{ 'member-tab-active': selectedMemberId === m.id }"
-          v-for="m in members"
-          :key="m.id"
-          @click="selectMember(m.id)"
-        >
-          <text>{{ m.nickname }}</text>
-        </view>
-      </view>
+      <MemberSelector
+        variant="minimal"
+        embedded
+        :members="members"
+        :selected-member-id="selectedMemberId"
+        :show-self="false"
+        @select="onMemberSelect" />
     </view>
     </view>
 
 
     <view class="scale-list">
     <view class="scale-list">
@@ -107,6 +103,8 @@
 </template>
 </template>
 
 
 <script>
 <script>
+import MemberSelector from '@/components/MemberSelector.vue'
+import { normalizeMembers, memberIdOf, resolveMemberId } from '@/utils/memberNormalize.js'
 import { BrandColors } from '../../utils/brand-colors.js'
 import { BrandColors } from '../../utils/brand-colors.js'
 import { getCognitiveChildScores, submitCognitiveSelfTest } from '../../utils/api.js'
 import { getCognitiveChildScores, submitCognitiveSelfTest } from '../../utils/api.js'
 import { getFamilyMemberList } from '../../utils/api.js'
 import { getFamilyMemberList } from '../../utils/api.js'
@@ -138,6 +136,7 @@ import fontScaleMixin from '@/mixins/font-scale.js'
 
 
 export default {
 export default {
   mixins: [fontScaleMixin],
   mixins: [fontScaleMixin],
+  components: { MemberSelector },
   data() {
   data() {
     return {
     return {
       selectedMemberId: null,
       selectedMemberId: null,
@@ -173,9 +172,15 @@ export default {
       getFamilyMemberList({ visibleOnly: true })
       getFamilyMemberList({ visibleOnly: true })
         .then(function(res) {
         .then(function(res) {
           if (res && res.code === 200 && res.data) {
           if (res && res.code === 200 && res.data) {
-            self.members = res.data
-            if (!self.selectedMemberId && res.data.length > 0) {
-              self.selectedMemberId = res.data[0].id
+            // 前端防御层:按 user_id 折叠同用户多行,isSelf 唯一
+            self.members = normalizeMembers(res.data, { userId: uni.getStorageSync('userId') })
+            // 历史标识(users.id / 遗留 children.id)归一到统一身份键 memberId
+            var mid = resolveMemberId(self.members, self.selectedMemberId) || self.selectedMemberId
+            if (mid && String(mid) !== String(self.selectedMemberId)) {
+              self.selectedMemberId = mid
+              self.loadScores()
+            } else if (!self.selectedMemberId && self.members.length > 0) {
+              self.selectedMemberId = memberIdOf(self.members[0])
               self.loadScores()
               self.loadScores()
             }
             }
           }
           }
@@ -184,8 +189,9 @@ export default {
           // 静默失败
           // 静默失败
         })
         })
     },
     },
-    selectMember: function(id) {
-      this.selectedMemberId = id
+    // 统一组件选中回调:member 已由 memberNormalize 规范化,携带 canonical memberId
+    onMemberSelect: function(member) {
+      this.selectedMemberId = memberIdOf(member)
       this.loadScores()
       this.loadScores()
     },
     },
     loadScores: function() {
     loadScores: function() {
@@ -305,6 +311,7 @@ export default {
   padding: 24rpx 30rpx;
   padding: 24rpx 30rpx;
   background: $app-surface;
   background: $app-surface;
   margin: 20rpx 0;
   margin: 20rpx 0;
+.member-selector .member-minimal { margin-top: 16rpx; }
   border-radius: 16rpx;
   border-radius: 16rpx;
   margin-left: 30rpx;
   margin-left: 30rpx;
   margin-right: 30rpx;
   margin-right: 30rpx;
@@ -315,26 +322,6 @@ export default {
   color: $facade-text-body;
   color: $facade-text-body;
 }
 }
 
 
-.member-tabs {
-  display: flex;
-  flex-direction: row;
-  flex-wrap: wrap;
-  margin-top: 16rpx;
-}
-
-.member-tab {
-  padding: 10rpx 28rpx;
-  border-radius: 30rpx;
-  margin: 8rpx 8rpx 0 0;
-  background: $facade-hairline-2;
-  font-size: calc(26rpx * var(--font-scale, 1));
-  color: $facade-text-body;
-}
-
-.member-tab-active {
-  background: $dim-wisdom;
-  color: $app-surface;
-}
 
 
 .scale-list {
 .scale-list {
   padding: 0 30rpx;
   padding: 0 30rpx;

+ 35 - 0
cfc-frontend/utils/memberNormalize.js

@@ -197,6 +197,41 @@ export function normalizeMembers(rawMembers, viewer) {
   return result
   return result
 }
 }
 
 
+/**
+ * 把历史遗留的成员标识归一到统一身份键(familyMemberId)。
+ *
+ * 背景:部分页面从 URL 参数 / 本地缓存拿到的还是旧口径标识
+ *      (users.id 或遗留 children.id),直接当选择器主键会选不中任何一项。
+ * 命中规则:先按 canonical memberId 精确匹配 → 再按 id 匹配 → 最后按 userId 匹配。
+ *
+ * @param {Array}  members 成员列表(已规范化或原始均可)
+ * @param {String|Number} rawId 历史标识
+ * @returns {String|Number|null} 统一身份键;无法匹配时返回 null
+ */
+export function resolveMemberId(members, rawId) {
+  if (rawId === null || rawId === undefined || rawId === '') return null
+  var list = members instanceof Array ? members : []
+  var target = String(rawId)
+  var i
+  // 1) canonical memberId 精确匹配
+  for (i = 0; i < list.length; i++) {
+    if (String(memberIdOf(list[i])) === target) return memberIdOf(list[i])
+  }
+  // 2) 行 id 匹配
+  for (i = 0; i < list.length; i++) {
+    if (list[i] && list[i].id !== null && list[i].id !== undefined && String(list[i].id) === target) {
+      return memberIdOf(list[i])
+    }
+  }
+  // 3) userId 匹配(旧口径:把 users.id 当成员主键)
+  for (i = 0; i < list.length; i++) {
+    if (list[i] && hasLoginUser(list[i]) && String(list[i].userId) === target) {
+      return memberIdOf(list[i])
+    }
+  }
+  return null
+}
+
 /** 从(已规范化或未规范化的)成员列表中找出"本人"一行 */
 /** 从(已规范化或未规范化的)成员列表中找出"本人"一行 */
 export function findSelfMember(members) {
 export function findSelfMember(members) {
   if (!(members instanceof Array)) return null
   if (!(members instanceof Array)) return null