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

feat(mp): family collect view for pearl & octopus diagrams

iwt 1 неделя назад
Родитель
Сommit
707e5b68d5

+ 84 - 10
cfc-frontend/pages/action-detail/components/OctopusDiagram.vue

@@ -1,6 +1,6 @@
 <template>
   <!-- 章鱼图 - 关键人拓展图谱 -->
-  <view class="octopus-wrapper" v-if="effects && effects.list && effects.list.length > 0" :style="{ '--font-scale': fontScale, height: canvasHeight + 'px' }">
+  <view class="octopus-wrapper" v-if="effects && effects.list && effects.list.length > 0" :style="{ '--font-scale': fontScale, minHeight: canvasHeight + 'px' }">
     <canvas
       class="octopus-canvas"
       canvas-id="octopusCanvas"
@@ -15,6 +15,7 @@
       <view class="popup-content">
         <view class="popup-header">
           <text class="popup-name">{{ selectedEffect.name }}</text>
+          <text class="popup-member" v-if="familyMode && selectedEffect.memberName">{{ selectedEffect.memberName }}</text>
           <text class="popup-tags">
             <text class="popup-tag" v-for="t in selectedEffect.effectTypeList" :key="t">{{ t }}</text>
           </text>
@@ -27,24 +28,31 @@
           </view>
         </view>
         <view class="popup-empty" v-else>
-          <text class="empty-text">暂无关键人,点击添加</text>
+          <text class="empty-text">{{ familyMode ? '暂无关键人' : '暂无关键人,点击添加' }}</text>
         </view>
-        <view class="popup-actions">
+        <view class="popup-actions" v-if="!familyMode">
           <text class="action-btn action-btn-primary" @tap="onAddKeyPerson(selectedEffect)">添加关键人</text>
         </view>
       </view>
     </view>
-    <!-- 重新分析按钮 -->
-    <view class="reanalyze-btn" @tap="$emit('reanalyze')">
+    <!-- 重新分析按钮(家庭汇集只读,隐藏个人操作) -->
+    <view class="reanalyze-btn" @tap="$emit('reanalyze')" v-if="!familyMode">
       <text>🔄 重新分析</text>
     </view>
+    <!-- 家庭汇集:成员图例 -->
+    <view class="octopus-member-legend" v-if="familyMode && memberLegend.length > 0" :style="{ marginTop: canvasHeight + 'px' }">
+      <view class="legend-item" v-for="m in memberLegend" :key="m.memberId">
+        <view class="legend-dot" :style="{ background: memberColorFor(m.colorKey) }"></view>
+        <text class="legend-label">{{ m.name }}</text>
+      </view>
+    </view>
   </view>
   <!-- 空状态 -->
   <view class="octopus-empty" v-else-if="effects && effects.list && effects.list.length === 0">
     <text class="empty-icon">🧭</text>
-    <text class="empty-text">暂无感恩记录</text>
-      <text class="empty-hint">记录你的感恩时刻,画出章鱼图</text>
-    <text class="action-btn action-btn-primary" @tap="$emit('add-effect')">+ 感恩记录</text>
+    <text class="empty-text">{{ familyMode ? '暂无家庭感恩记录' : '暂无感恩记录' }}</text>
+    <text class="empty-hint">{{ familyMode ? '家庭成员暂未记录成效' : '记录你的感恩时刻,画出章鱼图' }}</text>
+    <text class="action-btn action-btn-primary" @tap="$emit('add-effect')" v-if="!familyMode">+ 感恩记录</text>
   </view>
   <!-- 加载状态 -->
   <view class="octopus-loading" v-else>
@@ -54,6 +62,17 @@
 
 <script>
 import { BrandColors } from '../../../utils/brand-colors.js'
