Jelajahi Sumber

feat: 行页面重构 - 能量沙盘+成员选择条+圈子卡片

- 能量沙盘移至品牌头部下方
- 家庭成员选择条添加能量值显示
- 添加圈子卡片(我的圈子+发现推荐)
- 删除章鱼图和导入联系人弹窗
- 成员卡片显示能量分数
liaoxg 1 bulan lalu
induk
melakukan
e38b06c370

+ 22 - 1
cfc-frontend/components/FamilyMemberCard.vue

@@ -7,6 +7,10 @@
       </view>
     </view>
     <text class="card-nickname">{{ member.nickname }}</text>
+    <view class="card-energy" v-if="energyScore !== null && energyScore !== undefined">
+      <text class="energy-value">{{ energyScore }}</text>
+      <text class="energy-unit">分</text>
+    </view>
     <view class="card-selected-mark" v-if="isSelected">
       <text class="mark-icon">✓</text>
     </view>
@@ -17,7 +21,8 @@
 export default {
   props: {
     member: { type: Object, required: true },
-    isSelected: { type: Boolean, default: false }
+    isSelected: { type: Boolean, default: false },
+    energyScore: { type: Number, default: null }
   },
   computed: {
     defaultAvatar: function() {
@@ -96,6 +101,22 @@ export default {
   text-overflow: ellipsis;
   white-space: nowrap;
 }
+.card-energy {
+  display: flex;
+  align-items: baseline;
+  justify-content: center;
+  margin-top: 4rpx;
+}
+.energy-value {
+  font-size: 20rpx;
+  color: #F97316;
+  font-weight: bold;
+}
+.energy-unit {
+  font-size: 16rpx;
+  color: #9CA3AF;
+  margin-left: 2rpx;
+}
 .card-selected-mark {
   position: absolute;
   top: 4rpx;

+ 8 - 1
cfc-frontend/components/FamilyMemberStrip.vue

@@ -14,6 +14,7 @@
           :key="member.id"
           :member="member"
           :isSelected="member.id === selectedMemberId"
+          :energyScore="getMemberEnergy(member)"
           @select="onSelect"
         />
       </view>
@@ -24,6 +25,7 @@
         :key="member.id"
         :member="member"
         :isSelected="member.id === selectedMemberId"
+        :energyScore="getMemberEnergy(member)"
         @select="onSelect"
       />
     </view>
@@ -37,11 +39,16 @@ export default {
   components: { FamilyMemberCard },
   props: {
     members: { type: Array, default: function() { return [] } },
-    selectedMemberId: { type: Number, default: null }
+    selectedMemberId: { type: Number, default: null },
+    energyMap: { type: Object, default: function() { return {} } }
   },
   methods: {
     onSelect: function(member) {
       this.$emit('select', member)
+    },
+    getMemberEnergy: function(member) {
+      if (!member || !this.energyMap) return null
+      return this.energyMap[member.id] || this.energyMap[member.memberId] || null
     }
   }
 }

+ 57 - 11
cfc-frontend/package-lock.json

@@ -34,6 +34,7 @@
         "copy-webpack-plugin": "^5.1.2",
         "jest": "^26.6.3",
         "node-sass": "^9.0.0",
+        "sass": "^1.77.8",
         "vue": "2.7.16",
         "vuex": "^3.6.2",
         "yalc": "^1.0.0-pre.53"
@@ -5004,7 +5005,6 @@
       "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
       "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
       "dev": true,
-      "optional": true,
       "engines": {
         "node": ">=8"
       },
@@ -5645,7 +5645,6 @@
       "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
       "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
       "dev": true,
-      "optional": true,
       "dependencies": {
         "anymatch": "~3.1.2",
         "braces": "~3.0.2",
@@ -5670,7 +5669,6 @@
       "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
       "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
       "dev": true,
-      "optional": true,
       "dependencies": {
         "is-glob": "^4.0.1"
       },
@@ -10401,6 +10399,12 @@
         "minimatch": "^3.0.4"
       }
     },
+    "node_modules/immutable": {
+      "version": "4.3.9",
+      "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.9.tgz",
+      "integrity": "sha512-ObHy4YN7ycwZOUCLI1/6svfyAFu7vL8RhAvVu/bh/RZW9EPlOyDaQ9jDQWCtdqzaXUjgXZCW1migtHE7YI7UGQ==",
+      "dev": true
+    },
     "node_modules/import-cwd": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz",
@@ -10845,7 +10849,6 @@
       "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
       "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
       "dev": true,
-      "optional": true,
       "dependencies": {
         "binary-extensions": "^2.0.0"
       },
@@ -15909,7 +15912,6 @@
       "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
       "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
       "dev": true,
-      "optional": true,
       "dependencies": {
         "picomatch": "^2.2.1"
       },
@@ -16898,6 +16900,23 @@
         "which": "bin/which"
       }
     },
+    "node_modules/sass": {
+      "version": "1.77.8",
+      "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.8.tgz",
+      "integrity": "sha512-4UHg6prsrycW20fqLGPShtEvo/WyHRVRHwOP4DzkUrObWoWI05QBSfzU71TVB7PFaL104TwNaHpjlWXAZbQiNQ==",
+      "dev": true,
+      "dependencies": {
+        "chokidar": ">=3.0.0 <4.0.0",
+        "immutable": "^4.0.0",
+        "source-map-js": ">=0.6.2 <2.0.0"
+      },
+      "bin": {
+        "sass": "sass.js"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
     "node_modules/sass-graph": {
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-4.0.1.tgz",
@@ -17765,6 +17784,15 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/source-map-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/source-map-resolve": {
       "version": "0.5.3",
       "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz",
@@ -25392,8 +25420,7 @@
       "version": "2.3.0",
       "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
       "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
-      "dev": true,
-      "optional": true
+      "dev": true
     },
     "bindings": {
       "version": "1.5.0",
@@ -25902,7 +25929,6 @@
       "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
       "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
       "dev": true,
-      "optional": true,
       "requires": {
         "anymatch": "~3.1.2",
         "braces": "~3.0.2",
@@ -25919,7 +25945,6 @@
           "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
           "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
           "dev": true,
-          "optional": true,
           "requires": {
             "is-glob": "^4.0.1"
           }
@@ -29508,6 +29533,12 @@
         "minimatch": "^3.0.4"
       }
     },
+    "immutable": {
+      "version": "4.3.9",
+      "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.9.tgz",
+      "integrity": "sha512-ObHy4YN7ycwZOUCLI1/6svfyAFu7vL8RhAvVu/bh/RZW9EPlOyDaQ9jDQWCtdqzaXUjgXZCW1migtHE7YI7UGQ==",
+      "dev": true
+    },
     "import-cwd": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz",
@@ -29832,7 +29863,6 @@
       "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
       "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
       "dev": true,
-      "optional": true,
       "requires": {
         "binary-extensions": "^2.0.0"
       }
@@ -33635,7 +33665,6 @@
       "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
       "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
       "dev": true,
-      "optional": true,
       "requires": {
         "picomatch": "^2.2.1"
       }
@@ -34361,6 +34390,17 @@
         }
       }
     },
