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

merge(miniprogram): 聊天/消息类入口按有效会员隐藏 合入 cfclub 主线

Agnes Agent 1 день назад
Родитель
Сommit
3d35ab714c

+ 1 - 3
cfc-frontend/pages/action-detail/index.vue

@@ -245,7 +245,6 @@
 
 
     <!-- 底部占位 -->
     <!-- 底部占位 -->
     <view class="bottom-spacer"></view>
     <view class="bottom-spacer"></view>
-    <FloatingAvatar />
 
 
     <!-- 帮助说明弹窗 -->
     <!-- 帮助说明弹窗 -->
     <view class="help-mask" v-if="helpPopup.visible" @tap="closeHelp">
     <view class="help-mask" v-if="helpPopup.visible" @tap="closeHelp">
@@ -275,7 +274,6 @@ import DimensionActivities from '../../components/DimensionActivities.vue'
 import DimensionProducts from '../../components/DimensionProducts.vue'
 import DimensionProducts from '../../components/DimensionProducts.vue'
 import DimensionIntroCard from '../../components/DimensionIntroCard.vue'
 import DimensionIntroCard from '../../components/DimensionIntroCard.vue'
 import DimensionVisitorCard from '../../components/DimensionVisitorCard.vue'
 import DimensionVisitorCard from '../../components/DimensionVisitorCard.vue'
-import FloatingAvatar from '../../components/AIFloatingAvatar.vue'
 import RadarChart from '../../components/RadarChart.vue'
 import RadarChart from '../../components/RadarChart.vue'
 import DimensionSubDims from './components/DimensionSubDims.vue'
 import DimensionSubDims from './components/DimensionSubDims.vue'
 import DimensionPageSkeleton from '../../components/DimensionPageSkeleton.vue'
 import DimensionPageSkeleton from '../../components/DimensionPageSkeleton.vue'
