Преглед на файлове

feat(首页快捷): 应用库自定义快捷方式+使用频度追踪

- 首页快捷区改为动态渲染(锁定占位+频度填充,最多6个),新增应用库入口
- 新建 pages/home/app-library 应用库配置页(已在首页区置灰+可配置区切换+恢复默认)
- 16个应用入口页注入 reportAppEnter 埋点(任意入口进入计数)
- pages.json 注册 home 分包
iwt преди 1 ден
родител
ревизия
b76a4ff3fe

+ 11 - 0
cfc-frontend/pages.json

@@ -1799,6 +1799,17 @@
           }
         }
       ]
+    },
+    {
+      "root": "pages/home",
+      "pages": [
+        {
+          "path": "app-library",
+          "style": {
+            "navigationBarTitleText": "应用库配置"
+          }
+        }
+      ]
     }
   ],
   "globalStyle": {

+ 4 - 1
cfc-frontend/pages/action-detail/interaction-log.vue

@@ -125,7 +125,8 @@
 </template>
 
 <script>
-import { getFamilyMemberList, addInteraction } from '../../utils/api.js'
+import { getFamilyMemberList, addInteraction, reportAppEnter
+} from '../../utils/api.js'
 import config from '../../config.js'
 
 export default {
@@ -176,6 +177,8 @@ export default {
     }
   },
   onLoad(options) {
+    reportAppEnter('interaction');
+    
     if (options.memberId) {
       this.preselectedMemberId = Number(options.memberId)
       this.selectedMemberId = Number(options.memberId)

+ 3 - 1
cfc-frontend/pages/activity/index.vue

@@ -301,7 +301,7 @@
 </template>
 
 <script>
-import { getActivityList, getMyActivityRegistrations, activityCheckin, cancelActivityRegistration, adminListMyActivities, adminPublishActivity, submitActivityForReview } from '@/utils/api.js'
+import { getActivityList, getMyActivityRegistrations, activityCheckin, cancelActivityRegistration, adminListMyActivities, adminPublishActivity, submitActivityForReview, reportAppEnter} from '@/utils/api.js'
 import config from '@/config.js'
 import { parseDate } from '../../utils/format.js'
 
@@ -356,6 +356,8 @@ export default {
     }
   },
   onLoad: function(options) {
+      reportAppEnter('activity');
+    
     if (options && options.tab) {
       this.currentTab = options.tab
       this.loadData()

+ 4 - 1
cfc-frontend/pages/ai/chat.vue

@@ -161,7 +161,8 @@
 </template>
 
 <script>
-import { aiSendMessage, aiGetConversations, aiGetMessages, aiDeleteConversation, aiSendNutritionMessage, aiSendHealthCoachMessage, aiSendHealthButlerMessage, getMyMembership, getAllQuotaStatus } from '../../utils/api.js'
+import { aiSendMessage, aiGetConversations, aiGetMessages, aiDeleteConversation, aiSendNutritionMessage, aiSendHealthCoachMessage, aiSendHealthButlerMessage, getMyMembership, getAllQuotaStatus, reportAppEnter
+} from '../../utils/api.js'
 import { sttTranscribe } from '../../utils/api.js'
 import BaseEmpty from '../../components/BaseEmpty.vue'
 import TaskCard from '../../components/AITaskCard.vue'
@@ -234,6 +235,8 @@ export default {
     }
   },
   onLoad: function(options) {
+    reportAppEnter('ai');
+    
     this.mode = options.mode || ''
     this.fromPage = options.fromPage ? decodeURIComponent(options.fromPage) : ''
     this.reportId = options.reportId || ''

+ 4 - 1
cfc-frontend/pages/assessment/apply.vue

@@ -136,7 +136,8 @@
 </template>
 
 <script>
-import { getAvailableGuides, createAssessmentAppointment, getChildren, getAssessmentQuotas, createAssessmentAppointmentWithQuota } from '../../utils/api.js'
+import { getAvailableGuides, createAssessmentAppointment, getChildren, getAssessmentQuotas, createAssessmentAppointmentWithQuota, reportAppEnter
+} from '../../utils/api.js'
 
 export default {
   data() {
@@ -159,6 +160,8 @@ export default {
     }
   },
   onLoad(options) {
+    reportAppEnter('assess');
+    
     this.quotaId = options.quotaId || null
 
     if (this.quotaId) {

+ 3 - 1
cfc-frontend/pages/diet/index.vue

@@ -159,7 +159,7 @@
 </template>
 
 <script>
-import { getDietIngredients, refreshDietIngredients, addDietIngredient, removeDietIngredient, generateDietRecommendation, getMealConfig, searchDietFoods } from '@/utils/api.js'
+import { getDietIngredients, refreshDietIngredients, addDietIngredient, removeDietIngredient, generateDietRecommendation, getMealConfig, searchDietFoods, reportAppEnter} from '@/utils/api.js'
 import { parseDate } from '@/utils/format.js'
 
 export default {
@@ -201,6 +201,8 @@ export default {
     }
   },
   onLoad: function() {
+      reportAppEnter('diet');
+    
     this.familyId = uni.getStorageSync('familyId')
     this.familyMemberId = uni.getStorageSync('currentChildId') || uni.getStorageSync('currentMemberId') || uni.getStorageSync('userId')
     this.loadIngredients()

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

@@ -143,7 +143,8 @@
 <script>
 import CircleCard from '../../components/CircleCard.vue'
 import CircleDetail from '../../components/CircleDetail.vue'
-import { discoverCircles, getMyCircles, joinGeneralCircle, leaveGeneralCircle, getPendingCircleMembers, approveCircleMember, updateCircle } from '../../utils/api.js'
+import { discoverCircles, getMyCircles, joinGeneralCircle, leaveGeneralCircle, getPendingCircleMembers, approveCircleMember, updateCircle, reportAppEnter
+} from '../../utils/api.js'
 import { parseDate } from '@/utils/format.js'
 
 export default {
@@ -175,6 +176,8 @@ export default {
     }
   },
   onLoad: function() {
+    reportAppEnter('circle');
+    
     this.loadData()
   },
   methods: {

+ 4 - 1
cfc-frontend/pages/games/list.vue

@@ -31,7 +31,8 @@
 </template>
 
 <script>
-import { getMiniGameList } from '../../utils/api.js'
+import { getMiniGameList, reportAppEnter
+} from '../../utils/api.js'
 
 export default {
   data() {
@@ -40,6 +41,8 @@ export default {
     }
   },
   onLoad() {
+    reportAppEnter('game');
+    
     this.loadGames()
   },
   methods: {

+ 4 - 1
cfc-frontend/pages/growth-main/index.vue

@@ -250,7 +250,8 @@ import LoginGuideCard from '../../components/LoginGuideCard.vue'
 import DimensionActivities from '../../components/DimensionActivities.vue'
 import DimensionProducts from '../../components/DimensionProducts.vue'
 import ArticleBookshelf from '../../components/ArticleBookshelf.vue'
-import { healthCheckinList, getHealthMealList, getFoodCautionList, getGrowthRecommendations, getExerciseRecords, getSleepRecords, getEmotionCheckinList, getCheckinList, getTodayTasks } from '../../utils/api.js'
+import { healthCheckinList, getHealthMealList, getFoodCautionList, getGrowthRecommendations, getExerciseRecords, getSleepRecords, getEmotionCheckinList, getCheckinList, getTodayTasks, reportAppEnter
+} from '../../utils/api.js'
 import { parseDate } from '../../utils/format.js'
 export default {
   components: { LoginGuideCard, DimensionActivities, DimensionProducts, ArticleBookshelf },
@@ -269,6 +270,8 @@ export default {
     }
   },
   onShow() {
+    reportAppEnter('growth');
+    
     this.isLoggedIn = !!uni.getStorageSync('token')
     if (this.isLoggedIn) {
       this.memberId = uni.getStorageSync('currentChildId') || uni.getStorageSync('currentMemberId') || uni.getStorageSync('userId') || ''

+ 341 - 0
cfc-frontend/pages/home/app-library.vue

@@ -0,0 +1,341 @@
+<template>
+  <view class="app-library-page">
+    <view class="page-header">
+      <view class="header-left" @click="goBack">
+        <text class="header-icon">←</text>
+        <text class="header-text">应用库</text>
+      </view>
+      <text class="header-title">自定义首页快捷功能</text>
+    </view>
+
+    <!-- 已在首页有显著入口的功能 -->
+    <view class="section" v-if="fixedApps.length > 0">
+      <view class="section-title">已在首页显著位置</view>
+      <view class="fixed-list">
+        <view class="fixed-item" v-for="app in fixedApps" :key="app.key">
+          <view class="fixed-info">
+            <view class="fixed-icon">{{ app.icon }}</view>
+            <view class="fixed-detail">
+              <text class="fixed-name">{{ app.name }}</text>
+              <text class="fixed-desc">{{ app.description }}</text>
+            </view>
+          </view>
+          <view class="fixed-badge">已在首页</view>
+        </view>
+      </view>
+    </view>
+
+    <!-- 可配置的功能 -->
+    <view class="section" v-if="configurableApps.length > 0">
+      <view class="section-title">可配置的功能</view>
+      <view class="configurable-list">
+        <view class="configurable-item" v-for="app in configurableApps" :key="app.key" @click="toggleApp(app.key)">
+          <view class="configurable-icon">{{ app.icon }}</view>
+          <view class="configurable-detail">
+            <text class="configurable-name">{{ app.name }}</text>
+            <view v-if="isAppInQuickApps(app.key)" class="status-badge">已在快捷区</view>
+            <view v-else class="status-badge">不在快捷区</view>
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <!-- 暂无配置 -->
+    <view class="empty-state" v-if="fixedApps.length === 0 && configurableApps.length === 0">
+      <text class="empty-icon">📦</text>
+      <text class="empty-title">暂无应用</text>
+      <text class="empty-desc">所有功能均在首页显著位置,无需额外配置</text>
+    </view>
+
+    <!-- 底部操作栏 -->
+    <view class="bottom-bar">
+      <view class="bottom-action">
+        <text class="action-text">快捷区当前排序:</text>
+        <view class="sort-info">
+          <view v-for="(app, idx) in quickApps" :key="app.key" class="sort-chip">
+            <text class="chip-icon">{{ app.icon }}</text>
+            <text class="chip-label">{{ app.name }}</text>
+            <text v-if="app.locked" class="chip-lock">🔒</text>
+          </view>
+        </view>
+      </view>
+      <view class="bottom-btn" @click="resetToDefault">恢复默认</view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { APP_LIBRARY, getShortcutableApps, findAppByKey } from '@/utils/app-library'
+import { getAppLibrary, saveShortcutConfig, resetShortcutConfig } from '@/utils/api.js'
+
+export default {
+  data() {
+    return {
+      fixedApps: [],
+      configurableApps: [],
+      quickApps: [],
+      myShortcuts: [],
+      appUsage: {}
+    }
+  },
+  onShow() {
+    this.loadAppLibrary()
+    this.loadQuickApps()
+  },
+  methods: {
+    goBack() {
+      uni.navigateBack()
+    },
+    loadAppLibrary() {
+      const allApps = APP_LIBRARY
+      this.fixedApps = allApps.filter(app => app.canShortcut === false).slice(0, 20)
+      this.configurableApps = allApps.filter(app => app.canShortcut === true).slice(0, 20)
+    },
+    loadQuickApps() {
+      var self = this
+      getAppLibrary().then(function(res) {
+        if (res && res.code === 200 && res.data) {
+          self.myShortcuts = res.data.myShortcuts || []
+          self.appUsage = res.data.usage || {}
+          // 计算 quickApps:锁定占位 + 频度填充
+          var lockedApps = (self.myShortcuts || []).filter(function(s) { return s.locked === 1 })
+            .sort(function(a, b) { return a.sortOrder - b.sortOrder })
+          if (lockedApps.length >= 6) {
+            self.quickApps = lockedApps.slice(0, 6).map(function(s) {
+              var app = findAppByKey(s.appKey)
+              return app ? { key: app.key, name: app.name, icon: app.icon, route: app.route, locked: s.locked } : null
+            }).filter(function(x) { return x })
+            return
+          }
+          var remaining = 6 - lockedApps.length
+          var lockedKeys = {}
+          lockedApps.forEach(function(s) { lockedKeys[s.appKey] = true })
+          var candidates = getShortcutableApps().filter(function(app) { return !lockedKeys[app.key] })
+          candidates.sort(function(a, b) {
+            var ca = (self.appUsage && self.appUsage[a.key]) || 0
+            var cb = (self.appUsage && self.appUsage[b.key]) || 0
+            return cb - ca
+          })
+          var shown = lockedApps.concat(candidates.slice(0, remaining))
+          self.quickApps = shown.map(function(item) {
+            var app = findAppByKey(item.appKey)
+            return app ? { key: app.key, name: app.name, icon: app.icon, route: app.route, locked: item.locked } : null
+          }).filter(function(x) { return x })
+        }
+      }).catch(function() {
+        self.loadAppLibrary()
+      })
+    },
+    isAppInQuickApps: function(appKey) {
+      return this.quickApps.some(function(app) { return app.key === appKey })
+    },
+    toggleApp: function(appKey) {
+      var isInQuick = this.isAppInQuickApps(appKey)
+      var items = []
+      // 保留其他已配置项,更新当前项
+      var existingItems = this.myShortcuts.map(function(s) { return { appKey: s.appKey, locked: s.locked, sortOrder: s.sortOrder } })
+      var idx = existingItems.findIndex(function(s) { return s.appKey === appKey })
+      if (idx >= 0) {
+        existingItems[idx].locked = !isInQuick
+      } else {
+        existingItems.push({ appKey: appKey, locked: !isInQuick, sortOrder: existingItems.length })
+      }
+      saveShortcutConfig(existingItems).then(function(res) {
+        if (res && res.code === 200) {
+          this.loadAppLibrary()
+          this.loadQuickApps()
+          uni.showToast({ title: isInQuick ? '已移出快捷区' : '已添加至快捷区', icon: 'none' })
+        }
+      }.bind(this)).catch(function() {
+        uni.showToast({ title: '操作失败', icon: 'none' })
+      })
+    },
+    resetToDefault() {
+      resetShortcutConfig().then(function(res) {
+        if (res && res.code === 200) {
+          this.loadAppLibrary()
+          this.loadQuickApps()
+          uni.showToast({ title: '已恢复默认配置', icon: 'success' })
+        }
+      }.bind(this)).catch(function() {
+        uni.showToast({ title: '恢复失败', icon: 'none' })
+      })
+    }
+  }
+}
+</script>
+
+<style>
+.app-library-page {
+  padding: 20rpx;
+  background: #F8F8F8;
+  min-height: 100vh;
+  box-sizing: border-box;
+}
+.page-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 30rpx;
+  padding-bottom: 20rpx;
+  border-bottom: 1rpx solid #E8E8E8;
+}
+.header-left {
+  display: flex;
+  align-items: center;
+  gap: 8rpx;
+  color: #333;
+  font-size: 30rpx;
+}
+.header-icon {
+  font-size: 32rpx;
+}
+.header-title {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #333;
+}
+.section {
+  margin-bottom: 30rpx;
+}
+.section-title {
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #333;
+  margin-bottom: 16rpx;
+  padding-left: 8rpx;
+}
+.fixed-list,
+.configurable-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 16rpx;
+}
+.fixed-item,
+.configurable-item {
+  width: calc(50% - 8rpx);
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 20rpx;
+  box-sizing: border-box;
+  position: relative;
+}
+.fixed-info,
+.configurable-detail {
+  display: flex;
+  flex-direction: column;
+  gap: 8rpx;
+}
+.fixed-icon,
+.configurable-icon {
+  width: 48rpx;
+  height: 48rpx;
+  background: #F5FAFE;
+  border-radius: 12rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 28rpx;
+}
+.fixed-name,
+.configurable-name {
+  font-size: 26rpx;
+  font-weight: bold;
+  color: #333;
+}
+.fixed-desc,
+.configurable-desc {
+  font-size: 20rpx;
+  color: #999;
+  line-height: 1.4;
+}
+.fixed-badge {
+  position: absolute;
+  top: 12rpx;
+  right: 12rpx;
+  background: #F0F0F0;
+  color: #666;
+  font-size: 18rpx;
+  padding: 4rpx 12rpx;
+  border-radius: 20rpx;
+}
+.status-badge {
+  font-size: 20rpx;
+  color: #999;
+  margin-top: 8rpx;
+}
+.empty-state {
+  padding: 120rpx 40rpx;
+  text-align: center;
+}
+.empty-icon {
+  font-size: 80rpx;
+  display: block;
+  margin-bottom: 20rpx;
+}
+.empty-title {
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #333;
+  margin-bottom: 10rpx;
+}
+.empty-desc {
+  font-size: 24rpx;
+  color: #999;
+}
+.bottom-bar {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 24rpx 0;
+  border-top: 1rpx solid #E8E8E8;
+  margin-top: 20rpx;
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 24rpx;
+}
+.bottom-action {
+  flex: 1;
+  margin-right: 20rpx;
+}
+.action-text {
+  font-size: 24rpx;
+  color: #666;
+  display: block;
+  margin-bottom: 12rpx;
+}
+.sort-info {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8rpx;
+}
+.sort-chip {
+  background: #F5FAFE;
+  border-radius: 20rpx;
+  padding: 8rpx 16rpx;
+  font-size: 22rpx;
+  color: #333;
+  display: flex;
+  align-items: center;
+  gap: 6rpx;
+}
+.chip-icon {
+  font-size: 22rpx;
+}
+.chip-label {
+  font-size: 22rpx;
+}
+.chip-lock {
+  font-size: 20rpx;
+}
+.bottom-btn {
+  flex: 0 0 auto;
+  background: linear-gradient(135deg, #F97316, #FB923C);
+  color: #fff;
+  font-size: 26rpx;
+  font-weight: bold;
+  padding: 16rpx 32rpx;
+  border-radius: 30rpx;
+  white-space: nowrap;
+}
+</style>

+ 105 - 29
cfc-frontend/pages/index-home/index.vue

@@ -118,37 +118,21 @@
       </view>
     </view>
 
-    <!-- ④ 内容区 — 快捷功能入口(单排) -->
+    <!-- ④ 内容区 — 快捷功能入口(动态渲染,可在应用库自定义) -->
 
     <view class="section">
+      <view class="section-header">
+        <text class="section-title">⚡ 快捷功能</text>
+        <text class="section-more" @click="goAppLibrary">应用库 ›</text>
+      </view>
       <view class="quick-bar">
-        <view class="quick-item" @click="navTo('/pages/tasks/tasks')">
-          <view class="quick-icon" style="background:rgba(99,102,241,0.15)"><text>📋</text></view>
-          <text class="quick-label">任务</text>
-        </view>
-        <view class="quick-item" @click="navTo('/pages/action-detail/interaction-log')">
-          <view class="quick-icon" style="background:rgba(16,185,129,0.15)"><text>💬</text></view>
-          <text class="quick-label">互动</text>
-        </view>
-        <view class="quick-item" @click="navTo('/pages/mind-detail/family-dashboard')">
-          <view class="quick-icon" style="background:rgba(139,92,246,0.15)"><text>🏠</text></view>
-          <text class="quick-label">天盘</text>
-        </view>
-        <view class="quick-item" @click="navTo('/pages/profile-extra/family-members')">
-          <view class="quick-icon" style="background:rgba(245,158,11,0.15)"><text>👨‍👩‍👧‍👦</text></view>
-          <text class="quick-label">成员</text>
-        </view>
-        <view class="quick-item" @click="navTo('/pages/discover/circles')">
-          <view class="quick-icon" style="background:rgba(255,107,157,0.15)"><text>🤝</text></view>
-          <text class="quick-label">圈子</text>
-        </view>
-        <view class="quick-item" @click="navTo('/pages/shop/index/index')">
-          <view class="quick-icon" style="background:rgba(249,115,22,0.15)"><text>🛒</text></view>
-          <text class="quick-label">商城</text>
+        <view class="quick-item" v-for="(app, idx) in quickApps" :key="app.key" @click="onQuickTap(app)">
+          <view class="quick-icon" :style="getQuickIconStyle(idx)"><text>{{ app.icon }}</text></view>
+          <text class="quick-label">{{ app.name }}</text>
         </view>
-        <view class="quick-item" @click="navTo('/pages/diet/food-query')">
-          <view class="quick-icon" style="background:rgba(16,185,129,0.15)"><text>🥗</text></view>
-          <text class="quick-label">食材</text>
+        <view class="quick-item" v-if="quickApps.length === 0" @click="goAppLibrary">
+          <view class="quick-icon" style="background:rgba(99,102,241,0.15)"><text>+</text></view>
+          <text class="quick-label">去添加</text>
         </view>
       </view>
     </view>
@@ -431,7 +415,8 @@ import DailyTaskCard from '../../components/daily-task-card.vue'
 import ReportUploadCard from '../../components/report-upload-card.vue'
 import OnboardingProgressBar from '../../components/OnboardingProgressBar.vue'
 import OnboardingGuide from '../../components/OnboardingGuide.vue'
-import { acceptParentInvite, getActivityList, getFeaturedArticles, getEnergySandbox, getEnergyOverview, getFamilyMemberList, getChallengeList, getGuideConfig, switchToFamilyMember, getMyMembership, updateChallengeProgress, respondChallenge, getTodayTasks, completeTask as completeTaskApi, getTodayParentTasks, completeParentTask as completeParentTaskApi, getTaskHistory, getNotices, getNotificationList, getSurveyStatus, getSelfCheckStatus, getReferralCode, extractReferralCode } from '../../utils/api.js'
+import { acceptParentInvite, getActivityList, getFeaturedArticles, getEnergySandbox, getEnergyOverview, getFamilyMemberList, getChallengeList, getGuideConfig, switchToFamilyMember, getMyMembership, updateChallengeProgress, respondChallenge, getTodayTasks, completeTask as completeTaskApi, getTodayParentTasks, completeParentTask as completeParentTaskApi, getTaskHistory, getNotices, getNotificationList, getSurveyStatus, getSelfCheckStatus, getReferralCode, extractReferralCode, getAppLibrary, reportAppEnter } from '../../utils/api.js'
+import { findAppByKey, getShortcutableApps } from '../../utils/app-library.js'
 import nav from '../../utils/nav.js'
 import config from '@/config.js'
 import { parseDate } from '../../utils/format.js'
@@ -516,7 +501,13 @@ membershipDays: 0,
 
       // 分享邀请码
       referralCode: '',
-      referralCodeLoaded: false
+      referralCodeLoaded: false,
+
+      // 首页快捷区(应用库自定义)
+      quickApps: [],
+      myShortcuts: [],
+      appUsage: {},
+      quickLoaded: false
 
 
     }
@@ -751,10 +742,95 @@ membershipDays: 0,
       }, 400)
     },
 
+    // ===== 首页快捷区(应用库自定义) =====
+    // 排序算法:锁定占位 + 频度填满剩余(最多 6 个)
+    computeQuickApps: function(myShortcuts, usage) {
+      var lockedApps = []
+      var myShortcutsArr = myShortcuts || []
+      // 1. 锁定应用按 sortOrder 升序
+      lockedApps = myShortcutsArr.filter(function(s) { return s.locked === 1 })
+        .sort(function(a, b) { return a.sortOrder - b.sortOrder })
+      if (lockedApps.length >= 6) {
+        return lockedApps.slice(0, 6)
+      }
+      // 2. 剩余格数:候选 = 可快捷且未被锁定的应用,按频度降序填充
+      var remaining = 6 - lockedApps.length
+      var lockedKeys = {}
+      lockedApps.forEach(function(s) { lockedKeys[s.appKey] = true })
+      var candidates = getShortcutableApps().filter(function(app) { return !lockedKeys[app.key] })
+      candidates.sort(function(a, b) {
+        var ca = (usage && usage[a.key]) || 0
+        var cb = (usage && usage[b.key]) || 0
+        return cb - ca
+      })
+      return lockedApps.concat(candidates.slice(0, remaining))
+    },
+    // 后端返回 myShortcuts(含 locked/sortOrder),无配置时为空数组 → 前端按频度填充
+    loadQuickApps: function() {
+      var self = this
+      getAppLibrary().then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.myShortcuts = res.data.myShortcuts || []
+          self.appUsage = res.data.usage || {}
+          var result = self.computeQuickApps(self.myShortcuts, self.appUsage)
+          // 转成展示对象:从应用库取 icon/name/route
+          var shown = []
+          result.forEach(function(item) {
+            var appKey = item.appKey
+            var app = findAppByKey(appKey)
+            if (app) {
+              shown.push({
+                key: app.key,
+                name: app.name,
+                icon: app.icon,
+                route: app.route,
+                locked: item.locked
+              })
+            }
+          })
+          self.quickApps = shown
+          self.quickLoaded = true
+        }
+      }).catch(function() {
+        // 接口失败:回退到默认 6 个
+        var defaultKeys = ['task', 'interaction', 'tiandi', 'members', 'circle', 'shop']
+        var shown = []
+        defaultKeys.forEach(function(key) {
+          var app = findAppByKey(key)
+          if (app) shown.push({ key: app.key, name: app.name, icon: app.icon, route: app.route, locked: 0 })
+        })
+        self.quickApps = shown
+        self.quickLoaded = true
+      })
+    },
+    onQuickTap: function(app) {
+      if (!app || !app.route) return
+      // 记录进入次数(静默,失败不影响跳转)
+      if (app.key) {
+        reportAppEnter(app.key).catch(function() {})
+      }
+      this.navTo(app.route)
+    },
+    goAppLibrary: function() {
+      uni.navigateTo({ url: '/pages/home/app-library' })
+    },
+    getQuickIconStyle: function(idx) {
+      var palette = [
+        'rgba(99,102,241,0.15)',
+        'rgba(16,185,129,0.15)',
+        'rgba(139,92,246,0.15)',
+        'rgba(245,158,11,0.15)',
+        'rgba(255,107,157,0.15)',
+        'rgba(249,115,22,0.15)'
+      ]
+      return { background: palette[idx % palette.length] }
+    },
+
     // ===== 登录态数据加载 =====
     loadLoggedInData: function() {
       this.loadFamilyData()
       this.loadReferralCode()
+      this.loadQuickApps()
     },
     // 合并原 loadFamilyMembers + loadChildren:仅调用一次 getFamilyMemberList
     loadFamilyData: function() {

+ 4 - 1
cfc-frontend/pages/membership/index.vue

@@ -80,7 +80,8 @@
 </template>
 
 <script>
-import { getMyMembership, getMyButler } from '../../utils/api.js'
+import { getMyMembership, getMyButler, reportAppEnter
+} from '../../utils/api.js'
 import { parseDate } from '../../utils/format.js'
 
 export default {
@@ -113,6 +114,8 @@ export default {
     }
   },
   onShow() {
+    reportAppEnter('membership');
+    
     this.loadData()
     this.loadButlerEntry()
   },

+ 4 - 1
cfc-frontend/pages/mind-detail/family-dashboard.vue

@@ -163,7 +163,8 @@
 <script>
 import MpHtml from '@/components/mp-html/mp-html.vue'
 import { parseDate } from '../../utils/format.js'
-import { getFamilyFortune, getWeekFortune, getMonthFortune, getFortuneReports } from '../../utils/api.js'
+import { getFamilyFortune, getWeekFortune, getMonthFortune, getFortuneReports, reportAppEnter
+} from '../../utils/api.js'
 /**
  * FamilyDashboard — 家庭天盘详情页
  * 包含:运势数据、能量趋势、历史周报、月度日历等
@@ -261,6 +262,8 @@ export default {
     }
   },
   onLoad() {
+    reportAppEnter('tiandi');
+    
     // 初始化页面
     this.loadTodayFortune()
     this.loadWeekFortuneData()

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

@@ -135,7 +135,7 @@ import {
   updateFamilyMember,
   getPendingJoinRequests,
   approveJoinRequest,
-  rejectJoinRequest
+  rejectJoinRequest, reportAppEnter
 } from '../../utils/api.js'
 import store from '../../store/index.js'
 
@@ -186,6 +186,8 @@ export default {
     }
   },
   onShow() {
+    reportAppEnter('members');
+    
     var userInfo = uni.getStorageSync('userInfo')
     this.userId = (userInfo && userInfo.id) || 0
     this.userNickname = uni.getStorageSync('nickname') || (userInfo && userInfo.nickname) || ''

+ 4 - 1
cfc-frontend/pages/rewards/rewards.vue

@@ -64,7 +64,8 @@
 
 <script>
 import config from '@/config.js'
-import { getWishlist, exchangeReward as exchangeRewardApi, createReward as createRewardApi, getPointsBalance, getChildren, getParentWishlist, createParentWishlist, exchangeParentWishlist } from '../../utils/api.js'
+import { getWishlist, exchangeReward as exchangeRewardApi, createReward as createRewardApi, getPointsBalance, getChildren, getParentWishlist, createParentWishlist, exchangeParentWishlist, reportAppEnter
+} from '../../utils/api.js'
 
 export default {
   data() {
@@ -81,6 +82,8 @@ export default {
     }
   },
   onShow() {
+    reportAppEnter('medal');
+    
     // 规划师角色跳转到消息页
     const role = uni.getStorageSync('currentRole') || uni.getStorageSync('role') || 'parent'
     if (role === 'teacher') {

+ 3 - 1
cfc-frontend/pages/shop/index/index.vue

@@ -147,7 +147,7 @@
 </template>
 
 <script>
-import { goodsCategory, productList, getMyMembership, cartCount } from '@/utils/api.js'
+import { goodsCategory, productList, getMyMembership, cartCount, reportAppEnter} from '@/utils/api.js'
 import config from '@/config.js'
 export default {
   data() {
@@ -169,6 +169,8 @@ export default {
     }
   },
   onLoad() {
+      reportAppEnter('shop');
+    
     this.checkMemberStatus()
     this.isLoggedIn = !!uni.getStorageSync('token')
     this.loadCategories()

+ 4 - 1
cfc-frontend/pages/tasks/tasks.vue

@@ -204,7 +204,8 @@
 </template>
 
 <script>
-import { getMyPendingTasks, acceptTask as acceptTaskApi, rejectTask as rejectTaskApi, getTodayTasks, completeTask as completeTaskApi, startTask as startTaskApi, createTask as createTaskApi, getFamilyMemberList, getMyMembership } from '../../utils/api.js'
+import { getMyPendingTasks, acceptTask as acceptTaskApi, rejectTask as rejectTaskApi, getTodayTasks, completeTask as completeTaskApi, startTask as startTaskApi, createTask as createTaskApi, getFamilyMemberList, getMyMembership, reportAppEnter
+} from '../../utils/api.js'
 import { parseDate } from '../../utils/format.js'
 import FamilyMemberStrip from '../../components/FamilyMemberStrip.vue'
 
@@ -240,6 +241,8 @@ export default {
     }
   },
   onLoad(options) {
+    reportAppEnter('task');
+    
     if (options && options.tab) {
       this.activeTab = options.tab
     }

+ 3 - 1
cfc-frontend/pages/wealth/index.vue

@@ -248,7 +248,7 @@ import {
   productList,
   extractReferralCode,
   getChildBadgeStats,
-  getDailyTaskOverview
+  getDailyTaskOverview, reportAppEnter
 } from '../../utils/api.js'
 import nav from '../../utils/nav.js'
 import config from '../../config.js'
@@ -356,6 +356,8 @@ export default {
     }
   },
   onShow() {
+    reportAppEnter('wealth');
+    
     this.showTabTransition = true
 
     var token = uni.getStorageSync('token')

+ 3 - 1
cfc-frontend/pages/wishes/index.vue

@@ -242,7 +242,7 @@ import {
   requestWishExchange,
   confirmWishReceived,
   cancelWish,
-  getPointsBalance
+  getPointsBalance, reportAppEnter
 } from '../../utils/api.js'
 
 import PlayfulCard from '../../components/PlayfulCard.vue'
@@ -363,6 +363,8 @@ export default {
     }
   },
   onShow() {
+    reportAppEnter('wish');
+    
     this.loadData()
   },
   methods: {