+    "sass": {
+      "version": "1.77.8",
+      "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.8.tgz",
+      "integrity": "sha512-4UHg6prsrycW20fqLGPShtEvo/WyHRVRHwOP4DzkUrObWoWI05QBSfzU71TVB7PFaL104TwNaHpjlWXAZbQiNQ==",
+      "dev": true,
+      "requires": {
+        "chokidar": ">=3.0.0 <4.0.0",
+        "immutable": "^4.0.0",
+        "source-map-js": ">=0.6.2 <2.0.0"
+      }
+    },
     "sass-graph": {
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-4.0.1.tgz",
@@ -35032,6 +35072,12 @@
       "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
       "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
     },
+    "source-map-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+      "dev": true
+    },
     "source-map-resolve": {
       "version": "0.5.3",
       "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz",

+ 1 - 0
cfc-frontend/package.json

@@ -32,6 +32,7 @@
     "copy-webpack-plugin": "^5.1.2",
     "jest": "^26.6.3",
     "node-sass": "^9.0.0",
+    "sass": "^1.77.8",
     "vue": "2.7.16",
     "vuex": "^3.6.2",
     "yalc": "^1.0.0-pre.53"

+ 15 - 8
cfc-frontend/pages/action/index.vue

@@ -37,13 +37,7 @@
     <WuxingSandbox
       v-if="isLoggedIn && sandboxData"
       mode="live"
-      :scores="{
-        body: sandboxData.bodyScore || 0,
-        mind: sandboxData.mindScore || 0,
-        wisdom: sandboxData.wisdomScore || 0,
-        action: sandboxData.actionScore || 0,
-        wealth: sandboxData.wealthScore || 0
-      }"
+      :scores="sandboxScores"
       :overallScore="sandboxData.overallScore || 0"
       :members="sandboxData.members || []"
       showDimensionBar
