Răsfoiți Sursa

feat(energy): 能量沙盘支持我的/全家本地切换,后端一次返回个人+全家数据

- /api/energy/sandbox 接受可选 memberId,一次返回 personalData(参数指定成员)+familyData(全家)+familyUnlocked
- wuxing-sandbox 新增 personalData prop,个人模式优先取指定成员数据,切换视角零请求
- 首页/家长/孩子三个页面缓存两份数据,onSandboxViewChange 本地切换不再重新请求
- 成员移出家庭后恢复到其自建家庭(families.creator_id),family-members 文案改为删除成员
Sisyphus 2 săptămâni în urmă
părinte
comite
fc6fdc95da

+ 42 - 25
cfc-backend/src/main/java/com/etotem/cfc/controller/EnergyController.java

@@ -41,8 +41,11 @@ public class EnergyController {
      * 获取能量沙盘数据(个人/全家两层,受通关门禁控制)
      * view=personal: 当前登录用户个人五维(需通过 SELF_CHECK 关卡)
      * view=family:   家庭所有成员聚合五维(需通过 INVITE_FAMILY 关卡)
+     * memberId(可选): 参数指定成员(family_members.id),personalData 返回该成员个人五维;
+     *                 不传时返回当前登录用户。响应始终同时携带 personalData + familyData,
+     *                 前端切换个人/全家无需再次请求。
      */
-    @Operation(summary = "获取能量沙盘(支持个人/全家,含门禁)")
+    @Operation(summary = "获取能量沙盘(个人/全家一次返回,支持指定成员,含门禁)")
     @PostMapping("/sandbox")
     public Result<Map<String, Object>> getFamilyEnergySandbox(
             @RequestAttribute("userId") Long userId,
@@ -57,31 +60,16 @@ public class EnergyController {
         }
 
         String view = "personal";
-        if (body != null && body.get("view") != null) {
-            view = String.valueOf(body.get("view"));
+        Long targetMemberId = null;
+        if (body != null) {
+            if (body.get("view") != null) {
+                view = String.valueOf(body.get("view"));
+            }
+            targetMemberId = ParamUtils.getLong(body.get("memberId"));
         }
 
         Map<String, Object> result = new HashMap<>();
 
-        if ("family".equals(view)) {
-            // 全家沙盘:需通过 INVITE_FAMILY 关卡
-            UnlockGate inviteGate = unlockGateService.getGateByType("INVITE_FAMILY");
-            boolean unlocked = inviteGate == null
-                    || unlockGateService.isGateUnlocked(user.getFamilyId(), inviteGate);
-            if (!unlocked && inviteGate != null) {
-                result.put("locked", true);
-                result.put("view", "family");
-                result.put("gate", gateToMap(inviteGate));
-                return Result.success(result);
-            }
-            EnergySandboxDTO dto = energyService.calculateFamilyEnergy(user.getFamilyId());
-            result.put("locked", false);
-            result.put("view", "family");
-            result.put("familyData", dto);
-            return Result.success(result);
-        }
-
-        // 个人沙盘(默认):家庭级判据 —— 任一成员做过五维自检(书稿"测量先行")
         boolean familySelfCheckPassed = unlockGateService.hasFamilySelfCheck(user.getFamilyId());
         if (!familySelfCheckPassed) {
             result.put("locked", true);
@@ -90,13 +78,17 @@ public class EnergyController {
             return Result.success(result);
         }
 
-        EnergySandboxDTO dto = energyService.calculatePersonalEnergy(userId);
+        EnergySandboxDTO personalData = resolvePersonalData(userId, user.getFamilyId(), targetMemberId);
+        EnergySandboxDTO familyData = energyService.calculateFamilyEnergy(user.getFamilyId());
+
         UnlockGate inviteGate = unlockGateService.getGateByType("INVITE_FAMILY");
         boolean familyUnlocked = inviteGate == null
                 || unlockGateService.isGateUnlocked(user.getFamilyId(), inviteGate);
+
         result.put("locked", false);
-        result.put("view", "personal");
-        result.put("personalData", dto);
+        result.put("view", "family".equals(view) ? "family" : "personal");
+        result.put("personalData", personalData);
+        result.put("familyData", familyData);
         result.put("familyUnlocked", familyUnlocked);
         if (!familyUnlocked && inviteGate != null) {
             result.put("familyGate", gateToMap(inviteGate));
@@ -104,6 +96,31 @@ public class EnergyController {
         return Result.success(result);
     }
 
+    private EnergySandboxDTO resolvePersonalData(Long userId, Long familyId, Long targetMemberId) {
+        if (targetMemberId != null) {
+            EnergySandboxDTO family = energyService.calculateFamilyEnergy(familyId);
+            if (family != null && family.getMembers() != null) {
+                MemberEnergyDTO target = family.getMembers().stream()
+                        .filter(m -> targetMemberId.equals(m.getMemberId()))
+                        .findFirst().orElse(null);
+                if (target != null) {
+                    EnergySandboxDTO dto = new EnergySandboxDTO();
+                    List<MemberEnergyDTO> members = new ArrayList<>();
+                    members.add(target);
+                    dto.setMembers(members);
+                    dto.setBodyScore(target.getBodyScore());
+                    dto.setMindScore(target.getMindScore());
+                    dto.setWisdomScore(target.getWisdomScore());
+                    dto.setActionScore(target.getActionScore());
+                    dto.setWealthScore(target.getWealthScore());
+                    dto.setOverallScore(target.getOverallScore());
+                    return dto;
+                }
+            }
+        }
+        return energyService.calculatePersonalEnergy(userId);
+    }
+
     private Map<String, Object> gateToMap(UnlockGate gate) {
         if (gate == null) return null;
         Map<String, Object> m = new HashMap<>();

+ 17 - 2
cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberService.java

@@ -304,11 +304,12 @@ public class FamilyMemberService {
             familyRelationshipService.deleteRelationsForMember(memberId);
             familyMemberMapper.deleteById(memberId);
             logMemberAction(operator.getFamilyId(), memberId, "kick", member.getNickname(), null, userId);
-            // 如果关联了 User,清除 familyId
+            // 如果关联了 User:优先恢复到其自己创建的家庭否则清除 familyId
             if (member.getUserId() != null) {
                 User targetUser = userMapper.selectById(member.getUserId());
                 if (targetUser != null && operator.getFamilyId().equals(targetUser.getFamilyId())) {
-                    targetUser.setFamilyId(null);
+                    Long ownFamilyId = findOwnFamilyId(targetUser.getId());
+                    targetUser.setFamilyId(ownFamilyId);
                     targetUser.setUpdatedAt(new Date());
                     userMapper.updateById(targetUser);
                 }
@@ -324,6 +325,20 @@ public class FamilyMemberService {
         throw new RuntimeException("成员不存在");
     }
 
+    /**
+     * 查找用户自己创建的家庭ID(families.creator_id = userId),不存在返回 null
+     */
+    private Long findOwnFamilyId(Long userId) {
+        try {
+            Family own = familyMapper.selectOne(
+                    new LambdaQueryWrapper<Family>().eq(Family::getCreatorId, userId).last("LIMIT 1"));
+            return own != null ? own.getId() : null;
+        } catch (Exception e) {
+            log.warn("查找用户创建的家庭异常: userId={}", userId, e);
+            return null;
+        }
+    }
+
     /**
      * 判断成员是否可编辑
      * 规则:有关联 userId 或有 phone 且 phone 能匹配到 User 的成员不可编辑

+ 17 - 0
cfc-frontend/components/wuxing-sandbox.vue

@@ -127,6 +127,11 @@ export default {
       type: Array,
       default: function() { return [] }
     },
+    /** "我的"个人数据(参数指定成员的 EnergySandboxDTO,含 bodyScore/mindScore/.../overallScore) */
+    personalData: {
+      type: Object,
+      default: null
+    },
     /** 顶部徽章文字,空则自动计算 */
     badgeText: {
       type: String,
@@ -213,6 +218,16 @@ export default {
           s.wealth || 0
         ]
       }
+      var p = this.personalData
+      if (p) {
+        return [
+          p.bodyScore || 0,
+          p.mindScore || 0,
+          p.wisdomScore || 0,
+          p.actionScore || 0,
+          p.wealthScore || 0
+        ]
+      }
       var m = this.currentMember
       if (!m) return [0, 0, 0, 0, 0]
       return [
@@ -229,6 +244,8 @@ export default {
         return Math.round((s[0] + s[1] + s[2] + s[3] + s[4]) / 5)
       }
       if (this.viewMode === 'family') return this.overallScore || 0
+      var p = this.personalData
+      if (p && p.overallScore != null) return p.overallScore
       var m = this.currentMember
       return m ? (m.overallScore || 0) : 0
     },

+ 72 - 43
cfc-frontend/pages/energy/index.vue

@@ -5,9 +5,16 @@
       <view class="nav-back" @click="goBack">
         <text class="nav-back-icon">&#x2190;</text>
       </view>
-<view class="nav-right" @click="showMemberPicker = true">
-        <text class="nav-current-member">{{ currentMemberName }}</text>
-        <text class="nav-arrow">▼</text>
+      <!-- 我的 / 全家 切换 Tab -->
+      <view class="nav-tabs">
+        <view class="nav-tab" :class="{ active: currentView !== 'family' }" @click="switchView('self')">
+          <text class="nav-tab-icon">{{ currentMemberIcon }}</text>
+          <text class="nav-tab-label">{{ currentTabLabel }}</text>
+        </view>
+        <view class="nav-tab" :class="{ active: currentView === 'family' }" @click="switchView('family')">
+          <text class="nav-tab-icon">🏠</text>
+          <text class="nav-tab-label">全家</text>
+        </view>
       </view>
     </view>
 
@@ -27,7 +34,7 @@
     </template>
 
     <!-- 已登录 -->
-    <template v-else-if="!loading && sandboxData">
+    <template v-else-if="!loading && (familySandboxData || sandboxData)">
       <!-- ===== 能量总览卡 ===== -->
       <view class="overview-card">
         <view class="overview-header">
@@ -132,33 +139,6 @@
       <!-- 底部占位 -->
       <view class="bottom-spacer"></view>
     </template>
-
-    <!-- 成员选择弹窗 -->
-    <view class="modal-mask" v-if="showMemberPicker" @click="showMemberPicker = false">
-      <view class="member-picker" @click.stop>
-        <view class="picker-header">
-          <text class="picker-title">选择查看对象</text>
-          <text class="picker-close" @click="showMemberPicker = false">×</text>
-        </view>
-        <view class="picker-item" :class="{ active: currentView === 'family' }" @click="switchView('family')">
-          <text class="picker-icon">🏠</text>
-          <text class="picker-label">全家总览</text>
-          <text class="picker-desc">家庭整体能量状态</text>
-        </view>
-        <view
-          class="picker-item"
-          v-for="m in familyMembers"
-          :key="m.memberId"
-          :class="{ active: selectedMemberId === m.memberId }"
-          @click="selectMember(m); showMemberPicker = false">
-          <view class="picker-avatar" :style="{ background: memberColor(m) }">
-            <text class="picker-avatar-text">{{ m.name ? m.name.charAt(0) : '?' }}</text>
-          </view>
-          <text class="picker-label">{{ m.name || '成员' }}</text>
-          <text class="picker-desc">{{ m.familyRole || m.memberType || '' }}</text>
-        </view>
-      </view>
-    </view>
   </view>
 </template>
 
@@ -181,6 +161,7 @@ export default {
       loading: true,
       isLoggedIn: false,
       sandboxData: null,
+      familySandboxData: null,
       familyMembers: [],
       selectedMemberId: null,
       currentView: 'self', // self | family
@@ -191,7 +172,10 @@ export default {
     dimList() { return DIM_CONFIG },
     currentMember() {
       if (this.currentView === 'family') return null
-      return this.familyMembers.find(m => m.memberId === this.selectedMemberId) || null
+      if (this.familySandboxData && this.familySandboxData.members) {
+        return this.familySandboxData.members.find(function(m) { return m.memberId === this.selectedMemberId }.bind(this)) || null
+      }
+      return this.familyMembers.find(function(m) { return m.memberId === this.selectedMemberId }) || null
     },
     currentMemberName() {
       if (this.currentView === 'family') return '全家'
@@ -205,11 +189,14 @@ export default {
     },
     overallScore() {
       if (this.currentView === 'family') {
-        return this.sandboxData && this.sandboxData.overallScore
+        return this.familySandboxData && this.familySandboxData.overallScore
       }
       if (this.currentMember) {
         return this.currentMember.overallScore
       }
+      if (this.sandboxData) {
+        return this.sandboxData.overallScore
+      }
       return null
     },
     avatarChar() {
@@ -223,6 +210,16 @@ export default {
       return 'linear-gradient(135deg,#8B5CF6,#A78BFA)'
     },
     navTitle() { return this.currentMemberName + ' · 五维能量' },
+    currentMemberIcon() {
+      if (this.currentView === 'family') return '🏠'
+      if (this.currentMember) return (this.currentMember.name || '?').charAt(0)
+      return '👤'
+    },
+    currentTabLabel() {
+      if (this.currentView === 'family') return '全家'
+      if (!this.currentMember) return '我的'
+      return this.currentMember.name || '我的'
+    },
     navGradient() {
       if (this.currentView === 'family') return 'linear-gradient(135deg,#667eea,#764ba2)'
       if (this.currentMember) {
@@ -237,11 +234,14 @@ export default {
     },
     radarScores() {
       if (this.currentView === 'family') {
-        var d = this.sandboxData
-        return DIM_CONFIG.map(dim => d ? (d[dim.code + 'Score'] || 0) : 0)
+        var d = this.familySandboxData
+        return DIM_CONFIG.map(function(dim) { return d ? (d[dim.code + 'Score'] || 0) : 0 })
       }
       if (this.currentMember) {
-        return DIM_CONFIG.map(dim => this.currentMember[dim.code + 'Score'] || 0)
+        return DIM_CONFIG.map(function(dim) { return this.currentMember[dim.code + 'Score'] || 0 }.bind(this))
+      }
+      if (this.sandboxData) {
+        return DIM_CONFIG.map(function(dim) { return this.sandboxData[dim.code + 'Score'] || 0 }.bind(this))
       }
       return [0, 0, 0, 0, 0]
     }
@@ -276,7 +276,7 @@ export default {
     },
     getDimScore(code) {
       if (this.currentView === 'family') {
-        return this.sandboxData ? (this.sandboxData[code + 'Score'] || 0) : 0
+        return this.familySandboxData ? (this.familySandboxData[code + 'Score'] || 0) : 0
       }
       if (this.currentMember) {
         return this.currentMember[code + 'Score'] || 0
@@ -286,22 +286,39 @@ export default {
     async loadData() {
       this.loading = true
       try {
+        // 个人沙盘(当前登录用户五维)
         var res = await getFamilyEnergySandbox()
         if (res && res.code === 200 && res.data) {
           this.sandboxData = res.data
-          // 如果是新格式,提取 familyData
-          if (res.data.familyData) {
+          // 兼容新格式:解包 personalData
+          if (res.data.personalData) {
+            this.sandboxData = res.data.personalData
+          } else if (res.data.familyData) {
             this.sandboxData = res.data.familyData
           }
         }
-        // 加载家庭成员
+        // 家庭沙盘(全家聚合 + 各成员个人数据,作为成员分数来源)
+        var famRes = await getEnergySandbox('family')
+        if (famRes && famRes.code === 200 && famRes.data && famRes.data.familyData) {
+          this.familySandboxData = famRes.data.familyData
+        }
+        // 加载家庭成员(visibleOnly 列表,用于 isSelf/name 等展示属性)
         var memRes = await getFamilyMemberList({ visibleOnly: true })
         if (memRes && memRes.code === 200 && memRes.data) {
           this.familyMembers = memRes.data
-          // 默认选中第一个非自己(如果有)
-          if (this.familyMembers.length > 0 && !this.selectedMemberId) {
-            this.selectedMemberId = this.familyMembers[0].memberId
+        }
+        // 确定默认选中成员:优先全局切换的成员
+        var switchedId = uni.getStorageSync('currentMemberId') || uni.getStorageSync('currentChildId')
+        var pool = (this.familySandboxData && this.familySandboxData.members) || this.familyMembers
+        if (switchedId && pool.length > 0) {
+          var matched = pool.find(function(m) { return m.memberId === switchedId })
+          if (matched) {
+            this.selectedMemberId = switchedId
+          } else if (pool.length > 0) {
+            this.selectedMemberId = pool[0].memberId
           }
+        } else if (pool.length > 0 && !this.selectedMemberId) {
+          this.selectedMemberId = pool[0].memberId
         }
       } catch (e) {
         console.error('加载能量数据失败', e)
@@ -313,6 +330,18 @@ export default {
       this.currentView = view
       if (view === 'family') {
         this.selectedMemberId = null
+      } else if (!this.selectedMemberId) {
+        // 「我的」Tab:若尚未选中成员,绑定全局切换成员或第一个
+        var switchedId = uni.getStorageSync('currentMemberId') || uni.getStorageSync('currentChildId')
+        if (switchedId && this.familyMembers.length > 0) {
+          var matched = this.familyMembers.find(function(m) { return m.memberId === switchedId })
+          if (matched) {
+            this.selectedMemberId = switchedId
+          }
+        }
+        if (!this.selectedMemberId && this.familyMembers.length > 0) {
+          this.selectedMemberId = this.familyMembers[0].memberId
+        }
       }
       this.showMemberPicker = false
     },

+ 35 - 35
cfc-frontend/pages/home-pages/child-index.vue

@@ -37,9 +37,10 @@
 <WuxingSandbox
   v-else-if="sandboxData"
   mode="live"
-  :scores="sandboxScores"
-  :overallScore="sandboxData.overallScore || 0"
-  :members="sandboxData.members || []"
+  :scores="familySandboxScores"
+  :overallScore="familySandboxOverall"
+  :members="familySandboxMembers"
+  :personalData="sandboxPersonalData"
   :dimensions="energyDimensions"
   :totalEnergy="totalEnergy"
   :totalHealthIndex="totalHealthIndex"
@@ -387,6 +388,8 @@ export default {
       totalHealthIndex: 0,
       energyLogs: [],
       sandboxData: null,
+      sandboxPersonalData: null,
+      sandboxFamilyData: null,
       sandboxLocked: false,
       sandboxLockGate: null,
       currentSandboxView: 'personal',
@@ -410,6 +413,22 @@ export default {
         wealth: this.sandboxData.wealthScore || 0
       }
     },
+    familySandboxScores() {
+      var d = this.sandboxFamilyData || {}
+      return {
+        body: d.bodyScore || 0,
+        mind: d.mindScore || 0,
+        wisdom: d.wisdomScore || 0,
+        action: d.actionScore || 0,
+        wealth: d.wealthScore || 0
+      }
+    },
+    familySandboxOverall() {
+      return this.sandboxFamilyData ? (this.sandboxFamilyData.overallScore || 0) : 0
+    },
+    familySandboxMembers() {
+      return this.sandboxFamilyData ? (this.sandboxFamilyData.members || []) : []
+    },
     // ---------- 等级相关 ----------
     currentLevelIndex() {
       const pts = this.totalPoints
@@ -609,24 +628,25 @@ export default {
           console.log('获取通关解锁状态失败', e)
         }
 
-        // 加载沙盘数据(含通关门禁)
+        // 加载沙盘数据(含通关门禁,一次返回个人+全家
         try {
-          var sandboxRes = await getEnergySandbox(this.currentSandboxView)
+          var memberId = uni.getStorageSync('currentMemberId') || uni.getStorageSync('currentChildId') || null
+          var sandboxRes = await getEnergySandbox(this.currentSandboxView, memberId)
           if (sandboxRes && sandboxRes.data) {
             var ed = sandboxRes.data
             if (ed.locked) {
               this.sandboxLocked = true
               this.sandboxLockGate = ed.gate || null
               this.sandboxData = null
+              this.sandboxPersonalData = null
+              this.sandboxFamilyData = null
             } else {
               this.sandboxLocked = false
               this.sandboxLockGate = null
-              if (ed.view === 'personal') {
-                this.sandboxData = ed.personalData || null
-                this.familyUnlocked = ed.familyUnlocked || false
-              } else {
-                this.sandboxData = ed.familyData || null
-              }
+              this.sandboxPersonalData = ed.personalData || null
+              this.sandboxFamilyData = ed.familyData || null
+              this.sandboxData = this.currentSandboxView === 'family' ? (ed.familyData || null) : (ed.personalData || null)
+              this.familyUnlocked = ed.familyUnlocked || false
             }
           }
         } catch (e) {
@@ -678,31 +698,11 @@ export default {
         url: '/pages/energy/detail?memberId=' + this.memberId + '&code=' + code
       })
     },
-    /** 切换沙盘个人/全家视角 */
-    onSandboxViewChange: async function(view) {
+    /** 切换沙盘个人/全家视角(本地切换,不重新请求) */
+    onSandboxViewChange: function(view) {
+      if (view === this.currentSandboxView) return
       this.currentSandboxView = view
-      try {
-        var sandboxRes = await getEnergySandbox(this.currentSandboxView)
-        if (sandboxRes && sandboxRes.data) {
-          var ed = sandboxRes.data
-          if (ed.locked) {
-            this.sandboxLocked = true
-            this.sandboxLockGate = ed.gate || null
-            this.sandboxData = null
-          } else {
-            this.sandboxLocked = false
-            this.sandboxLockGate = null
-            if (ed.view === 'personal') {
-              this.sandboxData = ed.personalData || null
-              this.familyUnlocked = ed.familyUnlocked || false
-            } else {
-              this.sandboxData = ed.familyData || null
-            }
-          }
-        }
-      } catch (e) {
-        console.log('切换沙盘视角失败', e)
-      }
+      this.sandboxData = view === 'family' ? (this.sandboxFamilyData || null) : (this.sandboxPersonalData || null)
     },
     goToRewards() {
       this.$emit('navigate', { page: 'rewards' })

+ 46 - 25
cfc-frontend/pages/home-pages/parent-index.vue

@@ -141,9 +141,10 @@
       <WuxingSandbox
         v-else-if="sandboxData"
         mode="live"
-        :scores="sandboxScores"
-        :overallScore="sandboxData.overallScore || 0"
-        :members="sandboxData.members || []"
+        :scores="familySandboxScores"
+        :overallScore="familySandboxOverall"
+        :members="familySandboxMembers"
+        :personalData="sandboxPersonalData"
         :dimensions="energyDimensions"
         :totalEnergy="totalEnergy"
         :totalHealthIndex="totalHealthIndex"
@@ -532,6 +533,8 @@ export default {
       recommendedActivities: [],
       recommendedProducts: [],
       sandboxData: null,
+      sandboxPersonalData: null,
+      sandboxFamilyData: null,
       errorEnergy: false,
       // 通关解锁状态
       sandboxLocked: false,
@@ -612,6 +615,22 @@ export default {
         wealth: this.sandboxData.wealthScore || 0
       }
     },
+    familySandboxScores() {
+      var d = this.sandboxFamilyData || {}
+      return {
+        body: d.bodyScore || 0,
+        mind: d.mindScore || 0,
+        wisdom: d.wisdomScore || 0,
+        action: d.actionScore || 0,
+        wealth: d.wealthScore || 0
+      }
+    },
+    familySandboxOverall() {
+      return this.sandboxFamilyData ? (this.sandboxFamilyData.overallScore || 0) : 0
+    },
+    familySandboxMembers() {
+      return this.sandboxFamilyData ? (this.sandboxFamilyData.members || []) : []
+    },
     greetingText() {
       const hour = new Date().getHours()
       if (hour < 6) return '夜深了'
@@ -841,26 +860,26 @@ export default {
 
         // 加载能量沙盘(含通关门禁)
         try {
-          var energyRes = await getEnergySandbox(this.currentSandboxView)
+          var memberId = uni.getStorageSync('currentMemberId') || uni.getStorageSync('currentChildId') || null
+          var energyRes = await getEnergySandbox(this.currentSandboxView, memberId)
           if (energyRes && energyRes.data) {
             var ed = energyRes.data
             if (ed.locked) {
               this.sandboxLocked = true
               this.sandboxLockGate = ed.gate || null
               this.sandboxData = null
+              this.sandboxPersonalData = null
+              this.sandboxFamilyData = null
               this.energyData = null
             } else {
               this.sandboxLocked = false
               this.sandboxLockGate = null
-              if (ed.view === 'personal') {
-                this.sandboxData = ed.personalData || null
-                this.energyData = ed.personalData || null
-                this.familyUnlocked = ed.familyUnlocked || false
-                this.familyLockGate = ed.familyGate || null
-              } else {
-                this.sandboxData = ed.familyData || null
-                this.energyData = ed.familyData || null
-              }
+              this.sandboxPersonalData = ed.personalData || null
+              this.sandboxFamilyData = ed.familyData || null
+              this.sandboxData = this.currentSandboxView === 'family' ? (ed.familyData || null) : (ed.personalData || null)
+              this.energyData = ed.personalData || null
+              this.familyUnlocked = ed.familyUnlocked || false
+              this.familyLockGate = ed.familyGate || null
             }
           }
         } catch (e) {
@@ -948,31 +967,33 @@ export default {
     onDomainClick(evt) {
       this.goToDomain(evt.code)
     },
-    /** 切换沙盘个人/全家视角 */
-    onSandboxViewChange: async function(view) {
+    /** 切换沙盘个人/全家视角(本地切换,不重新请求) */
+    onSandboxViewChange: function(view) {
+      if (view === this.currentSandboxView) return
       this.currentSandboxView = view
-      this.loadSandboxData()
+      this.sandboxData = view === 'family' ? (this.sandboxFamilyData || null) : (this.sandboxPersonalData || null)
     },
-    /** 单独加载沙盘数据(用于视角切换) */
+    /** 单独加载沙盘数据(首次进入时使用,一次返回个人+全家) */
     loadSandboxData: async function() {
       try {
-        var energyRes = await getEnergySandbox(this.currentSandboxView)
+        var memberId = uni.getStorageSync('currentMemberId') || uni.getStorageSync('currentChildId') || null
+        var energyRes = await getEnergySandbox(this.currentSandboxView, memberId)
         if (energyRes && energyRes.data) {
           var ed = energyRes.data
           if (ed.locked) {
             this.sandboxLocked = true
             this.sandboxLockGate = ed.gate || null
             this.sandboxData = null
+            this.sandboxPersonalData = null
+            this.sandboxFamilyData = null
           } else {
             this.sandboxLocked = false
             this.sandboxLockGate = null
-            if (ed.view === 'personal') {
-              this.sandboxData = ed.personalData || null
-              this.familyUnlocked = ed.familyUnlocked || false
-              this.familyLockGate = ed.familyGate || null
-            } else {
-              this.sandboxData = ed.familyData || null
-            }
+            this.sandboxPersonalData = ed.personalData || null
+            this.sandboxFamilyData = ed.familyData || null
+            this.sandboxData = this.currentSandboxView === 'family' ? (ed.familyData || null) : (ed.personalData || null)
+            this.familyUnlocked = ed.familyUnlocked || false
+            this.familyLockGate = ed.familyGate || null
           }
         }
       } catch (e) {

+ 32 - 12
cfc-frontend/pages/index-home/index.vue

@@ -65,9 +65,10 @@
     <WuxingSandbox
       v-else-if="sandboxData"
       mode="live"
-      :scores="sandboxScores"
-      :overallScore="sandboxData.overallScore || 0"
-      :members="sandboxData.members || []"
+      :scores="familySandboxScores"
+      :overallScore="familySandboxOverall"
+      :members="familySandboxMembers"
+      :personalData="sandboxPersonalData"
       :dimensions="energyDimensions"
       :totalEnergy="totalEnergy"
       :totalHealthIndex="totalHealthIndex"
@@ -461,6 +462,8 @@ export default {
       membersChecked: false,
       selectedMemberId: null,
       sandboxData: null,
+      sandboxPersonalData: null,
+      sandboxFamilyData: null,
       sandboxLocked: false,
       sandboxLockGate: null,
       currentSandboxView: 'personal',
@@ -500,6 +503,22 @@ export default {
         wealth: d.wealthScore || 0
       }
     },
+    familySandboxScores: function() {
+      var d = this.sandboxFamilyData || {}
+      return {
+        body: d.bodyScore || 0,
+        mind: d.mindScore || 0,
+        wisdom: d.wisdomScore || 0,
+        action: d.actionScore || 0,
+        wealth: d.wealthScore || 0
+      }
+    },
+    familySandboxOverall: function() {
+      return this.sandboxFamilyData ? (this.sandboxFamilyData.overallScore || 0) : 0
+    },
+    familySandboxMembers: function() {
+      return this.sandboxFamilyData ? (this.sandboxFamilyData.members || []) : []
+    },
     hasPendingTasks: function() {
       return (this.pendingTasks && this.pendingTasks.length > 0)
     },
@@ -643,7 +662,8 @@ export default {
     },
     loadSandboxData: function() {
       var self = this
-      getEnergySandbox(this.currentSandboxView).then(function(res) {
+      var memberId = uni.getStorageSync('currentMemberId') || uni.getStorageSync('currentChildId') || null
+      getEnergySandbox(this.currentSandboxView, memberId).then(function(res) {
         if (res && res.data) {
           var d = res.data
           if (d.locked) {
@@ -651,26 +671,26 @@ export default {
             self.sandboxLocked = true
             self.sandboxLockGate = d.gate || null
             self.sandboxData = null
+            self.sandboxPersonalData = null
+            self.sandboxFamilyData = null
           } else {
             self.sandboxLocked = false
             self.sandboxLockGate = null
-            if (d.view === 'personal') {
-              self.sandboxData = d.personalData || null
-              self.familyUnlocked = d.familyUnlocked || false
-            } else {
-              self.sandboxData = d.familyData || null
-            }
+            self.sandboxPersonalData = d.personalData || null
+            self.sandboxFamilyData = d.familyData || null
+            self.sandboxData = self.currentSandboxView === 'family' ? (d.familyData || null) : (d.personalData || null)
+            self.familyUnlocked = d.familyUnlocked || false
           }
           self._splashReady = true
           self._tryHideSplash()
         }
       }).catch(function() {})
     },
-    /** 切换沙盘个人/全家视角 */
+    /** 切换沙盘个人/全家视角(本地切换,不重新请求) */
     onSandboxViewChange: function(view) {
       if (view === this.currentSandboxView) return
       this.currentSandboxView = view
-      this.loadSandboxData()
+      this.sandboxData = view === 'family' ? (this.sandboxFamilyData || null) : (this.sandboxPersonalData || null)
     },
     loadActionArticles: function() {
       var self = this

+ 3 - 3
cfc-frontend/pages/profile-extra/family-members.vue

@@ -101,7 +101,7 @@
           <button class="detail-action-btn action-edit" @click="onEditRelation(detailMember)" v-if="canEditMember(detailMember)">编辑资料</button>
           <button class="detail-action-btn action-questionnaire" @click="doQuestionnaire(detailMember)">关系问卷</button>
           <button class="detail-action-btn action-interaction" @click="recordInteraction(detailMember)">记录互动</button>
-          <button class="detail-action-btn action-kick" @click="confirmKick(detailMember)" v-if="isAdmin && detailMember.source === 'family_member'">移出家庭</button>
+          <button class="detail-action-btn action-kick" @click="confirmKick(detailMember)" v-if="isAdmin && detailMember.source === 'family_member'">删除成员</button>
         </view>
       </view>
     </view>
@@ -278,8 +278,8 @@ export default {
     confirmKick(member) {
       var self = this
       uni.showModal({
-        title: '除成员',
-        content: '确定要将 ' + (member.nickname || '该成员') + ' 移出家庭吗?',
+        title: '除成员',
+        content: '确定要删除 ' + (member.nickname || '该成员') + ' 吗?删除后成员将回到自己的家庭。',
         success: function(res) {
           if (res.confirm) {
             self.onKickMember(member)

+ 4 - 2
cfc-frontend/utils/api.js

@@ -1588,8 +1588,10 @@ export const getFamilyEnergySandbox = () => {
 }
 
 // 获取能量沙盘(支持 personal/family 双视角,含通关门禁,返回原始格式 {locked, view, personalData/familyData, familyUnlocked})
-export const getEnergySandbox = (view) => {
-  return request('/api/energy/sandbox', 'POST', { view: view || 'personal' })
+export const getEnergySandbox = (view, memberId) => {
+  const data = { view: view || 'personal' }
+  if (memberId) data.memberId = memberId
+  return request('/api/energy/sandbox', 'POST', data)
 }
 
 // 获取用户通关解锁全景(/api/unlock/status)