+// 家庭汇集成员色板(8 色循环,稳定):引用 BrandColors 令牌镜像(SCSS 侧等值令牌见 uni.scss),禁止硬编码 hex
+var FAMILY_MEMBER_COLORS = [
+  BrandColors.success,   // emerald($status-success / $dim-action)
+  BrandColors.info,      // sky($status-info)
+  BrandColors.wisdom,    // indigo($dim-wisdom)
+  BrandColors.wealth,    // amber($dim-wealth)
+  BrandColors.primary,   // orange($brand-primary)
+  BrandColors.mind,      // pink($dim-mind)
+  BrandColors.danger,    // red($status-danger)
+  BrandColors.violet     // violet-500($facade-violet)
+]
 /**
  * OctopusDiagram - 章鱼图(关键人拓展)
  *
@@ -72,7 +91,11 @@ export default {
     expansions: { type: Array, default: function() { return [] } },
     canvasWidth: { type: Number, default: 340 },
     canvasHeight: { type: Number, default: 420 },
-    interactive: { type: Boolean, default: true }
+    interactive: { type: Boolean, default: true },
+    /** 家庭汇集模式:节点按成员 colorKey 着色,隐藏个人操作(只读合并展示) */
+    familyMode: { type: Boolean, default: false },
+    /** 家庭汇集成员图例 [{ name, colorKey }] */
+    memberLegend: { type: Array, default: function() { return [] } }
   },
   data: function() {
     return {
@@ -174,7 +197,10 @@ export default {
           effectType: effects[i].effectType,
           effectTypeList: effects[i].effectTypeList || [],
           effectAmount: effects[i].effectAmount,
-          color: this._getTypeColor(effects[i].effectType)
+          // 家庭汇集:节点按成员着色,并携带成员标注
+          color: this.familyMode ? this._familyMemberColor(effects[i].colorKey || '') : this._getTypeColor(effects[i].effectType),
+          memberId: effects[i].memberId || null,
+          memberName: effects[i].memberName || ''
         })
       }
     },
@@ -533,6 +559,21 @@ export default {
       if (list.length === 0) return BrandColors.slate500
       return this.effectColors[list[0]] || BrandColors.slate500
     },