@@ -123,7 +117,7 @@
         <!-- 圈子详情弹窗 -->
         <CircleDetail
           :visible="circleDetailVisible"
-          :circle="selectedCircle || {}"
+          :circle="detailCircle"
           :isMember="selectedCircleIsMember"
           @close="circleDetailVisible = false"
           @join="onCircleJoin"
@@ -198,6 +192,19 @@ export default {
     }
   },
   computed: {
+    sandboxScores: function() {
+      var d = this.sandboxData || {}
+      return {
+        body: d.bodyScore || 0,
+        mind: d.mindScore || 0,
+        wisdom: d.wisdomScore || 0,
+        action: d.actionScore || 0,
+        wealth: d.wealthScore || 0
+      }
+    },
+    detailCircle: function() {
+      return this.selectedCircle || {}
+    },
     energyMapForGraph: function() {
       var map = {}
       if (this.sandboxData && this.sandboxData.members) {

+ 1 - 1
cfc-frontend/pages/activity/activity-detail/activity-detail.vue

@@ -200,7 +200,7 @@
 
 <script>
 import { getActivityDetail, activityCheckin, registerActivity, cancelActivityRegistration, getMyActivityRegistrations, submitActivityFeedback, getActivityFeedbackList, getMyActivityFeedback } from '@/utils/api.js'
-import shareMixin from '../../components/share-mixin.js'
+import shareMixin from '../../../components/share-mixin.js'
 
 export default {
   mixins: [shareMixin],

+ 6 - 1
cfc-frontend/pages/discover/circles.vue

@@ -64,7 +64,7 @@
     <!-- 圈子详情弹窗 -->
     <CircleDetail
       :visible="detailVisible"
-      :circle="selectedCircle || {}"
+      :circle="detailCircle"
       :isMember="selectedIsMember"
       @close="detailVisible = false"
       @join="onJoin"
@@ -79,6 +79,11 @@ import { discoverCircles, getMyCircles, joinGeneralCircle, leaveGeneralCircle }
 
 export default {
   components: { CircleCard, CircleDetail },
+  computed: {
+    detailCircle: function() {
+      return this.selectedCircle || {}
+    }
+  },
   data: function() {
     return {
       loading: true,

+ 216 - 59
cfc-frontend/pages/index-home/index.vue

@@ -54,10 +54,21 @@
       tagline="知行合一 · 快乐成长"
       quote="千里之行,始于足下" />
 
+    <!-- 能量沙盘 — Canvas 五行星图 -->
+    <WuxingSandbox
+      v-if="sandboxData"
+      mode="live"
+      :scores="sandboxScores"
+      :overallScore="sandboxData.overallScore || 0"
+      :members="sandboxData.members || []"
+      showDimensionBar
+      starHeight="480rpx" />
+
     <!-- 家庭成员选择条 -->
     <FamilyMemberStrip
       :members="familyMembersVisible"
       :selectedMemberId="selectedMemberId"
+      :energyMap="energyMapForGraph"
       @select="onFamilyMemberSelect" />
 
     <!-- 五维能量条 -->
@@ -67,21 +78,16 @@
       :sandboxData="sandboxData"
       :dualDimension="dualDimension" />
 
-    <!-- 能量沙盘 — Canvas 五行星图 -->
-    <WuxingSandbox
-      v-if="sandboxData"
-      mode="live"
-      :scores="{
-        body: sandboxData.bodyScore || 0,
-        mind: sandboxData.mindScore || 0,
-        wisdom: sandboxData.wisdomScore || 0,
-        action: sandboxData.actionScore || 0,
-        wealth: sandboxData.wealthScore || 0
-      }"
-      :overallScore="sandboxData.overallScore || 0"
-      :members="sandboxData.members || []"
-      showDimensionBar
-      starHeight="480rpx" />
+    <!-- 家庭挑战任务 -->
+    <view class="section challenge-section" v-if="isLoggedIn && activeChallenges.length > 0">
+      <view class="section-header">
+        <text class="section-title">🔥 家庭挑战</text>
+      </view>
+      <FamilyChallengeCard
+        v-for="ch in activeChallenges"
+        :key="ch.id"
+        :challenge="ch" />
+    </view>
 
     <!-- 今日行动任务 -->
     <DimensionTasks
@@ -111,11 +117,45 @@
       @productClick="goProductDetail"
       @moreProducts="goMoreProducts" />
 
-    <!-- ===== 重要关系维护(章鱼图) ===== -->
-    <OctopusGraph
-      :contactList="contactList"
-      :healthAlerts="healthAlerts"
-      :isLoggedIn="true" />
+    <!-- ===== 我的圈子 ===== -->
+    <view class="circle-section" v-if="isLoggedIn">
+      <view class="circle-header">
+        <text class="circle-section-title">🤝 我的圈子</text>
+        <text class="circle-header-link" @click="goDiscoverCircles">发现更多 ›</text>
+      </view>
+      <scroll-view class="circle-scroll" scroll-x enable-flex v-if="myCircles.length > 0">
+        <view class="circle-scroll-inner">
+          <view class="circle-card-wrap" v-for="item in myCircles" :key="getCircleKey(item)" @click="onCircleClick(item)">
+            <CircleCard :circle="item" compact />
+          </view>
+        </view>
+      </scroll-view>
+      <view class="circle-empty" v-else>
+        <text class="circle-empty-text">暂未加入圈子,去发现看看</text>
+      </view>
+
+      <!-- ===== 发现推荐 ===== -->
+      <view class="circle-discover" v-if="discoverCirclesList.length > 0">
+        <view class="circle-header">
+          <text class="circle-section-title">🔍 发现推荐</text>
+          <text class="circle-header-link" @click="goDiscoverCircles">查看更多 ›</text>
+        </view>
+        <view class="discover-list">
+          <view class="discover-item" v-for="item in discoverCirclesList" :key="getCircleKey(item)" @click="onCircleClick(item)">
+            <CircleCard :circle="item" />
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <!-- 圈子详情弹窗 -->
+    <CircleDetail
+      :visible="circleDetailVisible"
+      :circle="detailCircle"
+      :isMember="selectedCircleIsMember"
+      @close="circleDetailVisible = false"
+      @join="onCircleJoin"
+      @leave="onCircleLeave" />
 
     <!-- 功能入口 -->
     <view class="func-section">
@@ -129,12 +169,6 @@
       </view>
     </view>
 
-    <!-- 导入联系人弹窗 -->
-    <ContactImport
-      :visible="showImportModal"
-      @close="onCloseImport"
-      @success="onImportSuccess" />
-
     <!-- 底部占位 -->
     <view class="bottom-spacer" style="height: 40rpx;"></view>
   </view>
@@ -316,10 +350,11 @@ import DimensionTasks from '../../components/DimensionTasks.vue'
 import DimensionActivities from '../../components/DimensionActivities.vue'
 import DimensionProducts from '../../components/DimensionProducts.vue'
 import DimensionArticles from '../../components/DimensionArticles.vue'
-import OctopusGraph from '../../components/OctopusGraph.vue'
-import ContactImport from '../../components/ContactImport.vue'
+import CircleCard from '../../components/CircleCard.vue'
+import CircleDetail from '../../components/CircleDetail.vue'
 import LoginGuideCard from '../../components/LoginGuideCard.vue'
-import { acceptParentInvite, productList, getActivityList, getFeaturedArticles, getFamilyEnergySandbox, getVisibleFamilyMembers, getTodayTasksByCategory, getChildren, getEnergyOverview, getProductsByDomain, getHealthAlerts, getContactList } from '../../utils/api.js'
+import FamilyChallengeCard from '../../components/FamilyChallengeCard.vue'
+import { acceptParentInvite, productList, getActivityList, getFeaturedArticles, getFamilyEnergySandbox, getVisibleFamilyMembers, getTodayTasksByCategory, getChildren, getEnergyOverview, getProductsByDomain, getChallengeList, getMyCircles, discoverCircles, joinGeneralCircle, leaveGeneralCircle } from '../../utils/api.js'
 
 var DIMENSIONS = [
   { code: 'body', name: '身', icon: '🌏', color: '#8D6E63', bgColor: 'rgba(141,110,99,0.15)', title: '主动健康', desc: '用最适合的方法,从根源调理家人体质,畅享高质量长寿生活' },
@@ -340,9 +375,10 @@ export default {
     DimensionActivities,
     DimensionProducts,
     DimensionArticles,
-    OctopusGraph,
-    ContactImport,
-    LoginGuideCard
+    CircleCard,
+    CircleDetail,
+    LoginGuideCard,
+    FamilyChallengeCard
   },
   data() {
     // 从 storage 预取登录态/角色,确保首次渲染即正确(WeChat 渲染层/逻辑层分离架构下 onLoad 执行前已渲染)
@@ -396,9 +432,12 @@ export default {
       articles: [],
       children: [],
       currentChildId: null,
-      contactList: [],
-      showImportModal: false,
-      healthAlerts: [],
+      activeChallenges: [],
+      myCircles: [],
+      discoverCirclesList: [],
+      circleDetailVisible: false,
+      detailCircle: null,
+      selectedCircleIsMember: false,
       funcList: [
         { icon: '\u{1F3CB}', label: '任务', needLogin: true, page: '/pages/tasks/tasks' },
         { icon: '\u{1F3C0}', label: '活动', needLogin: false, page: '/pages/discover/index?type=activity' },
@@ -409,6 +448,27 @@ export default {
     }
   },
   computed: {
+    sandboxScores: function() {
+      var d = this.sandboxData || {}
+      return {
+        body: d.bodyScore || 0,
+        mind: d.mindScore || 0,
+        wisdom: d.wisdomScore || 0,
+        action: d.actionScore || 0,
+        wealth: d.wealthScore || 0
+      }
+    },
+    energyMapForGraph: function() {
+      var map = {}
+      var members = this.sandboxData && this.sandboxData.members || []
+      for (var i = 0; i < members.length; i++) {
+        var m = members[i]
+        if (m.memberId && m.actionScore !== undefined) {
+          map[m.memberId] = m.actionScore
+        }
+      }
+      return map
+    },
     actionArticles: function() {
       if (!this.articles || this.articles.length === 0) return []
       var result = []
@@ -420,6 +480,12 @@ export default {
       }
       if (result.length === 0) return this.articles.slice(0, 5)
       return result.slice(0, 5)
+    },
+    dimList: function() {
+      return DIMENSIONS
+    },
+    energyDotColors: function() {
+      return ['#8D6E63', '#FF6B35', '#FFD700', '#4CAF50', '#42A5F5']
     }
   },
   onLoad(options) {
@@ -483,7 +549,6 @@ export default {
     // ===== 登录态数据加载 =====
     loadLoggedInData: function() {
       this.loadFamilyMembers()
-      this.loadContacts()
       this.loadChildren()
     },
     loadChildren: function() {
@@ -506,6 +571,9 @@ export default {
       this.loadActionActivities()
       this.loadActionProducts()
       this.loadActionArticles()
+      this.loadChallenges()
+      this.loadMyCircles()
+      this.loadDiscoverCircles()
     },
     loadEnergyData: function() {
       var self = this
@@ -590,31 +658,74 @@ export default {
         }
       }).catch(function() {})
     },
-    loadFamilyMembers: function() {
+    loadChallenges: function() {
+      var familyId = uni.getStorageSync('familyId')
+      if (!familyId) return
       var self = this
-      getVisibleFamilyMembers().then(function(res) {
+      getChallengeList({ familyId: familyId }).then(function(res) {
+        if (res.code === 200 && Array.isArray(res.data)) {
+          self.activeChallenges = res.data
+        }
+      }).catch(function() {
+        self.activeChallenges = []
+      })
+    },
+    loadMyCircles: function() {
+      var self = this
+      getMyCircles({}).then(function(res) {
         if (res.code === 200 && res.data) {
-          self.familyMembersVisible = Array.isArray(res.data) ? res.data : (res.data.list || [])
+          self.myCircles = Array.isArray(res.data) ? res.data : (res.data.list || [])
         }
-      }).catch(function() {})
+      }).catch(function() { self.myCircles = [] })
     },
-    loadContacts: function() {
+    loadDiscoverCircles: function() {
       var self = this
-      getContactList({}).then(function(res) {
+      discoverCircles({ page: 1, size: 3 }).then(function(res) {
         if (res.code === 200 && res.data) {
-          self.contactList = Array.isArray(res.data) ? res.data : []
+          self.discoverCirclesList = Array.isArray(res.data) ? res.data : (res.data.list || [])
+        }
+      }).catch(function() { self.discoverCirclesList = [] })
+    },
+    getCircleKey: function(item) {
+      return item.id || item.circleId || Math.random()
+    },
+    onCircleClick: function(item) {
+      this.detailCircle = item
+      this.selectedCircleIsMember = item.isMember || false
+      this.circleDetailVisible = true
+    },
+    onCircleJoin: function(circle) {
+      var self = this
+      joinGeneralCircle({ circleId: circle.id }).then(function(res) {
+        if (res.code === 200) {
+          self.circleDetailVisible = false
+          self.loadMyCircles()
+          self.loadDiscoverCircles()
+          uni.showToast({ title: '加入成功', icon: 'success' })
         }
       }).catch(function() {})
     },
-    onShowImport: function() {
-      this.showImportModal = true
+    onCircleLeave: function(circle) {
+      var self = this
+      leaveGeneralCircle({ circleId: circle.id }).then(function(res) {
+        if (res.code === 200) {
+          self.circleDetailVisible = false
+          self.loadMyCircles()
+          self.loadDiscoverCircles()
+          uni.showToast({ title: '已退出', icon: 'none' })
+        }
+      }).catch(function() {})
     },
-    onCloseImport: function() {
-      this.showImportModal = false
+    goDiscoverCircles: function() {
+      uni.navigateTo({ url: '/pages/discover/circles' })
     },
-    onImportSuccess: function() {
-      this.showImportModal = false
-      this.loadContacts()
+    loadFamilyMembers: function() {
+      var self = this
+      getVisibleFamilyMembers().then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.familyMembersVisible = Array.isArray(res.data) ? res.data : (res.data.list || [])
+        }
+      }).catch(function() {})
     },
     parseRelatedDimensions: function(val) {
       if (!val) return []
@@ -638,6 +749,10 @@ export default {
     goTasks: function() {
       uni.switchTab({ url: '/pages/tasks/tasks' })
     },
+    goMemberDetail: function(member) {
+      if (!member || !member.memberId) return
+      uni.navigateTo({ url: '/pages/child/child-detail?id=' + member.memberId })
+    },
     goActivityDetail: function(act) {
       if (!act || !act.id) return
       uni.navigateTo({ url: '/pages/discover-detail/activity-detail/activity-detail?id=' + act.id })
@@ -882,15 +997,6 @@ export default {
       }, 500)
     },
 
-  },
-  computed: {
-    dimList: function() {
-      return DIMENSIONS
-    },
-
-    energyDotColors: function() {
-      return ['#8D6E63', '#FF6B35', '#FFD700', '#4CAF50', '#42A5F5']
-    }
   }
 }
 </script>
@@ -1506,4 +1612,55 @@ export default {
   color: #333;
   font-weight: 500;
 }
+
+/* ===== 圈子卡片 ===== */
+.circle-section {
+  margin: 20rpx 30rpx;
+}
+.circle-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20rpx;
+}
+.circle-section-title {
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #333;
+}
+.circle-header-link {
+  font-size: 24rpx;
+  color: #5B9BD5;
+}
+.circle-scroll {
+  white-space: nowrap;
+}
+.circle-scroll-inner {
+  display: inline-flex;
+  gap: 16rpx;
+}
+.circle-card-wrap {
+  display: inline-block;
+}
+.circle-empty {
+  padding: 40rpx 0;
+  text-align: center;
+}
+.circle-empty-text {
+  font-size: 26rpx;
+  color: #999;
+}
+.circle-discover {
+  margin-top: 30rpx;
+}
+.discover-list {
+  display: flex;
+  flex-direction: column;
+  gap: 16rpx;
+}
+.discover-item {
+  background: #fff;
+  border-radius: 16rpx;
+  overflow: hidden;
+}
 </style>

TEMPAT SAMPAH
cfc-frontend/static/tab-action-active.png


TEMPAT SAMPAH
cfc-frontend/static/tab-action.png


TEMPAT SAMPAH
cfc-frontend/static/tab-body-active.png


TEMPAT SAMPAH
cfc-frontend/static/tab-body.png


TEMPAT SAMPAH
cfc-frontend/static/tab-mind-active.png


TEMPAT SAMPAH
cfc-frontend/static/tab-mind.png


TEMPAT SAMPAH
cfc-frontend/static/tab-wealth-active.png


TEMPAT SAMPAH
cfc-frontend/static/tab-wealth.png


TEMPAT SAMPAH
cfc-frontend/static/tab-wisdom-active.png


TEMPAT SAMPAH
cfc-frontend/static/tab-wisdom.png