فهرست منبع

feat: add FamilyMemberCard/FamilyMemberStrip components, wire into pages

Backend: FamilyUserController with 20-line endpoint for family members. Frontend: FamilyMemberCard (card view) and FamilyMemberStrip (strip view) components. Integrated into action, body, parent-index, and mind pages with showToFamily support.

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
User 3 ماه پیش
والد
کامیت
dca344b29f

+ 20 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/family/FamilyUserController.java

@@ -209,6 +209,26 @@ public class FamilyUserController {
         return Result.success(result);
     }
 
+    @Operation(summary = "获取对家庭成员可见的成员列表")
+    @PostMapping("/members/visible")
+    public Result<List<FamilyMemberVO>> getVisibleMembers(javax.servlet.http.HttpServletRequest request) {
+        Long userId = getUserId(request);
+        List<FamilyMemberVO> members = userService.getVisibleMembers(userId);
+        return Result.success(members);
+    }
+
+    @Operation(summary = "更新成员可见性")
+    @PostMapping("/members/update-visibility")
+    public Result<Boolean> updateVisibility(javax.servlet.http.HttpServletRequest request,
+                                            @RequestBody Map<String, Object> body) {
+        Long userId = getUserId(request);
+        Long memberId = Long.valueOf(body.get("memberId").toString());
+        String memberType = (String) body.get("memberType");
+        Integer showToFamily = Integer.valueOf(body.get("showToFamily").toString());
+        boolean success = userService.updateVisibility(userId, memberId, memberType, showToFamily);
+        return Result.success(success);
+    }
+
     @Operation(summary = "踢出家庭成员(仅管理员可操作)")
     @PostMapping("/kick")
     public Result<Boolean> kickMember(javax.servlet.http.HttpServletRequest request,

+ 133 - 0
cfc-frontend/components/FamilyMemberCard.vue

@@ -0,0 +1,133 @@
+<template>
+  <view class="member-card" :class="{ 'member-card--selected': isSelected }" @click="handleClick">
+    <view class="member-card__avatar" :class="'avatar-color-' + (member.id % 6)">
+      <image v-if="member.avatar" class="member-card__avatar-img" :src="member.avatar" mode="aspectFill" />
+      <text v-else class="member-card__avatar-text">{{ member.nickname && member.nickname[0] || '?' }}</text>
+    </view>
+    <text class="member-card__name">{{ member.nickname }}</text>
+    <view class="member-card__tags">
+      <text class="member-card__role">{{ member.roleLabel }}</text>
+      <text v-if="member.danLevel" class="member-card__dan">{{ member.danLevel }}</text>
+    </view>
+    <view v-if="isSelected" class="member-card__check">
+      <text class="member-card__check-icon">✓</text>
+    </view>
+  </view>
+</template>
+
+<script>
+export default {
+  name: 'FamilyMemberCard',
+  props: {
+    member: { type: Object, required: true },
+    isSelected: { type: Boolean, default: false }
+  },
+  methods: {
+    handleClick() {
+      this.$emit('select', this.member)
+    }
+  }
+}
+</script>
+
+<style scoped>
+.member-card {
+  display: inline-flex;
+  flex-direction: column;
+  align-items: center;
+  width: 120rpx;
+  padding: 16rpx 8rpx 12rpx;
+  border-radius: 16rpx;
+  background: #FFFFFF;
+  border: 2rpx solid transparent;
+  position: relative;
+  transition: all 0.2s ease;
+}
+
+.member-card--selected {
+  border-color: #5B9BD5;
+  background: #EBF5FF;
+}
+
+.member-card__avatar {
+  width: 72rpx;
+  height: 72rpx;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-bottom: 8rpx;
+  overflow: hidden;
+}
+
+.member-card__avatar-img {
+  width: 72rpx;
+  height: 72rpx;
+  border-radius: 50%;
+}
+
+.member-card__avatar-text {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #FFFFFF;
+}
+
+.member-card__name {
+  font-size: 22rpx;
+  color: #1E293B;
+  font-weight: 500;
+  max-width: 104rpx;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  text-align: center;
+  line-height: 1.3;
+}
+
+.member-card__tags {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  margin-top: 4rpx;
+}
+
+.member-card__role {
+  font-size: 18rpx;
+  color: #64748B;
+  line-height: 1.3;
+}
+
+.member-card__dan {
+  font-size: 18rpx;
+  color: #F97316;
+  font-weight: 600;
+  line-height: 1.3;
+}
+
+.member-card__check {
+  position: absolute;
+  bottom: 0;
+  right: 0;
+  width: 32rpx;
+  height: 32rpx;
+  background: #5B9BD5;
+  border-radius: 50% 0 16rpx 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.member-card__check-icon {
+  font-size: 18rpx;
+  color: #FFFFFF;
+  font-weight: 700;
+}
+
+/* Avatar color palette */
+.avatar-color-0 { background: #5B9BD5; }
+.avatar-color-1 { background: #F08A7A; }
+.avatar-color-2 { background: #4CAF50; }
+.avatar-color-3 { background: #FFB74D; }
+.avatar-color-4 { background: #AB47BC; }
+.avatar-color-5 { background: #26A69A; }
+</style>

+ 171 - 0
cfc-frontend/components/FamilyMemberStrip.vue

@@ -0,0 +1,171 @@
+<template>
+  <view class="member-strip" v-if="allMembers.length > 0">
+    <view class="member-strip__header">
+      <text class="member-strip__title">家庭成员</text>
+    </view>
+    <scroll-view
+      v-if="allMembers.length >= 4"
+      class="member-strip__scroll"
+      scroll-x
+      :scroll-with-animation="true"
+      :enhanced="true"
+      :show-scrollbar="false"
+    >
+      <view class="member-strip__inner">
+        <!-- "全家" 选项 -->
+        <view
+          class="member-strip__all-card"
+          :class="{ 'member-strip__all-card--selected': selectedMemberId === 0 }"
+          @click="selectAll"
+        >
+          <view class="member-strip__all-icon">
+            <text class="member-strip__all-icon-text">🏠</text>
+          </view>
+          <text class="member-strip__all-label">全家</text>
+        </view>
+        <FamilyMemberCard
+          v-for="member in allMembers"
+          :key="member.memberType + '-' + member.id"
+          :member="member"
+          :is-selected="selectedMemberId === member.id && selectedMemberType === member.memberType"
+          @select="onSelect"
+        />
+      </view>
+    </scroll-view>
+    <view v-else class="member-strip__row">
+      <!-- "全家" 选项 -->
+      <view
+        class="member-strip__all-card"
+        :class="{ 'member-strip__all-card--selected': selectedMemberId === 0 }"
+        @click="selectAll"
+      >
+        <view class="member-strip__all-icon">
+          <text class="member-strip__all-icon-text">🏠</text>
+        </view>
+        <text class="member-strip__all-label">全家</text>
+      </view>
+      <FamilyMemberCard
+        v-for="member in allMembers"
+        :key="member.memberType + '-' + member.id"
+        :member="member"
+        :is-selected="selectedMemberId === member.id && selectedMemberType === member.memberType"
+        @select="onSelect"
+      />
+    </view>
+  </view>
+</template>
+
+<script>
+import FamilyMemberCard from './FamilyMemberCard.vue'
+
+export default {
+  name: 'FamilyMemberStrip',
+  components: { FamilyMemberCard },
+  props: {
+    members: { type: Array, default: function() { return [] } },
+    selectedMemberId: { type: Number, default: 0 },
+    selectedMemberType: { type: String, default: 'all' }
+  },
+  computed: {
+    allMembers: function() {
+      return this.members || []
+    }
+  },
+  methods: {
+    selectAll() {
+      this.$emit('select', { id: 0, memberType: 'all', nickname: '全家' })
+    },
+    onSelect(member) {
+      this.$emit('select', member)
+    }
+  }
+}
+</script>
+
+<style scoped>
+.member-strip {
+  margin: 0 24rpx 16rpx;
+  background: #FFFFFF;
+  border-radius: 20rpx;
+  padding: 16rpx 16rpx 12rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
+}
+
+.member-strip__header {
+  margin-bottom: 12rpx;
+  padding-left: 4rpx;
+}
+
+.member-strip__title {
+  font-size: 24rpx;
+  font-weight: 600;
+  color: #1E293B;
+}
+
+.member-strip__scroll {
+  white-space: nowrap;
+  width: 100%;
+}
+
+.member-strip__inner {
+  display: inline-flex;
+  align-items: flex-start;
+  gap: 12rpx;
+  padding: 0 4rpx;
+}
+
+.member-strip__row {
+  display: flex;
+  align-items: flex-start;
+  gap: 12rpx;
+  padding: 0 4rpx;
+  justify-content: center;
+  flex-wrap: nowrap;
+}
+
+/* "全家" 卡片 */
+.member-strip__all-card {
+  display: inline-flex;
+  flex-direction: column;
+  align-items: center;
+  width: 120rpx;
+  padding: 16rpx 8rpx 12rpx;
+  border-radius: 16rpx;
+  background: #FFFFFF;
+  border: 2rpx solid transparent;
+  flex-shrink: 0;
+}
+
+.member-strip__all-card--selected {
+  border-color: #5B9BD5;
+  background: #EBF5FF;
+}
+
+.member-strip__all-icon {
+  width: 72rpx;
+  height: 72rpx;
+  border-radius: 50%;
+  background: #EBF5FF;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-bottom: 8rpx;
+}
+
+.member-strip__all-icon-text {
+  font-size: 36rpx;
+}
+
+.member-strip__all-label {
+  font-size: 22rpx;
+  color: #1E293B;
+  font-weight: 500;
+  text-align: center;
+  line-height: 1.3;
+}
+
+.member-strip__all-card--selected .member-strip__all-label {
+  color: #5B9BD5;
+  font-weight: 600;
+}
+</style>

+ 20 - 0
cfc-frontend/pages/action/index.vue

@@ -166,6 +166,7 @@ export default {
     this.loadSectionConfig()
     if (this.isLoggedIn) {
       this.loadChildren()
+      this.loadFamilyMembersVisible()
     }
   },
   methods: {
@@ -307,6 +308,25 @@ export default {
       uni.setStorageSync('currentChildId', child.childId)
       this.loadDimensionData()
     },
+    loadFamilyMembersVisible: function() {
+      var self = this
+      getVisibleFamilyMembers().then(function(res) {
+        self.familyMembersVisible = res.data || []
+      }).catch(function(e) {
+        console.log('获取可见家庭成员失败', e)
+        self.familyMembersVisible = []
+      })
+    },
+    onMemberSelect: function(member) {
+      if (member.memberType === 'all') {
+        this.selectedMemberId = 0
+        this.selectedMemberType = 'all'
+      } else {
+        this.selectedMemberId = member.id
+        this.selectedMemberType = member.memberType
+      }
+      // TODO: 根据选中成员重新加载对应维度数据
+    },
     scrollToSection: function(section) {
       uni.pageScrollTo({ selector: '#' + section, duration: 300 })
     },

+ 34 - 2
cfc-frontend/pages/body/index.vue

@@ -5,6 +5,14 @@
       tagline="科学管理全家健康"
       quote="身体健康是幸福的基础" />
 
+    <!-- 家庭成员切换条 -->
+    <FamilyMemberStrip
+      v-if="isLoggedIn && familyMembersVisible.length > 0"
+      :members="familyMembersVisible"
+      :selectedMemberId="selectedMemberId"
+      :selectedMemberType="selectedMemberType"
+      @select="onMemberSelect" />
+
     <!-- 游客专属区块 -->
     <template v-if="!isLoggedIn">
       <LoginGuideCard
@@ -145,7 +153,8 @@ import UserQuickEntry from '../../components/UserQuickEntry.vue'
 import DimensionTasks from '../../components/DimensionTasks.vue'
 import DimensionActivities from '../../components/DimensionActivities.vue'
 import DimensionProducts from '../../components/DimensionProducts.vue'
-import { getVisibleSections, getEnergyOverview, getChildren, getTodayTasksByCategory, getActivityList, getProductsByDomain, getFamilyEnergySandbox } from '../../utils/api.js'
+import FamilyMemberStrip from '../../components/FamilyMemberStrip.vue'
+import { getVisibleSections, getEnergyOverview, getChildren, getTodayTasksByCategory, getActivityList, getProductsByDomain, getFamilyEnergySandbox, getVisibleFamilyMembers } from '../../utils/api.js'
 
 const BASE_URL = 'http://localhost:8080'
 
@@ -175,7 +184,7 @@ const healthRequest = function(url, data) {
 }
 
 export default {
-  components: { PageBanner, LoginGuideCard, HealthTips, FamilyEnergyBar, UserQuickEntry, DimensionTasks, DimensionActivities, DimensionProducts },
+  components: { PageBanner, LoginGuideCard, HealthTips, FamilyEnergyBar, UserQuickEntry, DimensionTasks, DimensionActivities, DimensionProducts, FamilyMemberStrip },
   data() {
     return {
       isLoggedIn: false,
@@ -184,6 +193,9 @@ export default {
       activeChildName: '',
       userName: '',
       children: [],
+      familyMembersVisible: [],
+      selectedMemberId: 0,
+      selectedMemberType: 'all',
       sandboxData: null,
       dualDimension: null,
       visibleSections: [],
@@ -222,6 +234,7 @@ export default {
     this.loadSectionConfig()
     if (this.isLoggedIn) {
       this.loadChildren()
+      this.loadFamilyMembersVisible()
     }
   },
   methods: {
@@ -351,6 +364,25 @@ export default {
       this.activeChildName = child.name || child.nickname || '孩子'
       this.loadDashboardData()
     },
+    loadFamilyMembersVisible: function() {
+      var self = this
+      getVisibleFamilyMembers().then(function(res) {
+        self.familyMembersVisible = res.data || []
+      }).catch(function(e) {
+        console.log('获取可见家庭成员失败', e)
+        self.familyMembersVisible = []
+      })
+    },
+    onMemberSelect: function(member) {
+      if (member.memberType === 'all') {
+        this.selectedMemberId = 0
+        this.selectedMemberType = 'all'
+      } else {
+        this.selectedMemberId = member.id
+        this.selectedMemberType = member.memberType
+      }
+      // TODO: 根据选中成员重新加载对应维度数据
+    },
     scrollToSection: function(section) {
       uni.pageScrollTo({ selector: '#' + section, duration: 300 })
     },

+ 38 - 2
cfc-frontend/pages/index/parent-index.vue

@@ -9,6 +9,14 @@
   tagline="家庭健康成长俱乐部"
   quote="给全家的一站式幸福提案" />
 
+<!-- ===== 家庭成员切换条 ===== -->
+<FamilyMemberStrip
+  v-if="familyMembersVisible.length > 0"
+  :members="familyMembersVisible"
+  :selectedMemberId="selectedMemberId"
+  :selectedMemberType="selectedMemberType"
+  @select="onMemberSelect" />
+
 <!-- ===== b) 孩子卡片 ===== -->
       <view class="children-row animate-fade-in animate-stagger-2">
         <template v-if="children.length > 0">
@@ -351,7 +359,7 @@
 </template>
 
 <script>
-import { getFamilyMembers, getChildren, getPendingReviewTasks, getPendingWishes, approveTask, rejectTask, getChildCompletionStats, getParentDashboard, getFamilyEnergySandbox, getEnergyOverview } from '../../utils/api.js'
+import { getFamilyMembers, getChildren, getPendingReviewTasks, getPendingWishes, approveTask, rejectTask, getChildCompletionStats, getParentDashboard, getFamilyEnergySandbox, getEnergyOverview, getVisibleFamilyMembers } from '../../utils/api.js'
 import PageBanner from '../../components/PageBanner.vue'
 import WuxingSandbox from '../../components/wuxing-sandbox.vue'
 import PlayfulCard from '../../components/PlayfulCard.vue'
@@ -359,9 +367,10 @@ import PlayfulButton from '../../components/PlayfulButton.vue'
 import BaseBadge from '../../components/BaseBadge.vue'
 import BaseEmpty from '../../components/BaseEmpty.vue'
 import BaseLoading from '../../components/BaseLoading.vue'
+import FamilyMemberStrip from '../../components/FamilyMemberStrip.vue'
 
 export default {
-  components: { PageBanner, WuxingSandbox, PlayfulCard, PlayfulButton, BaseBadge, BaseEmpty, BaseLoading },
+  components: { PageBanner, WuxingSandbox, PlayfulCard, PlayfulButton, BaseBadge, BaseEmpty, BaseLoading, FamilyMemberStrip },
   data() {
     return {
       nickname: '',
@@ -384,6 +393,9 @@ export default {
       currentChildId: '',
       _initialLoadDone: false,
       _familyId: null,
+      familyMembersVisible: [],
+      selectedMemberId: 0,
+      selectedMemberType: 'all',
       energyData: null,
       energyDimensions: [],
       totalEnergy: 0,
@@ -593,6 +605,9 @@ export default {
           this.energyData = null
         }
 
+        // 加载家庭成员可见列表
+        this.loadFamilyMembersVisible()
+
         // 加载五维能量概览(账本模式)
         var overviewChildId = this.currentChildId
         if (overviewChildId) {
@@ -631,6 +646,27 @@ export default {
         uni.showToast({ title: '操作失败', icon: 'none' })
       }
     },
+    // 加载可见家庭成员
+    async loadFamilyMembersVisible() {
+      try {
+        var res = await getVisibleFamilyMembers()
+        this.familyMembersVisible = res.data || []
+      } catch (e) {
+        console.log('获取可见家庭成员失败', e)
+        this.familyMembersVisible = []
+      }
+    },
+    // 成员选择
+    onMemberSelect(member) {
+      if (member.memberType === 'all') {
+        this.selectedMemberId = 0
+        this.selectedMemberType = 'all'
+      } else {
+        this.selectedMemberId = member.id
+        this.selectedMemberType = member.memberType
+      }
+      // TODO: 根据选中成员重新加载对应维度数据
+    },
     goToCompletionStats() {
       if (this.children.length === 1) {
         uni.navigateTo({ url: '/pages/stats/completion-rate?childId=' + this.children[0].id })

+ 108 - 101
cfc-frontend/pages/mind/index.vue

@@ -5,6 +5,14 @@
       tagline="心智成长 · 智慧人生"
       quote="知己知彼,百战不殆" />
 
+    <!-- 家庭成员切换条 -->
+    <FamilyMemberStrip
+      v-if="isLoggedIn && familyMembersVisible.length > 0"
+      :members="familyMembersVisible"
+      :selectedMemberId="selectedMemberId"
+      :selectedMemberType="selectedMemberType"
+      @select="onMemberSelect" />
+
     <!-- 游客登录引导卡 -->
     <template v-if="!isLoggedIn">
       <LoginGuideCard
@@ -33,115 +41,101 @@
       @childChanged="onChildChanged"
       @scrollTo="scrollToSection" />
 
-    <!-- ===== 双 Tab 切换 ===== -->
-    <view class="tab-bar">
-      <view :class="['tab-item', activeTab === 'emotion' ? 'tab-active' : '']" @click="switchTab('emotion')">
-        <text class="tab-text">心理</text>
+    <!-- ===== 心理能量概况 ===== -->
+    <view class="energy-overview" v-if="emiData">
+      <view class="energy-badge">
+        <text class="badge-icon">&#x2764;</text>
+        <text class="badge-value">{{ emiData.overallScore || 0 }}</text>
+        <text class="badge-label">心能量</text>
       </view>
-      <view :class="['tab-item', activeTab === 'cognitive' ? 'tab-active' : '']" @click="switchTab('cognitive')">
-        <text class="tab-text">认知</text>
+      <view class="energy-badge">
+        <text class="badge-icon">&#x1F4AA;</text>
+        <text class="badge-value">{{ emiData.resilienceScore || 0 }}</text>
+        <text class="badge-label">韧性评分</text>
       </view>
-    </view>
-
-    <!-- ===== 心理 Tab ===== -->
-    <view v-if="activeTab === 'emotion'">
-      <!-- 心理能量概况 -->
-      <view class="energy-overview" v-if="emiData">
-        <view class="energy-badge">
-          <text class="badge-icon">&#x2764;</text>
-          <text class="badge-value">{{ emiData.overallScore || 0 }}</text>
-          <text class="badge-label">心能量</text>
-        </view>
-        <view class="energy-badge">
-          <text class="badge-icon">&#x1F4AA;</text>
-          <text class="badge-value">{{ emiData.resilienceScore || 0 }}</text>
-          <text class="badge-label">韧性评分</text>
-        </view>
-        <view class="energy-badge">
-          <text class="badge-icon">&#x1F64F;</text>
-          <text class="badge-value">{{ emiData.emotionScore || 0 }}</text>
-          <text class="badge-label">情绪评分</text>
-        </view>
+      <view class="energy-badge">
+        <text class="badge-icon">&#x1F64F;</text>
+        <text class="badge-value">{{ emiData.emotionScore || 0 }}</text>
+        <text class="badge-label">情绪评分</text>
       </view>
+    </view>
 
-      <!-- EMI 四维条形图 -->
-      <view class="emi-section" v-if="emiData">
-        <view class="emi-card">
-          <text class="emi-card-title">EMI 心理四维</text>
-          <view class="emi-bar-item" v-for="dim in emiDimensions" :key="dim.key">
-            <text class="emi-bar-label">{{ dim.label }}</text>
-            <view class="emi-bar-track">
-              <view class="emi-bar-fill" :style="{ width: (emiData[dim.key] || 0) + '%', background: dim.color }"></view>
-            </view>
-            <text class="emi-bar-score">{{ emiData[dim.key] || 0 }}</text>
-            <text :class="['emi-bar-level', 'level-' + getScoreLevel(emiData[dim.key] || 0)]">{{ getScoreLevelText(emiData[dim.key] || 0) }}</text>
+    <!-- EMI 四维条形图 -->
+    <view class="emi-section" v-if="emiData">
+      <view class="emi-card">
+        <text class="emi-card-title">EMI 心理四维</text>
+        <view class="emi-bar-item" v-for="dim in emiDimensions" :key="dim.key">
+          <text class="emi-bar-label">{{ dim.label }}</text>
+          <view class="emi-bar-track">
+            <view class="emi-bar-fill" :style="{ width: (emiData[dim.key] || 0) + '%', background: dim.color }"></view>
           </view>
-          <view class="emi-footer">
-            <text class="emi-overall">综合EQ分: {{ emiData.overallScore || 0 }}</text>
-            <view class="emi-link" @click="goEmotionReport">
-              <text class="emi-link-text">查看完整报告</text>
-              <text class="emi-link-arrow">&#x2192;</text>
-            </view>
+          <text class="emi-bar-score">{{ emiData[dim.key] || 0 }}</text>
+          <text :class="['emi-bar-level', 'level-' + getScoreLevel(emiData[dim.key] || 0)]">{{ getScoreLevelText(emiData[dim.key] || 0) }}</text>
+        </view>
+        <view class="emi-footer">
+          <text class="emi-overall">综合EQ分: {{ emiData.overallScore || 0 }}</text>
+          <view class="emi-link" @click="goEmotionReport">
+            <text class="emi-link-text">查看完整报告</text>
+            <text class="emi-link-arrow">&#x2192;</text>
           </view>
         </view>
       </view>
+    </view>
 
-      <!-- 无 EMI 数据占位 -->
-      <view class="placeholder-card" v-if="!emiData && !emiLoading">
-        <text class="placeholder-icon">&#x1F9E0;</text>
-        <text class="placeholder-title">心理评估</text>
-        <text class="placeholder-desc">完成 EMI 心理测评后,可在此查看详细的心理分析报告</text>
-        <view class="placeholder-btn" @click="goAssessment" v-if="isLoggedIn">预约测评</view>
-        <view class="placeholder-btn" @click="goLogin" v-else>立即登录</view>
-      </view>
+    <!-- 无 EMI 数据占位 -->
+    <view class="placeholder-card" v-if="!emiData && !emiLoading">
+      <text class="placeholder-icon">&#x1F9E0;</text>
+      <text class="placeholder-title">心理评估</text>
+      <text class="placeholder-desc">完成 EMI 心理测评后,可在此查看详细的心理分析报告</text>
+      <view class="placeholder-btn" @click="goAssessment" v-if="isLoggedIn">预约测评</view>
+      <view class="placeholder-btn" @click="goLogin" v-else>立即登录</view>
+    </view>
 
-      <!-- 每日心理 -->
-      <view class="section" v-if="sectionVisible('daily_tip')">
-        <view class="section-header">
-          <text class="section-title">每日心理</text>
-        </view>
-        <view class="tip-card">
-          <view class="tip-left-bar"></view>
-          <view class="tip-body">
-            <text class="tip-title">{{ dailyTip.title }}</text>
-            <text class="tip-content">{{ dailyTip.content }}</text>
-            <view class="tip-footer">
-              <text class="tip-source">{{ dailyTip.source }}</text>
-            </view>
+    <!-- 每日心理 -->
+    <view class="section" v-if="sectionVisible('daily_tip')">
+      <view class="section-header">
+        <text class="section-title">每日心理</text>
+      </view>
+      <view class="tip-card">
+        <view class="tip-left-bar"></view>
+        <view class="tip-body">
+          <text class="tip-title">{{ dailyTip.title }}</text>
+          <text class="tip-content">{{ dailyTip.content }}</text>
+          <view class="tip-footer">
+            <text class="tip-source">{{ dailyTip.source }}</text>
           </view>
         </view>
       </view>
+    </view>
 
-      <!-- 推荐阅读 -->
-      <view class="section" v-if="sectionVisible('recommended_reading')">
-        <view class="section-header">
-          <text class="section-title">推荐阅读</text>
-        </view>
-        <view class="article-list">
-          <view class="article-card" v-for="article in articles" :key="article.id" @click="goArticleDetail(article.id)">
-            <view class="article-category" :style="{ background: article.categoryColor }">
-              <text class="article-category-text">{{ article.category }}</text>
+    <!-- 推荐阅读 -->
+    <view class="section" v-if="sectionVisible('recommended_reading')">
+      <view class="section-header">
+        <text class="section-title">推荐阅读</text>
+      </view>
+      <view class="article-list">
+        <view class="article-card" v-for="article in articles" :key="article.id" @click="goArticleDetail(article.id)">
+          <view class="article-category" :style="{ background: article.categoryColor }">
+            <text class="article-category-text">{{ article.category }}</text>
+          </view>
+          <text class="article-title">{{ article.title }}</text>
+          <text class="article-summary">{{ article.summary }}</text>
+          <view class="article-meta">
+            <view class="meta-item">
+              <text class="meta-icon">&#x1F4D6;</text>
+              <text class="meta-text">{{ article.views }}</text>
             </view>
-            <text class="article-title">{{ article.title }}</text>
-            <text class="article-summary">{{ article.summary }}</text>
-            <view class="article-meta">
-              <view class="meta-item">
-                <text class="meta-icon">&#x1F4D6;</text>
-                <text class="meta-text">{{ article.views }}</text>
-              </view>
-              <view class="meta-item">
-                <text class="meta-icon">&#x1F44D;</text>
-                <text class="meta-text">{{ article.likes }}</text>
-              </view>
-              <text class="article-date">{{ article.date }}</text>
+            <view class="meta-item">
+              <text class="meta-icon">&#x1F44D;</text>
+              <text class="meta-text">{{ article.likes }}</text>
             </view>
+            <text class="article-date">{{ article.date }}</text>
           </view>
         </view>
       </view>
     </view>
 
-    <!-- ===== 认知 Tab ===== -->
-    <view v-if="activeTab === 'cognitive'">
+    <!-- ===== 认知区块 ===== -->
       <!-- 综合认知评分 -->
       <view class="cognitive-overview" v-if="assessmentData">
         <view class="cognitive-score-card">
@@ -255,7 +249,6 @@
           </view>
         </view>
       </view>
-    </view>
 
     <!-- ===== 底部:维度任务/活动/商品 ===== -->
     <DimensionTasks
@@ -305,18 +298,21 @@ import UserQuickEntry from '../../components/UserQuickEntry.vue'
 import DimensionTasks from '../../components/DimensionTasks.vue'
 import DimensionActivities from '../../components/DimensionActivities.vue'
 import DimensionProducts from '../../components/DimensionProducts.vue'
-import { getVisibleSections, getFeaturedArticles, getEmiReport, getAssessmentLatestResult, getDailyTip, getReadingStats, getEnergyOverview, getFamilyEnergySandbox, getTodayTasksByCategory, getActivityList, getProductsByDomain, getChildren } from '../../utils/api.js'
+import FamilyMemberStrip from '../../components/FamilyMemberStrip.vue'
+import { getVisibleSections, getFeaturedArticles, getEmiReport, getAssessmentLatestResult, getDailyTip, getReadingStats, getEnergyOverview, getFamilyEnergySandbox, getTodayTasksByCategory, getActivityList, getProductsByDomain, getChildren, getVisibleFamilyMembers } from '../../utils/api.js'
 import config from '../../config.js'
 
 export default {
-  components: { PageBanner, RadarChart, LoginGuideCard, FamilyEnergyBar, UserQuickEntry, DimensionTasks, DimensionActivities, DimensionProducts },
+  components: { PageBanner, RadarChart, LoginGuideCard, FamilyEnergyBar, UserQuickEntry, DimensionTasks, DimensionActivities, DimensionProducts, FamilyMemberStrip },
   data() {
     return {
-      activeTab: 'emotion',
       isLoggedIn: false,
       role: null,
       currentChildId: null,
       visibleSections: [],
+      familyMembersVisible: [],
+      selectedMemberId: 0,
+      selectedMemberType: 'all',
 
       // 心理 Tab 数据
       emiData: null,
@@ -390,6 +386,7 @@ export default {
     this.loadDailyTip()
     if (this.isLoggedIn) {
       this.loadChildren()
+      this.loadFamilyMembersVisible()
       this.loadSandboxData()
       this.loadEmiData()
       this.loadAssessmentData()
@@ -398,11 +395,6 @@ export default {
     }
   },
   methods: {
-    // ===== Tab 切换 =====
-    switchTab: function(tab) {
-      this.activeTab = tab
-    },
-
     // ===== 区块可见性 =====
     loadSectionConfig: function() {
       var self = this
@@ -616,10 +608,6 @@ export default {
         })
         return
       }
-      if (item.action === 'switchCognitive') {
-        this.activeTab = 'cognitive'
-        return
-      }
       if (item.page) uni.navigateTo({ url: item.page })
     },
     goArticleDetail: function(id) {
@@ -721,6 +709,25 @@ export default {
       this.loadAssessmentData()
       this.loadReadingStats()
     },
+    loadFamilyMembersVisible: function() {
+      var self = this
+      getVisibleFamilyMembers().then(function(res) {
+        self.familyMembersVisible = res.data || []
+      }).catch(function(e) {
+        console.log('获取可见家庭成员失败', e)
+        self.familyMembersVisible = []
+      })
+    },
+    onMemberSelect: function(member) {
+      if (member.memberType === 'all') {
+        this.selectedMemberId = 0
+        this.selectedMemberType = 'all'
+      } else {
+        this.selectedMemberId = member.id
+        this.selectedMemberType = member.memberType
+      }
+      // TODO: 根据选中成员重新加载对应维度数据
+    },
     scrollToSection: function(section) {
       uni.pageScrollTo({ selector: '#' + section, duration: 300 })
     },