@@ -284,7 +282,7 @@ import fontScaleMixin from '@/mixins/font-scale.js'
 
 
 export default {
 export default {
   mixins: [fontScaleMixin],
   mixins: [fontScaleMixin],
-  components: { TabTransition, PageBanner, FamilyEnergyBar, FamilyRelationGraph, PearlDiagram, AbilityDiagram, OctopusDiagram, MemberSelector, DimensionTasks, DimensionActivities, DimensionProducts, DimensionIntroCard, DimensionVisitorCard, FloatingAvatar, RadarChart, DimensionSubDims, DimensionPageSkeleton },
+  components: { TabTransition, PageBanner, FamilyEnergyBar, FamilyRelationGraph, PearlDiagram, AbilityDiagram, OctopusDiagram, MemberSelector, DimensionTasks, DimensionActivities, DimensionProducts, DimensionIntroCard, DimensionVisitorCard, RadarChart, DimensionSubDims, DimensionPageSkeleton },
   data() {
   data() {
     return {
     return {
       gridColor: BrandColors.emerald100,
       gridColor: BrandColors.emerald100,

+ 1 - 3
cfc-frontend/pages/body-detail/index.vue

@@ -238,7 +238,6 @@
     </view>
     </view>
 
 
     <view class="bottom-spacer"></view>
     <view class="bottom-spacer"></view>
-    <FloatingAvatar />
   </view>
   </view>
 </template>
 </template>
 
 
@@ -248,7 +247,6 @@ import TabTransition from '../../components/tab-transition.vue'
 import PageBanner from '../../components/PageBanner.vue'
 import PageBanner from '../../components/PageBanner.vue'
 import FamilyEnergyBar from '../../components/FamilyEnergyBar.vue'
 import FamilyEnergyBar from '../../components/FamilyEnergyBar.vue'
 import RadarChart from '../../components/RadarChart.vue'
 import RadarChart from '../../components/RadarChart.vue'
-import FloatingAvatar from '../../components/AIFloatingAvatar.vue'
 import DimensionIntroCard from '../../components/DimensionIntroCard.vue'
 import DimensionIntroCard from '../../components/DimensionIntroCard.vue'
 import DimensionVisitorCard from '../../components/DimensionVisitorCard.vue'
 import DimensionVisitorCard from '../../components/DimensionVisitorCard.vue'
 import DimensionPageSkeleton from '../../components/DimensionPageSkeleton.vue'
 import DimensionPageSkeleton from '../../components/DimensionPageSkeleton.vue'
@@ -273,7 +271,7 @@ import fontScaleMixin from '@/mixins/font-scale.js'
 
 
 export default {
 export default {
   mixins: [fontScaleMixin],
   mixins: [fontScaleMixin],
-  components: { TabTransition, PageBanner, FamilyEnergyBar, RadarChart, FloatingAvatar, MemberSelector, DimensionIntroCard, DimensionVisitorCard, DimensionPageSkeleton },
+  components: { TabTransition, PageBanner, FamilyEnergyBar, RadarChart, MemberSelector, DimensionIntroCard, DimensionVisitorCard, DimensionPageSkeleton },
   data() {
   data() {
     return {
     return {
       gridColor: BrandColors.orange200,
       gridColor: BrandColors.orange200,

+ 30 - 7
cfc-frontend/pages/home/app-library.vue

@@ -67,6 +67,7 @@
 <script>
 <script>
 import { APP_LIBRARY, getShortcutableApps, findAppByKey } from '@/utils/app-library'
 import { APP_LIBRARY, getShortcutableApps, findAppByKey } from '@/utils/app-library'
 import { getAppLibrary, saveShortcutConfig, resetShortcutConfig } from '@/utils/api.js'
 import { getAppLibrary, saveShortcutConfig, resetShortcutConfig } from '@/utils/api.js'
+import { isActiveMember } from '@/utils/membershipGate.js'
 import fontScaleMixin from '@/mixins/font-scale.js'
 import fontScaleMixin from '@/mixins/font-scale.js'
 
 
 export default {
 export default {
@@ -77,21 +78,42 @@ export default {
       configurableApps: [],
       configurableApps: [],
       quickApps: [],
       quickApps: [],
       myShortcuts: [],
       myShortcuts: [],
-      appUsage: {}
+      appUsage: {},
+      // 会员有效态(isActive):控制 AI 管家入口可见性,默认 false(fail-closed)
+      memberActive: false
     }
     }
   },
   },
   onShow() {
   onShow() {
+    // 每次进入刷新会员有效态(覆盖从升级/续费页返回),再按最新口径重建列表
+    this.refreshMemberActive()
     this.loadAppLibrary()
     this.loadAppLibrary()
     this.loadQuickApps()
     this.loadQuickApps()
   },
   },
   methods: {
   methods: {
+    // 会员有效态刷新:唯一判据 /api/ai/gate 的 data.isActive(不随运营开关,fail-closed)
+    refreshMemberActive: function() {
+      var self = this
+      isActiveMember().then(function(active) {
+        var changed = self.memberActive !== active
+        self.memberActive = active
+        if (changed) {
+          self.loadAppLibrary()
+          self.loadQuickApps()
+        }
+      })
+    },
+    // 非有效会员剔除 AI 管家(key='ai');会员原样返回
+    filterAiApps: function(list) {
+      var active = this.memberActive
+      return (list || []).filter(function(app) { return app.key !== 'ai' || active })
+    },
     goBack() {
     goBack() {
       uni.navigateBack()
       uni.navigateBack()
     },
     },
     loadAppLibrary() {
     loadAppLibrary() {
       const allApps = APP_LIBRARY
       const allApps = APP_LIBRARY
       this.fixedApps = allApps.filter(app => app.canShortcut === false).slice(0, 20)
       this.fixedApps = allApps.filter(app => app.canShortcut === false).slice(0, 20)
-      this.configurableApps = allApps.filter(app => app.canShortcut === true).slice(0, 20)
+      this.configurableApps = this.filterAiApps(allApps.filter(app => app.canShortcut === true).slice(0, 20))
     },
     },
     loadQuickApps() {
     loadQuickApps() {
       var self = this
       var self = this
@@ -103,26 +125,27 @@ export default {
           var lockedApps = (self.myShortcuts || []).filter(function(s) { return s.locked === 1 })
           var lockedApps = (self.myShortcuts || []).filter(function(s) { return s.locked === 1 })
             .sort(function(a, b) { return a.sortOrder - b.sortOrder })
             .sort(function(a, b) { return a.sortOrder - b.sortOrder })
           if (lockedApps.length >= 6) {
           if (lockedApps.length >= 6) {
-            self.quickApps = lockedApps.slice(0, 6).map(function(s) {
+            self.quickApps = self.filterAiApps(lockedApps.slice(0, 6).map(function(s) {
               var app = findAppByKey(s.appKey)
               var app = findAppByKey(s.appKey)
               return app ? { key: app.key, name: app.name, icon: app.icon, route: app.route, locked: s.locked } : null
               return app ? { key: app.key, name: app.name, icon: app.icon, route: app.route, locked: s.locked } : null
-            }).filter(function(x) { return x })
+            }).filter(function(x) { return x }))
             return
             return
           }
           }
           var remaining = 6 - lockedApps.length
           var remaining = 6 - lockedApps.length
           var lockedKeys = {}
           var lockedKeys = {}
           lockedApps.forEach(function(s) { lockedKeys[s.appKey] = true })
           lockedApps.forEach(function(s) { lockedKeys[s.appKey] = true })
-          var candidates = getShortcutableApps().filter(function(app) { return !lockedKeys[app.key] })
+          var selfActive = self.memberActive
+          var candidates = getShortcutableApps().filter(function(app) { return !lockedKeys[app.key] && (app.key !== 'ai' || selfActive) })
           candidates.sort(function(a, b) {
           candidates.sort(function(a, b) {
             var ca = (self.appUsage && self.appUsage[a.key]) || 0
             var ca = (self.appUsage && self.appUsage[a.key]) || 0
             var cb = (self.appUsage && self.appUsage[b.key]) || 0
             var cb = (self.appUsage && self.appUsage[b.key]) || 0
             return cb - ca
             return cb - ca
           })
           })
           var shown = lockedApps.concat(candidates.slice(0, remaining))
           var shown = lockedApps.concat(candidates.slice(0, remaining))
-          self.quickApps = shown.map(function(item) {
+          self.quickApps = self.filterAiApps(shown.map(function(item) {
             var app = findAppByKey(item.appKey)
             var app = findAppByKey(item.appKey)
             return app ? { key: app.key, name: app.name, icon: app.icon, route: app.route, locked: item.locked } : null
             return app ? { key: app.key, name: app.name, icon: app.icon, route: app.route, locked: item.locked } : null
-          }).filter(function(x) { return x })
+          }).filter(function(x) { return x }))
         }
         }
       }).catch(function() {
       }).catch(function() {
         self.loadAppLibrary()
         self.loadAppLibrary()

+ 30 - 3
cfc-frontend/pages/index-home/index.vue

@@ -456,6 +456,7 @@ import OnboardingGuide from '../../components/OnboardingGuide.vue'
 import ConfigurableGuide from '../../components/ConfigurableGuide.vue'
 import ConfigurableGuide from '../../components/ConfigurableGuide.vue'
 import { acceptParentInvite, getActivityList, getFeaturedArticles, productList, getEnergySandbox, getEnergyOverview, getFamilyMemberList, getChallengeList, getGuideConfig, getGuidePopupList, consumeGuidePopup, switchToFamilyMember, getMyMembership, updateChallengeProgress, respondChallenge, getTodayTasks, completeTask as completeTaskApi, getTodayParentTasks, completeParentTask as completeParentTaskApi, getTaskHistory, getNotices, getNotificationList, readNotification, getSurveyStatus, getSelfCheckStatus, getReferralCode, extractReferralCode, getAppLibrary, reportAppEnter, getEnergyLogs, getPointsLogs, getCfList, getCfSummary } from '../../utils/api.js'
 import { acceptParentInvite, getActivityList, getFeaturedArticles, productList, getEnergySandbox, getEnergyOverview, getFamilyMemberList, getChallengeList, getGuideConfig, getGuidePopupList, consumeGuidePopup, switchToFamilyMember, getMyMembership, updateChallengeProgress, respondChallenge, getTodayTasks, completeTask as completeTaskApi, getTodayParentTasks, completeParentTask as completeParentTaskApi, getTaskHistory, getNotices, getNotificationList, readNotification, getSurveyStatus, getSelfCheckStatus, getReferralCode, extractReferralCode, getAppLibrary, reportAppEnter, getEnergyLogs, getPointsLogs, getCfList, getCfSummary } from '../../utils/api.js'
 import { findAppByKey, getShortcutableApps } from '../../utils/app-library.js'
 import { findAppByKey, getShortcutableApps } from '../../utils/app-library.js'
+import { isActiveMember } from '../../utils/membershipGate.js'
 import nav from '../../utils/nav.js'
 import nav from '../../utils/nav.js'
 import config from '@/config.js'
 import config from '@/config.js'
 import { parseDate } from '../../utils/format.js'
 import { parseDate } from '../../utils/format.js'
@@ -595,6 +596,8 @@ membershipDays: 0,
       // 首页快捷区(应用库自定义)
       // 首页快捷区(应用库自定义)
       quickApps: [],
       quickApps: [],
       myShortcuts: [],
       myShortcuts: [],
+      // 会员有效态(isActive):控制 AI 管家入口可见性,默认 false(fail-closed)
+      memberActive: false,
       appUsage: {},
       appUsage: {},
       quickLoaded: false,
       quickLoaded: false,
 
 
@@ -845,6 +848,8 @@ membershipDays: 0,
     this._doInit(options)
     this._doInit(options)
   },
   },
   onShow() {
   onShow() {
+    // 每次进入前台刷新会员有效态(覆盖从升级/续费页返回),无 token 由封装内部短路
+    this.refreshMemberActive()
     // splash overlay 已在 data() 中根据 storage 标记初始化,此处只需配置关闭条件
     // splash overlay 已在 data() 中根据 storage 标记初始化,此处只需配置关闭条件
     if (this.splashOverlay) {
     if (this.splashOverlay) {
       uni.removeStorageSync('_showSplashOverlay')
       uni.removeStorageSync('_showSplashOverlay')
@@ -929,7 +934,11 @@ membershipDays: 0,
       var remaining = 6 - lockedApps.length
       var remaining = 6 - lockedApps.length
       var lockedKeys = {}
       var lockedKeys = {}
       lockedApps.forEach(function(s) { lockedKeys[s.appKey] = true })
       lockedApps.forEach(function(s) { lockedKeys[s.appKey] = true })
-      var candidates = getShortcutableApps().filter(function(app) { return !lockedKeys[app.key] })
+      // 非有效会员:频度候选中剔除 AI 管家(key='ai'),避免其占位后被渲染
+      var selfActive = this.memberActive
+      var candidates = getShortcutableApps().filter(function(app) {
+        return !lockedKeys[app.key] && (app.key !== 'ai' || selfActive)
+      })
       candidates.sort(function(a, b) {
       candidates.sort(function(a, b) {
         var ca = (usage && usage[a.key]) || 0
         var ca = (usage && usage[a.key]) || 0
         var cb = (usage && usage[b.key]) || 0
         var cb = (usage && usage[b.key]) || 0
@@ -975,15 +984,33 @@ membershipDays: 0,
               }
               }
             })
             })
           }
           }
-          self.quickApps = shown
+          // 合并结果出口再兜底过滤一次:连用户已锁定的 ai 项对非会员也不可见
+          self.quickApps = self.filterAiApps(shown)
           self.quickLoaded = true
           self.quickLoaded = true
         }
         }
       }).catch(function() {
       }).catch(function() {
         // 接口失败:回退到默认 6 个
         // 接口失败:回退到默认 6 个
-        self.quickApps = self.getDefaultQuickApps()
+        self.quickApps = self.filterAiApps(self.getDefaultQuickApps())
         self.quickLoaded = true
         self.quickLoaded = true
       })
       })
     },
     },
+    // 会员有效态刷新:唯一判据 /api/ai/gate 的 data.isActive(不随运营开关,fail-closed)
+    refreshMemberActive: function() {
+      var self = this
+      isActiveMember().then(function(active) {
+        var changed = self.memberActive !== active
+        self.memberActive = active
+        // 频度候选依赖 memberActive,状态变化后按最新口径重算快捷区
+        if (changed && self.isLoggedIn()) {
+          self.loadQuickApps()
+        }
+      })
+    },
+    // 非有效会员剔除 AI 管家(key='ai');会员原样返回
+    filterAiApps: function(list) {
+      var active = this.memberActive
+      return (list || []).filter(function(app) { return app.key !== 'ai' || active })
+    },
     onQuickTap: function(app) {
     onQuickTap: function(app) {
       if (!app || !app.route) return
       if (!app || !app.route) return
       // 记录进入次数(静默,失败不影响跳转)
       // 记录进入次数(静默,失败不影响跳转)

+ 1 - 3
cfc-frontend/pages/mind-detail/index.vue

@@ -253,7 +253,6 @@
 
 
     <!-- 底部占位 -->
     <!-- 底部占位 -->
     <view class="bottom-spacer"></view>
     <view class="bottom-spacer"></view>
-    <FloatingAvatar />
   </view>
   </view>
 </template>
 </template>
 
 
@@ -266,7 +265,6 @@ import DimensionVisitorCard from '../../components/DimensionVisitorCard.vue'
 import RadarChart from '../../components/RadarChart.vue'
 import RadarChart from '../../components/RadarChart.vue'
 import FamilyEnergyBar from '../../components/FamilyEnergyBar.vue'
 import FamilyEnergyBar from '../../components/FamilyEnergyBar.vue'
 import PsychCrisisBanner from '@/pages/mind-detail/components/PsychCrisisBanner.vue'
 import PsychCrisisBanner from '@/pages/mind-detail/components/PsychCrisisBanner.vue'
-import FloatingAvatar from '../../components/AIFloatingAvatar.vue'
 import FamilyTianpanCard from '@/pages/mind-detail/components/FamilyTianpanCard.vue'
 import FamilyTianpanCard from '@/pages/mind-detail/components/FamilyTianpanCard.vue'
 import DimensionPageSkeleton from '../../components/DimensionPageSkeleton.vue'
 import DimensionPageSkeleton from '../../components/DimensionPageSkeleton.vue'
 import MemberSelector from '@/components/MemberSelector.vue'
 import MemberSelector from '@/components/MemberSelector.vue'
@@ -278,7 +276,7 @@ import fontScaleMixin from '@/mixins/font-scale.js'
 
 
 export default {
 export default {
   mixins: [fontScaleMixin],
   mixins: [fontScaleMixin],
-components: { TabTransition, PageBanner, RadarChart, FamilyEnergyBar, PsychCrisisBanner, FloatingAvatar, FamilyTianpanCard, MemberSelector, DimensionIntroCard, DimensionVisitorCard, DimensionPageSkeleton },
+components: { TabTransition, PageBanner, RadarChart, FamilyEnergyBar, PsychCrisisBanner, FamilyTianpanCard, MemberSelector, DimensionIntroCard, DimensionVisitorCard, DimensionPageSkeleton },
   data() {
   data() {
     return {
     return {
       gridColor: BrandColors.pink100,
       gridColor: BrandColors.pink100,

+ 14 - 4
cfc-frontend/pages/profile-main/profile.vue

@@ -333,7 +333,7 @@
 
 
       <!-- 设置 + 退出 -->
       <!-- 设置 + 退出 -->
       <view class="bottom-menu-section">
       <view class="bottom-menu-section">
-        <view class="menu-row" @click="goToMessageHistory">
+        <view class="menu-row" v-if="memberActive" @click="goToMessageHistory">
           <text class="menu-row-icon">📨</text>
           <text class="menu-row-icon">📨</text>
           <text class="menu-row-text">历史消息</text>
           <text class="menu-row-text">历史消息</text>
           <text class="menu-row-arrow">›</text>
           <text class="menu-row-arrow">›</text>
@@ -362,12 +362,10 @@
       :inviteText="posterInviteText"
       :inviteText="posterInviteText"
       @close="closeSharePoster"
       @close="closeSharePoster"
     />
     />
-    <FloatingAvatar />
   </view>
   </view>
 </template>
 </template>
 
 
 <script>
 <script>
-import FloatingAvatar from '../../components/AIFloatingAvatar.vue'
 import ContentSharePoster from '../../components/ContentSharePoster.vue'
 import ContentSharePoster from '../../components/ContentSharePoster.vue'
 import {
 import {
   getChildren, getFamilyMemberList,
   getChildren, getFamilyMemberList,
@@ -382,11 +380,12 @@ import {
 import { BrandColors } from '../../utils/brand-colors.js'
 import { BrandColors } from '../../utils/brand-colors.js'
 import { isApprovedActivityProvider } from '../../utils/activity-role.js'
 import { isApprovedActivityProvider } from '../../utils/activity-role.js'
 import fontScaleMixin from '@/mixins/font-scale.js'
 import fontScaleMixin from '@/mixins/font-scale.js'
+import { isActiveMember } from '@/utils/membershipGate.js'
 import shareMixin from '../../components/share-mixin.js'
 import shareMixin from '../../components/share-mixin.js'
 
 
 export default {
 export default {
   mixins: [fontScaleMixin, shareMixin],
   mixins: [fontScaleMixin, shareMixin],
-  components: { FloatingAvatar, ContentSharePoster },
+  components: { ContentSharePoster },
   data() {
   data() {
     return {
     return {
       nickname: '',
       nickname: '',
@@ -397,6 +396,8 @@ export default {
       userInfo: {},
       userInfo: {},
       memberLevel: null,
       memberLevel: null,
       membershipDays: 0,
       membershipDays: 0,
+      // 会员有效态(isActive):控制「历史消息」等入口可见性,默认 false(fail-closed)
+      memberActive: false,
       totalPoints: 0,
       totalPoints: 0,
       referralEarnings: 0,
       referralEarnings: 0,
       teamMembers: 0,
       teamMembers: 0,
@@ -477,6 +478,8 @@ export default {
     if (!this.isLoggedIn) {
     if (!this.isLoggedIn) {
       return
       return
     }
     }
+    // 每次进入刷新会员有效态(覆盖从升级/续费页返回)
+    this.refreshMemberActive()
     var role = uni.getStorageSync('currentRole') || uni.getStorageSync('role') || 'parent'
     var role = uni.getStorageSync('currentRole') || uni.getStorageSync('role') || 'parent'
     this.role = role
     this.role = role
     this.isSwitchedChild = uni.getStorageSync('isSwitchedChild') || false
     this.isSwitchedChild = uni.getStorageSync('isSwitchedChild') || false
@@ -495,6 +498,13 @@ export default {
     }
     }
   },
   },
   methods: {
   methods: {
+    // 会员有效态刷新:唯一判据 /api/ai/gate 的 data.isActive(不随运营开关,fail-closed)
+    refreshMemberActive: function() {
+      var self = this
+      isActiveMember().then(function(active) {
+        self.memberActive = active
+      })
+    },
     // 服务商身份:兼容两条申请/审核链路(vendor 入驻 / 服务角色申请)
     // 服务商身份:兼容两条申请/审核链路(vendor 入驻 / 服务角色申请)
     // 用 isApprovedActivityProvider 统一判定,避免只认 vendorStatus 漏掉服务角色申请链路
     // 用 isApprovedActivityProvider 统一判定,避免只认 vendorStatus 漏掉服务角色申请链路
     checkVendorStatus() {
     checkVendorStatus() {

+ 6 - 1
cfc-frontend/pages/profile/components/ProfileMenu.vue

@@ -4,7 +4,7 @@
       <!-- ===== 个人 ===== -->
       <!-- ===== 个人 ===== -->
       <view class="menu-group">
       <view class="menu-group">
         <view class="menu-group-title">── 个人 ──</view>
         <view class="menu-group-title">── 个人 ──</view>
-        <view class="menu-item" @click="goToMessageHistory">
+        <view class="menu-item" v-if="memberActive" @click="goToMessageHistory">
           <text>📨 消息历史</text>
           <text>📨 消息历史</text>
           <text class="arrow">›</text>
           <text class="arrow">›</text>
         </view>
         </view>
@@ -155,6 +155,11 @@ export default {
     showServices: {
     showServices: {
       type: Boolean,
       type: Boolean,
       default: true
       default: true
+    },
+    // 会员有效态(isActive),由宿主页传入;默认 false(fail-closed)
+    memberActive: {
+      type: Boolean,
+      default: false
     }
     }
   },
   },
   data() {
   data() {

+ 13 - 2
cfc-frontend/pages/profile/index.vue

@@ -15,7 +15,7 @@
         <text class="back-label">返回富页面</text>
         <text class="back-label">返回富页面</text>
       </view>
       </view>
       <ProfileHeader @avatar-click="goToEditInfo" />
       <ProfileHeader @avatar-click="goToEditInfo" />
-      <ProfileMenu :role="role" @invite-generate="onInviteGenerate" />
+      <ProfileMenu :role="role" :member-active="memberActive" @invite-generate="onInviteGenerate" />
       <view class="sso-entry" @click="goTrain">
       <view class="sso-entry" @click="goTrain">
         <text class="sso-entry-icon">🎓</text>
         <text class="sso-entry-icon">🎓</text>
         <text class="sso-entry-text">进入训练营</text>
         <text class="sso-entry-text">进入训练营</text>
@@ -31,6 +31,7 @@ import ProfileMenu from '@/pages/profile/components/ProfileMenu.vue'
 import { getReferralCode, extractReferralCode } from '../../utils/api.js'
 import { getReferralCode, extractReferralCode } from '../../utils/api.js'
 import { jumpToTrain } from '../../utils/sso.js'
 import { jumpToTrain } from '../../utils/sso.js'
 import { getFontScale } from '../../utils/fontScale.js'
 import { getFontScale } from '../../utils/fontScale.js'
+import { isActiveMember } from '../../utils/membershipGate.js'
 
 
 export default {
 export default {
   components: {
   components: {
@@ -47,7 +48,9 @@ export default {
       fromWealth: false,
       fromWealth: false,
       referralCode: '',
       referralCode: '',
       referralCodeLoaded: false,
       referralCodeLoaded: false,
-      fontScale: 1
+      fontScale: 1,
+      // 会员有效态(isActive):控制 ProfileMenu「消息历史」可见性,默认 false(fail-closed)
+      memberActive: false
     }
     }
   },
   },
   computed: {
   computed: {
@@ -77,6 +80,7 @@ export default {
     this.isLoggedIn = !!uni.getStorageSync('token')
     this.isLoggedIn = !!uni.getStorageSync('token')
     if (this.isLoggedIn) {
     if (this.isLoggedIn) {
       this.loadReferralCode()
       this.loadReferralCode()
+      this.refreshMemberActive()
     }
     }
   },
   },
   onUnload() {
   onUnload() {
@@ -114,6 +118,13 @@ export default {
     }
     }
   },
   },
   methods: {
   methods: {
+    // 会员有效态刷新:唯一判据 /api/ai/gate 的 data.isActive(不随运营开关,fail-closed)
+    refreshMemberActive: function() {
+      var self = this
+      isActiveMember().then(function(active) {
+        self.memberActive = active
+      })
+    },
     /** 获取自己的邀请码用于分享 */
     /** 获取自己的邀请码用于分享 */
     async loadReferralCode() {
     async loadReferralCode() {
       if (this.referralCodeLoaded || !this.isLoggedIn) return
       if (this.referralCodeLoaded || !this.isLoggedIn) return

+ 1 - 3
cfc-frontend/pages/wisdom-detail/index.vue

@@ -235,7 +235,6 @@
 
 
     <!-- 底部占位 -->
     <!-- 底部占位 -->
     <view class="bottom-spacer"></view>
     <view class="bottom-spacer"></view>
-    <FloatingAvatar />
   </view>
   </view>
 </template>
 </template>
 
 
@@ -245,7 +244,6 @@ import nav from '../../utils/nav'
 import TabTransition from '../../components/tab-transition.vue'
 import TabTransition from '../../components/tab-transition.vue'
 import RadarChart from '../../components/RadarChart.vue'
 import RadarChart from '../../components/RadarChart.vue'
 import FamilyEnergyBar from '../../components/FamilyEnergyBar.vue'
 import FamilyEnergyBar from '../../components/FamilyEnergyBar.vue'
-import FloatingAvatar from '../../components/AIFloatingAvatar.vue'
 import DimensionIntroCard from '../../components/DimensionIntroCard.vue'
 import DimensionIntroCard from '../../components/DimensionIntroCard.vue'
 import DimensionVisitorCard from '../../components/DimensionVisitorCard.vue'
 import DimensionVisitorCard from '../../components/DimensionVisitorCard.vue'
 import DimensionPageSkeleton from '../../components/DimensionPageSkeleton.vue'
 import DimensionPageSkeleton from '../../components/DimensionPageSkeleton.vue'
@@ -256,7 +254,7 @@ import fontScaleMixin from '@/mixins/font-scale.js'
 
 
 export default {
 export default {
   mixins: [fontScaleMixin],
   mixins: [fontScaleMixin],
-  components: { TabTransition, RadarChart, FamilyEnergyBar, FloatingAvatar, MemberSelector, DimensionIntroCard, DimensionVisitorCard, DimensionPageSkeleton },
+  components: { TabTransition, RadarChart, FamilyEnergyBar, MemberSelector, DimensionIntroCard, DimensionVisitorCard, DimensionPageSkeleton },
   data() {
   data() {
     return {
     return {
       gridColor: BrandColors.indigo100,
       gridColor: BrandColors.indigo100,

+ 27 - 0
cfc-frontend/utils/membershipGate.js

@@ -0,0 +1,27 @@
+/**
+ * 会员有效态(前端入口可见性统一判据)
+ *
+ * 唯一判据 = POST /api/ai/gate 返回 data.isActive === true。
+ * 刻意不使用 canUse / status:运营开关 ai.coach.member_gate_enabled 关闭时,
+ * 后端会把未付费用户的 status 也置为 ALLOW / canUse=true,用它判入口会放行非会员。
+ * isActive 在开关判断之前按真实付费记录填充
+ * (users.member_expire_time > now 或 family_memberships payment_status='paid' 且 end_date > now),
+ * 不受运营开关影响,也不受当日 AI 配额影响。
+ *
+ * 过期会员 isActive=false,按需求一律隐藏入口、不补历史入口。
+ * fail-closed:无 token 直接 false(不发请求);接口异常/超时/字段缺失一律 false。
+ *
+ * @returns {Promise<boolean>} 是否为有效付费会员
+ */
+import { aiGateStatus } from './api.js'
+
+export const isActiveMember = () => {
+  if (!uni.getStorageSync('token')) {
+    return Promise.resolve(false)
+  }
+  return aiGateStatus().then(function(res) {
+    return !!(res && res.data && res.data.isActive === true)
+  }).catch(function() {
+    return false
+  })
+}