+
+    /** 家庭汇集成员色:colorKey 稳定哈希取模映射到 8 色板 */
+    memberColorFor: function(colorKey) {
+      if (!colorKey || typeof colorKey !== 'string') return FAMILY_MEMBER_COLORS[0]
+      var hash = 0
+      for (var i = 0; i < colorKey.length; i++) {
+        hash = (hash * 31 + colorKey.charCodeAt(i)) >>> 0
+      }
+      return FAMILY_MEMBER_COLORS[hash % FAMILY_MEMBER_COLORS.length]
+    },
+
+    /** 家庭汇集成员色(canvas 用,同 memberColorFor) */
+    _familyMemberColor: function(colorKey) {
+      return this.memberColorFor(colorKey)
+    },
     formatDate: function(dateStr) {
       if (!dateStr) return ''
       return String(dateStr).substring(0, 10)
@@ -558,6 +599,34 @@ export default {
   left: 0;
   z-index: 1;
 }
+
+/* 家庭汇集:成员图例(canvas 下方,按成员着色) */
+.octopus-member-legend {
+  position: relative;
+  z-index: 2;
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+  align-items: center;
+  gap: 8rpx 20rpx;
+  padding: 20rpx 8rpx 8rpx;
+}
+.legend-item {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  gap: 8rpx;
+}
+.legend-dot {
+  width: 20rpx;
+  height: 20rpx;
+  border-radius: 50%;
+  flex-shrink: 0;
+}
+.legend-label {
+  font-size: calc(22rpx * var(--font-scale, 1));
+  color: $app-text-subtle;
+}
 .octopus-empty, .octopus-loading {
   display: flex;
   flex-direction: column;
@@ -636,6 +705,11 @@ export default {
   font-weight: 600;
   color: $uni-text-color;
 }
+.popup-member {
+  font-size: calc(20rpx * var(--font-scale, 1));
+  color: $facade-text-slate;
+  margin-top: 2rpx;
+}
 .popup-tags {
   display: flex;
   flex-wrap: wrap;

+ 88 - 6
cfc-frontend/pages/action-detail/components/PearlDiagram.vue

@@ -17,6 +17,7 @@
           <image class="popup-avatar" :src="selectedNode.avatar || defaultAvatar" mode="aspectFill" />
           <view class="popup-info">
             <text class="popup-name">{{ selectedNode.name }}</text>
+            <text class="popup-member" v-if="familyMode && selectedNode.memberName">{{ selectedNode.memberName }}</text>
             <view class="popup-tags">
               <text class="popup-tag" :style="{ background: selectedNode.color, color: BrandColors.white }">{{ selectedNode.groupTypeName }}</text>
               <text class="popup-tag popup-tag-rel" v-if="selectedNode.relationshipType">{{ selectedNode.relationshipType }}</text>
@@ -34,8 +35,8 @@
         <view class="popup-desc" v-if="selectedNode.description">
           <text class="popup-desc-text">{{ selectedNode.description }}</text>
         </view>
-        <!-- 操作按钮 -->
-        <view class="popup-actions">
+        <!-- 操作按钮(家庭汇集视图为只读合并展示,隐藏个人操作) -->
+        <view class="popup-actions" v-if="!familyMode">
           <text class="action-btn action-btn-contact" v-if="selectedNode.groupType !== 'PERSON' && selectedNode.groupType !== 'SKILL'" @tap="onContact">联系</text>
           <text class="action-btn action-btn-priority" v-if="selectedNode.groupType !== 'PERSON' && selectedNode.groupType !== 'SKILL'" @tap="onAdjustPriority">优先级</text>
           <text class="action-btn action-btn-delete" v-if="canDelete" @tap="onDelete">删除</text>
@@ -43,13 +44,20 @@
         <view class="popup-arrow" :class="{ 'popup-arrow-down': showPopupBelow }"></view>
       </view>
     </view>
+    <!-- 家庭汇集:成员图例 -->
+    <view class="pearl-member-legend" v-if="familyMode && memberLegend.length > 0" :style="{ marginTop: canvasHeight + 'px' }">
+      <view class="legend-item" v-for="m in memberLegend" :key="m.memberId">
+        <view class="legend-dot" :style="{ background: memberColorFor(m.colorKey) }"></view>
+        <text class="legend-label">{{ m.name }}</text>
+      </view>
+    </view>
   </view>
   <!-- 空状态 -->
   <view class="pearl-graph-empty" v-else-if="isEmpty">
     <text class="empty-icon">📿</text>
-    <text class="empty-text">暂无珍珠图数据</text>
-    <text class="empty-hint">登记你的社会连接,画出珍珠项链图</text>
-    <text class="action-btn action-btn-primary" @tap="onAdd">+ 登记资源</text>
+    <text class="empty-text">{{ familyMode ? '暂无家庭珍珠图数据' : '暂无珍珠图数据' }}</text>
+    <text class="empty-hint">{{ familyMode ? '家庭成员暂未登记社会连接资源' : '登记你的社会连接,画出珍珠项链图' }}</text>
+    <text class="action-btn action-btn-primary" @tap="onAdd" v-if="!familyMode">+ 登记资源</text>
   </view>
   <!-- 加载状态 -->
   <view class="pearl-loading" v-else>
@@ -61,6 +69,18 @@
 import { addPearlInteraction, updatePearlPriority } from '@/utils/api.js'
 import { BrandColors, INDUSTRY_COLORS, danger, primary, success, white } from '@/utils/brand-colors.js'
 
+// 家庭汇集成员色板(8 色循环,稳定):引用 BrandColors 令牌镜像(SCSS 侧等值令牌见 uni.scss),禁止硬编码 hex
+var FAMILY_MEMBER_COLORS = [
+  BrandColors.success,   // emerald($status-success / $dim-action)
+  BrandColors.info,      // sky($status-info)
+  BrandColors.wisdom,    // indigo($dim-wisdom)
+  BrandColors.wealth,    // amber($dim-wealth)
+  BrandColors.primary,   // orange($brand-primary)
+  BrandColors.mind,      // pink($dim-mind)
+  BrandColors.danger,    // red($status-danger)
+  BrandColors.violet     // violet-500($facade-violet)
+]
+
 /**
  * PearlDiagram - 珍珠图 Canvas 组件
  *
@@ -108,6 +128,16 @@ export default {
     overdueMap: {
       type: Object,
       default: null
+    },
+    /** 家庭汇集模式:节点按成员 colorKey 着色,隐藏个人操作(只读合并展示) */
+    familyMode: {
+      type: Boolean,
+      default: false
+    },
+    /** 家庭汇集成员图例 [{ name, colorKey }] */
+    memberLegend: {
+      type: Array,
+      default: function() { return [] }
     }
   },
   data: function() {
@@ -182,6 +212,7 @@ export default {
     },
     /** 选中节点是否可删除(仅 INFO/PLACE 为手工登记资源;PERSON 来自联系人、SKILL 来自帮助记录聚合,均不可删) */
     canDelete: function() {
+      if (this.familyMode) return false
       if (!this.selectedNode) return false
       return this.selectedNode.groupType === 'INFO' || this.selectedNode.groupType === 'PLACE'
     },
@@ -356,7 +387,10 @@ export default {
               valueScore: item.valueScore || 0,
               daysSinceLast: item.daysSinceLast || null,
               radius: itemRadius,
-              color: config.color,
+              // 家庭汇集:节点按成员着色,并携带成员标注
+              color: self.familyMode ? self._familyMemberColor(item.colorKey || '') : config.color,
+              memberId: item.memberId || null,
+              memberName: item.memberName || '',
               ring: config.ring,
               x: cx + R * Math.cos(angle),
               y: cy + R * Math.sin(angle)
@@ -850,6 +884,21 @@ export default {
       var g = parseInt(hex.slice(3, 5), 16)
       var b = parseInt(hex.slice(5, 7), 16)
       return 'rgba(' + r + ',' + g + ',' + b + ',' + alpha + ')'
+    },
+
+    /** 家庭汇集成员色:colorKey 稳定哈希取模映射到 8 色板 */
+    memberColorFor: function(colorKey) {
+      if (!colorKey || typeof colorKey !== 'string') return FAMILY_MEMBER_COLORS[0]
+      var hash = 0
+      for (var i = 0; i < colorKey.length; i++) {
+        hash = (hash * 31 + colorKey.charCodeAt(i)) >>> 0
+      }
+      return FAMILY_MEMBER_COLORS[hash % FAMILY_MEMBER_COLORS.length]
+    },
+
+    /** 家庭汇集成员色(canvas 用,同 memberColorFor) */
+    _familyMemberColor: function(colorKey) {
+      return this.memberColorFor(colorKey)
     }
   }
 }
@@ -872,6 +921,34 @@ export default {
   z-index: 1;
 }
 
+/* 家庭汇集:成员图例(canvas 下方,按成员着色) */
+.pearl-member-legend {
+  position: relative;
+  z-index: 2;
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+  align-items: center;
+  gap: 8rpx 20rpx;
+  padding: 20rpx 8rpx 8rpx;
+}
+.legend-item {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  gap: 8rpx;
+}
+.legend-dot {
+  width: 20rpx;
+  height: 20rpx;
+  border-radius: 50%;
+  flex-shrink: 0;
+}
+.legend-label {
+  font-size: calc(22rpx * var(--font-scale, 1));
+  color: $app-text-subtle;
+}
+
 /* 详情弹窗 */
 .pearl-detail-popup {
   position: absolute;
@@ -913,6 +990,11 @@ export default {
   font-weight: 600;
   color: $uni-text-color;
 }
+.popup-member {
+  font-size: calc(20rpx * var(--font-scale, 1));
+  color: $facade-text-slate;
+  margin-top: 2rpx;
+}
 .popup-tags {
   display: flex;
   flex-wrap: wrap;

+ 135 - 10
cfc-frontend/pages/action-detail/index.vue

@@ -130,14 +130,16 @@
             <text class="section-sub">从成效出发,挖掘关键人,画出拓展路径</text>
           </view>
         </view>
-        <text class="section-link section-link-octopus" @tap="goOctopusEffectsList">+ 感恩记录</text>
+        <text class="section-link section-link-octopus" @tap="goOctopusEffectsList" v-if="currentView === 'self'">+ 感恩记录</text>
       </view>
       <OctopusDiagram
         ref="octopusDiagram"
         :selfId="selfId"
-        :effects="octopusEffects"
-        :expansions="octopusExpansions"
+        :effects="octopusDiagramData"
+        :expansions="currentView === 'family' ? [] : octopusExpansions"
         :interactive="true"
+        :family-mode="currentView === 'family'"
+        :member-legend="familyOctopusLegend"
         @effect-click="onEffectClick"
         @expansion-click="onExpansionClick"
         @add-effect="goOctopusAddEffect"
@@ -154,14 +156,16 @@
             <text class="section-sub">把可调用的资源串成珍珠项链——盘点我有什么</text>
           </view>
         </view>
-        <text class="section-link section-link-pearl" @tap="goPearlAddResource">+ 登记</text>
+        <text class="section-link section-link-pearl" @tap="goPearlAddResource" v-if="currentView === 'self'">+ 登记</text>
       </view>
       <PearlDiagram
         ref="pearlDiagram"
         :selfId="selfId"
-        :resources="pearlData"
-        :overdue-map="overdueMap"
+        :resources="pearlDiagramData"
+        :overdue-map="currentView === 'family' ? null : overdueMap"
         :interactive="true"
+        :family-mode="currentView === 'family'"
+        :member-legend="familyPearlLegend"
         @add="goPearlAddResource"
         @delete="onDeletePearlResource" />
     </view>
@@ -275,7 +279,7 @@ import FloatingAvatar from '../../components/AIFloatingAvatar.vue'
 import RadarChart from '../../components/RadarChart.vue'
 import DimensionSubDims from './components/DimensionSubDims.vue'
 import DimensionPageSkeleton from '../../components/DimensionPageSkeleton.vue'
-import { getVisibleSections, getEnergyOverview, getChildren, getFamilyEnergySandbox, getEnergySandbox, getFamilyMemberList, getOctopusTentacles, getPearlResources, deletePearlResource, getAbilityMap, getRadarData, getPearlReminders, getOctopusEffects, getOctopusAnalysisSummary, getOctopusExpansionSuggest, getOctopusEffectKeyPersons } from '../../utils/api.js'
+import { getVisibleSections, getEnergyOverview, getChildren, getFamilyEnergySandbox, getEnergySandbox, getFamilyMemberList, getOctopusTentacles, getPearlResources, deletePearlResource, getAbilityMap, getRadarData, getPearlReminders, getOctopusEffects, getOctopusAnalysisSummary, getOctopusExpansionSuggest, getOctopusEffectKeyPersons, getFamilyPearlResources, getFamilyOctopusEffects } from '../../utils/api.js'
 import fontScaleMixin from '@/mixins/font-scale.js'
 
 export default {
@@ -286,7 +290,7 @@ export default {
       gridColor: BrandColors.emerald100,
       labelColor: BrandColors.emerald800,
       successColor: BrandColors.success,
-    pageLoading: false,
+      pageLoading: false,
       isLoggedIn: false,
       role: null,
       showTabTransition: true,
@@ -298,7 +302,7 @@ export default {
       familyMembersVisible: [],
       childrenChecked: false,
       membersChecked: false,
-            actionSubDims: [],
+      actionSubDims: [],
       sandboxData: null,
       actionPopulationScoreMap: {},
       currentView: 'self', // 'self' | 'family'
@@ -311,6 +315,10 @@ export default {
       octopusExpansions: [],
       octopusKeyPersons: {},
       overdueMap: null,
+      // 家庭汇集数据
+      familyPearlData: null,
+      familyOctopusEffects: null,
+      familyLoading: false,
       // 帮助弹窗
       helpPopup: { visible: false, title: '', content: '' },
       funcList: [
@@ -529,6 +537,101 @@ export default {
         result.tip = '建议寻求专业家庭辅导,或从简单的家庭对话开始改善'
       }
       return result
+    },
+
+    /** 当前是否处于「全家」汇集视图 */
+    isFamilyView: function() {
+      return this.currentView === 'family'
+    },
+
+    /** 珍珠图数据源:family 视图取家庭汇集(members 合并展开),否则取个人数据 */
+    pearlDiagramData: function() {
+      if (this.currentView === 'family') {
+        return this.familyPearlDiagram
+      }
+      return this.pearlData
+    },
+
+    /** 章鱼图数据源:family 视图取家庭汇集(members 合并展开),否则取个人数据 */
+    octopusDiagramData: function() {
+      if (this.currentView === 'family') {
+        return this.familyOctopusDiagram
+      }
+      return this.octopusEffects
+    },
+
+    /** 家庭珍珠图:将 members[].items 合并展开为 PearlDiagram 可消费结构,每项带成员标注 */
+    familyPearlDiagram: function() {
+      if (!this.familyPearlData) return null
+      var members = this.familyPearlData.members
+      if (!members || !Array.isArray(members)) return null
+      var groupMap = {}
+      var groupOrder = []
+      for (var i = 0; i < members.length; i++) {
+        var m = members[i]
+        if (!m || !Array.isArray(m.items)) continue
+        for (var j = 0; j < m.items.length; j++) {
+          var item = m.items[j]
+          if (!item) continue
+          // 归属组:优先按连接类型/资源类型字段归组,缺失则归入 INFO(历史资源)
+          var type = item.connectionType || item.type || item.groupType || 'INFO'
+          var stamped = Object.assign({}, item, {
+            memberId: m.memberId,
+            memberName: m.memberName || '',
+            memberType: m.memberType || '',
+            colorKey: m.colorKey || ''
+          })
+          if (!groupMap[type]) {
+            groupMap[type] = { type: type, typeName: item.groupTypeName || item.typeName || type, items: [] }
+            groupOrder.push(type)
+          }
+          groupMap[type].items.push(stamped)
+        }
+      }
+      var groups = []
+      for (var k = 0; k < groupOrder.length; k++) {
+        groups.push(groupMap[groupOrder[k]])
+      }
+      return { groups: groups, legacyGroups: [] }
+    },
+
+    /** 家庭章鱼图:将 members[].effects 合并展开为 OctopusDiagram 可消费结构,每项带成员标注 */
+    familyOctopusDiagram: function() {
+      if (!this.familyOctopusEffects) return null
+      var members = this.familyOctopusEffects.members
+      if (!members || !Array.isArray(members)) return null
+      var list = []
+      for (var i = 0; i < members.length; i++) {
+        var m = members[i]
+        if (!m || !Array.isArray(m.effects)) continue
+        for (var j = 0; j < m.effects.length; j++) {
+          var effect = m.effects[j]
+          if (!effect) continue
+          list.push(Object.assign({}, effect, {
+            memberId: m.memberId,
+            memberName: m.memberName || '',
+            memberType: m.memberType || '',
+            colorKey: m.colorKey || ''
+          }))
+        }
+      }
+      return { list: list }
+    },
+
+    /** 珍珠图成员图例(家庭视图) */
+    familyPearlLegend: function() {
+      if (!this.familyPearlData || !this.familyPearlData.members) return []
+      return this.familyPearlData.members.map(function(m) {
+        return { memberId: m.memberId, name: m.memberName || '成员', colorKey: m.colorKey || '', memberType: m.memberType || '' }
+      })
+    },
+
+    /** 章鱼图成员图例(家庭视图) */
+    familyOctopusLegend: function() {
+      if (!this.familyOctopusEffects || !this.familyOctopusEffects.members) return []
+      return this.familyOctopusEffects.members.map(function(m) {
+        return { memberId: m.memberId, name: m.memberName || '成员', colorKey: m.colorKey || '', memberType: m.memberType || '' }
+      })
     }
   },
   created() {
@@ -650,18 +753,40 @@ export default {
     },
     switchView: function(view) {
       this.currentView = view
+      var self = this
       if (view === 'family') {
         this.selectedMemberId = null
-        var self = this
+        // 全家视图:同时加载家庭汇集珍珠图/章鱼图(只读合并展示)
+        this.loadFamilyCollectData()
         getEnergySandbox('family').then(function(res) {
           if (res && res.data && !res.data.locked) {
             self.sandboxData = res.data.familyData || res.data
           }
         }).catch(function(e) { console.log('获取家庭数据失败', e) })
       } else {
+        // 切回自己视图:恢复个人珍珠图/章鱼图数据
         this.loadSandboxData()
+        this.loadPearlResources()
+        this.loadOctopusData()
       }
     },
+    /** 加载家庭汇集珍珠图/章鱼图数据(Promise.all 并发,任一失败不白屏) */
+    loadFamilyCollectData: function() {
+      var self = this
+      this.familyLoading = true
+      Promise.all([getFamilyPearlResources(), getFamilyOctopusEffects()]).then(function(resArr) {
+        var pearlRes = resArr[0]
+        var octopusRes = resArr[1]
+        self.familyPearlData = (pearlRes && pearlRes.code === 200 && pearlRes.data) ? pearlRes.data : null
+        self.familyOctopusEffects = (octopusRes && octopusRes.code === 200 && octopusRes.data) ? octopusRes.data : null
+        self.familyLoading = false
+      }).catch(function(e) {
+        console.log('获取家庭汇集数据失败', e)
+        self.familyPearlData = null
+        self.familyOctopusEffects = null
+        self.familyLoading = false
+      })
+    },
     loadEnergyData: function() {
       var self = this
       getEnergyOverview(this.activeChildId).then(function(res) {

+ 6 - 0
cfc-frontend/utils/api.js

@@ -2532,6 +2532,9 @@ export const getPearlReminders = () => request('/api/pearl/reminder/list', 'POST
 export const updatePearlPriority = (data) => request('/api/pearl/item/update-priority', 'POST', data)
 export const getAbilityMap = () => request('/api/ability/map', 'POST')
 
+// 家庭汇集:珍珠图
+export const getFamilyPearlResources = () => request('/api/pearl/family/resources', 'POST')
+
 // ===== 章鱼图(关键人拓展) =====
 export const getOctopusEffects = (data) => request('/api/octopus/effect/list', 'POST', data)
 export const addOctopusEffect = (data) => request('/api/octopus/effect/add', 'POST', data)
@@ -2549,6 +2552,9 @@ export const getOctopusAnalysisKeywords = (data) => request('/api/octopus/analys
 export const getOctopusAnalysisSummary = (data) => request('/api/octopus/analysis/summary', 'POST', data)
 export const getOctopusExpansionSuggest = (data) => request('/api/octopus/expansion/suggest', 'POST', data)
 
+// 家庭汇集:章鱼图
+export const getFamilyOctopusEffects = () => request('/api/octopus/effect/family/list', 'POST')
+
 // ===== 椋熻氨鎺ㄨ崘 =====
 export const getMealRecommend = (params) => request('/api/meal/recommend', 'POST', params)
 export const replaceFood = (data) => request('/api/meal/replace', 'POST', data)