Browse Source

Merge branch 'cfclub' of https://git.iwintrue.com/liaoxg/cfc into cfclub

Xiaogang Liao 2 months ago
parent
commit
59e42f6804

+ 38 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -2592,6 +2592,44 @@ log.info("已添加template_id列到tasks表");
             log.warn("创建 user_address 表失败: {}", e.getMessage());
         }
 
+        // ==================== 会员价格体系 ====================
+
+        // 迁移: activities表添加member_price列
+        try {
+            Integer exists = jdbcTemplate.queryForObject(
+                "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'activities' AND COLUMN_NAME = 'member_price'",
+                Integer.class);
+            if (exists == null || exists == 0) {
+                jdbcTemplate.execute("ALTER TABLE activities ADD COLUMN member_price INT COMMENT '会员价(分)' AFTER price");
+                log.info("已添加member_price列到activities表");
+            } else {
+                log.info("member_price列已存在,跳过");
+            }
+        } catch (Exception e) {
+            log.warn("检查/添加activities.member_price失败: {}", e.getMessage());
+        }
+
+        // 迁移: 创建 member_discount_configs 表(分类级别折扣配置)
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS member_discount_configs (" +
+                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                "level_code VARCHAR(20) NOT NULL COMMENT '会员等级: FAMILY/PROVIDER', " +
+                "target_type VARCHAR(20) NOT NULL COMMENT '目标类型: product/activity', " +
+                "target_id BIGINT DEFAULT NULL COMMENT '目标ID(NULL表示分类级别)', " +
+                "category_id BIGINT DEFAULT NULL COMMENT '类目ID(分类级别折扣)', " +
+                "discount_percent INT NOT NULL COMMENT '折扣百分比(如90表示90%)', " +
+                "enabled TINYINT DEFAULT 1 COMMENT '是否启用', " +
+                "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+                "INDEX idx_level (level_code), " +
+                "INDEX idx_target (target_type, target_id), " +
+                "INDEX idx_category (target_type, category_id)" +
+                ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员折扣配置'");
+            log.info("已创建member_discount_configs表");
+        } catch (Exception e) {
+            log.warn("创建member_discount_configs表失败: {}", e.getMessage());
+        }
+
         log.info("数据库迁移完成");
 
         // ==================== 健康维度 Phase 1: health_dimension_score / health_data_source_record / health_norm_reference ====================

+ 24 - 3
cfc-backend/src/main/java/com/etotem/cfc/dto/ActivityDTO.java

@@ -24,10 +24,14 @@ public class ActivityDTO {
     private String priceLabel;
 
     public static ActivityDTO from(Activity activity) {
-        return from(activity, false);
+        return from(activity, false, null);
     }
 
     public static ActivityDTO from(Activity activity, boolean isGuest) {
+        return from(activity, isGuest, null);
+    }
+
+    public static ActivityDTO from(Activity activity, boolean isGuest, String memberLevel) {
         ActivityDTO dto = new ActivityDTO();
         dto.setId(activity.getId());
         dto.setTitle(activity.getTitle());
@@ -47,8 +51,10 @@ public class ActivityDTO {
             dto.setPrice(null);
             dto.setPriceLabel("登录查看价格");
         } else {
-            dto.setPrice(activity.getPrice());
-            if (activity.getPrice() != null && activity.getPrice() == 0) {
+            // Member-aware pricing: resolve final price based on membership level
+            Integer resolvedPrice = resolveMemberPrice(activity.getPrice(), activity.getMemberPrice(), memberLevel);
+            dto.setPrice(resolvedPrice);
+            if (resolvedPrice != null && resolvedPrice == 0) {
                 dto.setPriceLabel("免费");
             } else {
                 dto.setPriceLabel(null);
@@ -56,4 +62,19 @@ public class ActivityDTO {
         }
         return dto;
     }
+
+    /**
+     * Resolve final price based on membership level.
+     * Members (FAMILY/PROVIDER) get memberPrice if available, otherwise base price.
+     */
+    public static Integer resolveMemberPrice(Integer basePrice, Integer memberPrice, String memberLevel) {
+        if (basePrice == null) return null;
+        // If user has member level and a memberPrice is set, use memberPrice
+        if (memberLevel != null && ("FAMILY".equals(memberLevel) || "PROVIDER".equals(memberLevel))) {
+            if (memberPrice != null) {
+                return memberPrice;
+            }
+        }
+        return basePrice;
+    }
 }

+ 13 - 2
cfc-backend/src/main/java/com/etotem/cfc/dto/ProductDTO.java

@@ -33,10 +33,14 @@ public class ProductDTO {
     private Date updatedAt;
 
     public static ProductDTO from(Product p) {
-        return from(p, false);
+        return from(p, false, null);
     }
 
     public static ProductDTO from(Product p, boolean isGuest) {
+        return from(p, isGuest, null);
+    }
+
+    public static ProductDTO from(Product p, boolean isGuest, String memberLevel) {
         if (p == null) return null;
         ProductDTO d = new ProductDTO();
         d.id = p.getId();
@@ -54,7 +58,6 @@ public class ProductDTO {
         } else {
             d.imageList = new ArrayList<>();
         }
-        d.price = p.getPrice();
         d.memberPrice = p.getMemberPrice();
         d.stock = p.getStock();
         d.salesCount = p.getSalesCount();
@@ -71,6 +74,14 @@ public class ProductDTO {
             d.price = null;
             d.memberPrice = null;
             d.priceLabel = "登录查看价格";
+        } else {
+            // Member-aware pricing: resolve final display price based on membership level
+            d.price = ActivityDTO.resolveMemberPrice(p.getPrice(), p.getMemberPrice(), memberLevel);
+            if (d.price != null && d.price == 0) {
+                d.priceLabel = "免费";
+            } else {
+                d.priceLabel = null;
+            }
         }
         return d;
     }

+ 3 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/Activity.java

@@ -42,6 +42,9 @@ public class Activity implements Serializable {
 
     private Integer price;
 
+    /** 会员价(分) */
+    private Integer memberPrice;
+
     /** 签到积分(0=使用系统默认) */
     private Integer checkinPoints;
 

+ 9 - 1
cfc-backend/src/main/java/com/etotem/cfc/service/ActivityService.java

@@ -47,6 +47,9 @@ public class ActivityService extends ServiceImpl<ActivityMapper, Activity> {
     @Resource
     private ActivityRegistrationService registrationService;
 
+    @Resource
+    private MembershipService membershipService;
+
     public Result<Map<String, Object>> list(String dimensionCode, Integer page, Integer size, Long userId) {
         LambdaQueryWrapper<Activity> query = new LambdaQueryWrapper<Activity>()
                 .eq(Activity::getStatus, "published")
@@ -62,8 +65,13 @@ public class ActivityService extends ServiceImpl<ActivityMapper, Activity> {
         }
         Page<Activity> pageResult = this.page(new Page<>(page, size), query);
         boolean isGuest = (userId == null);
+        String memberLevel = null;
+        if (!isGuest) {
+            try { memberLevel = membershipService.getMemberLevel(userId); } catch (Exception e) { /* ignore */ }
+        }
+        final String level = memberLevel;
         Map<String, Object> data = new HashMap<>();
-        data.put("records", pageResult.getRecords().stream().map(a -> ActivityDTO.from(a, isGuest)).collect(Collectors.toList()));
+        data.put("records", pageResult.getRecords().stream().map(a -> ActivityDTO.from(a, isGuest, level)).collect(Collectors.toList()));
         data.put("total", pageResult.getTotal());
         data.put("page", pageResult.getCurrent());
         data.put("size", pageResult.getSize());

+ 9 - 1
cfc-backend/src/main/java/com/etotem/cfc/service/ProductService.java

@@ -29,6 +29,9 @@ public class ProductService {
     @Resource
     private UserMapper userMapper;
 
+    @Resource
+    private MembershipService membershipService;
+
     public Result<Map<String, Object>> list(ProductListQueryDTO query, Long userId) {
         Page<Product> page = new Page<>(query.getPage(), query.getSize());
         LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<Product>()
@@ -49,8 +52,13 @@ public class ProductService {
         }
         IPage<Product> result = productMapper.selectPage(page, wrapper);
         boolean isGuest = (userId == null);
+        String memberLevel = null;
+        if (!isGuest) {
+            try { memberLevel = membershipService.getMemberLevel(userId); } catch (Exception e) { /* ignore */ }
+        }
+        final String level = memberLevel;
         List<ProductDTO> records = result.getRecords().stream()
-            .map(p -> ProductDTO.from(p, isGuest))
+            .map(p -> ProductDTO.from(p, isGuest, level))
             .collect(Collectors.toList());
         Map<String, Object> data = new HashMap<>();
         data.put("records", records);

+ 1 - 0
cfc-frontend/AGENTS.md

@@ -54,6 +54,7 @@ pages.json 中定义 5 个 TabBar 页面(TabBar 文案,非五维维度名)
 - **NEVER** 跳过 JWT 认证直接调用需登录接口
 - **NEVER** 在本地存储中保存敏感用户信息
 - **NEVER** 在 WXML/Vue 模板中使用可选链 `?.`,微信小程序不支持 → 使用 `&&` 代替(如 `currentWish?.title` 改为 `currentWish && currentWish.title`)
+- **NEVER** 在 `:class` 绑定中调用方法(如 `:class="getStatusClass(item)"`),微信小程序模板编译器不支持带参数的方法调用 → 改用内联表达式(如 `:class="'status-' + item.status"`)或计算属性
 
 ## UNIQUE FEATURES
 

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

@@ -3,7 +3,6 @@
     <view class="ai-float-avatar" :class="mascotCode">
       <text class="ai-float-icon">{{ mascotIcon }}</text>
     </view>
-    <view class="ai-float-pulse" v-if="showPulse"></view>
   </view>
 </template>
 
@@ -13,16 +12,12 @@ export default {
   data() {
     return {
       mascotCode: 'xibao',
-      mascotIcon: '🌟',
-      showPulse: true
+      mascotIcon: '🌟'
     }
   },
   mounted() {
     this.loadMascot()
   },
-  onShow() {
-    this.loadMascot()
-  },
   methods: {
     loadMascot() {
       var userInfo = uni.getStorageSync('userInfo')
@@ -106,24 +101,8 @@ export default {
   line-height: 1;
 }
 
-/* 脉冲动画 */
-.ai-float-pulse {
-  position: absolute;
-  width: 96rpx;
-  height: 96rpx;
-  border-radius: 50%;
-  background: rgba(249, 115, 22, 0.25);
-  animation: pulseRing 2s ease-out infinite;
-  z-index: 1;
-}
-
 @keyframes floatBounce {
   0%, 100% { transform: translateY(0); }
   50% { transform: translateY(-8rpx); }
 }
-
-@keyframes pulseRing {
-  0% { transform: scale(1); opacity: 0.6; }
-  100% { transform: scale(1.6); opacity: 0; }
-}
 </style>

+ 2 - 1
cfc-frontend/components/DimensionActivities.vue

@@ -17,7 +17,8 @@
           </view>
           <view class="activity-bottom">
             <text class="activity-status" :class="'status-' + (act.status || 'upcoming')">{{ statusText(act.status) }}</text>
-            <text class="activity-fee" v-if="act.priceLabel">{{ act.priceLabel }}</text>
+            <text class="activity-fee" v-if="!isLoggedIn">登录查看</text>
+            <text class="activity-fee" v-else-if="act.priceLabel">{{ act.priceLabel }}</text>
             <text class="activity-fee" v-else-if="act.price && act.price > 0">{{ formatPriceWithSymbol(act.price) }}</text>
             <text class="activity-fee fee-free" v-else>免费</text>
           </view>

+ 1 - 0
cfc-frontend/pages/action/index.vue

@@ -57,6 +57,7 @@
     <DimensionActivities
       dimensionCode="action"
       :activities="dimensionActivities"
+      :isLoggedIn="isLoggedIn"
       @activityClick="goActivityDetail"
       @moreActivities="goMoreActivities" />
 

+ 25 - 2
cfc-frontend/pages/activity/index.vue

@@ -62,8 +62,8 @@
               <text class="card-meta">{{ act.startTime }}</text>
               <text class="card-meta" v-if="act.location">📍 {{ act.location }}</text>
               <view class="card-bottom">
-                <text class="card-status" :class="getStatusClass(act)">
-                  {{ getStatusText(act) }}
+                <text class="card-status" :class="'status-' + (act._cls || 'upcoming')">
+                  {{ act._txt || '待开始' }}
                 </text>
                 <text class="card-price" v-if="act.price > 0">¥{{ formatPrice(act.price) }}</text>
                 <text class="card-price free" v-else>免费</text>
@@ -343,6 +343,9 @@ export default {
             }
             records = filtered
           }
+          for (var di = 0; di < records.length; di++) {
+            self.decorateActivity(records[di])
+          }
           if (refresh) {
             self.myActivities = records
           } else {
@@ -519,6 +522,26 @@ export default {
     formatPrice: function(price) {
       return (price / 100).toFixed(2)
     },
+    // Pre-compute display properties for WXML compatibility (no method calls in :class)
+    decorateActivity: function(act) {
+      if (act.status) {
+        act._cls = act.status
+        var map = { pending: '待审核', approved: '报名成功', rejected: '已拒绝', cancelled: '已取消', checked_in: '已签到' }
+        act._txt = map[act.status] || act.status
+      } else {
+        if (act.endTime) {
+          var endTime = new Date(act.endTime.replace(/-/g, '/'))
+          if (endTime < new Date()) {
+            act._cls = 'ended'
+            act._txt = '已结束'
+            return act
+          }
+        }
+        act._cls = 'upcoming'
+        act._txt = '待开始'
+      }
+      return act
+    },
     onCheckin: function(act) {
       var self = this
       var childId = uni.getStorageSync('currentChildId')

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

@@ -146,6 +146,7 @@
     <DimensionActivities
       dimensionCode="body"
       :activities="dimensionActivities"
+      :isLoggedIn="isLoggedIn"
       @activityClick="goActivityDetail"
       @moreActivities="goMoreActivities" />
 
@@ -188,6 +189,7 @@
 
     <!-- 底部占位 -->
     <view class="bottom-spacer"></view>
+    <AIFloatingAvatar />
   </view>
 </template>
 
@@ -204,6 +206,7 @@ import DimensionProducts from '../../components/DimensionProducts.vue'
 import DimensionArticles from '../../components/DimensionArticles.vue'
 import FamilyRelationGraph from '../../components/FamilyRelationGraph.vue'
 import RadarChart from '../../components/RadarChart.vue'
+import AIFloatingAvatar from '../../components/AIFloatingAvatar.vue'
 import { getVisibleSections, getEnergyOverview, getChildren, getTodayTasksByCategory, getActivityList, getProductsByDomain, getFamilyEnergySandbox, getVisibleFamilyMembers, getDimensionOverview, getFeaturedArticles } from '../../utils/api.js'
 import config from '../../config.js'
 
@@ -235,7 +238,7 @@ const healthRequest = function(url, data) {
 }
 
 export default {
-  components: { TabTransition, PageBanner, LoginGuideCard, HealthTips, FamilyEnergyBar, UserQuickEntry, DimensionTasks, DimensionActivities, DimensionProducts, DimensionArticles, FamilyRelationGraph, RadarChart },
+  components: { TabTransition, PageBanner, LoginGuideCard, HealthTips, FamilyEnergyBar, UserQuickEntry, DimensionTasks, DimensionActivities, DimensionProducts, DimensionArticles, FamilyRelationGraph, RadarChart, AIFloatingAvatar },
   data() {
     return {
       isLoggedIn: false,

+ 2 - 2
cfc-frontend/pages/guide/activities/detail.vue

@@ -124,7 +124,7 @@
 </template>
 
 <script>
-import { getActivityDetail, createActivity, updateActivity, publishActivity } from '../../../utils/api.js'
+import { getGuideActivityDetail, createActivity, updateActivity, publishActivity } from '../../../utils/api.js'
 
 export default {
   data() {
@@ -169,7 +169,7 @@ export default {
     loadDetail() {
       var self = this
       self.loading = true
-      getActivityDetail(self.activityId).then(function(res) {
+      getGuideActivityDetail(self.activityId).then(function(res) {
         self.loading = false
         if (res.code === 200 && res.data) {
           self.form = res.data

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

@@ -189,6 +189,7 @@
         <text>加载中...</text>
       </view>
     </transition>
+    <AIFloatingAvatar />
   </view>
 </view>
 </template>
@@ -200,6 +201,7 @@ import ChildIndex from './child-index.vue'
 import PageBanner from '../../components/PageBanner.vue'
 import WuxingSandbox from '../../components/wuxing-sandbox.vue'
 import TabTransition from '../../components/tab-transition.vue'
+import AIFloatingAvatar from '../../components/AIFloatingAvatar.vue'
 import { acceptParentInvite, productList, getActivityList, getFeaturedArticles } from '../../utils/api.js'
 
 var DIMENSIONS = [
@@ -216,7 +218,8 @@ export default {
     ChildIndex,
     PageBanner,
     WuxingSandbox,
-    TabTransition
+    TabTransition,
+    AIFloatingAvatar
   },
   data() {
     // 从 storage 预取登录态/角色,确保首次渲染即正确(WeChat 渲染层/逻辑层分离架构下 onLoad 执行前已渲染)

+ 0 - 65
cfc-frontend/pages/index/parent-index.vue

@@ -101,20 +101,6 @@
         </view>
       </view>
 
-      <!-- ===== AI 家庭助手入口 ===== -->
-      <view class="ai-entry-section animate-fade-in animate-stagger-6">
-        <PlayfulCard variant="flat" shadow="sm" :clickable="true" @click="goToAIChat" class="ai-entry-card" padding="24rpx">
-          <view class="ai-entry">
-            <view class="ai-entry-icon">🤖</view>
-            <view class="ai-entry-body">
-              <text class="ai-entry-title">AI 家庭助手</text>
-              <text class="ai-entry-desc">问问孩子的表现,了解家庭情况</text>
-            </view>
-            <text class="ai-entry-arrow">›</text>
-          </view>
-        </PlayfulCard>
-      </view>
-
       <!-- ===== d) 今日重点 ===== -->
       <view class="today-section animate-fade-in animate-stagger-7">
         <view class="section-title">今日重点</view>
@@ -758,8 +744,6 @@ export default {
         self.errorProducts = true
       })
     },
-    goToAIChat() { uni.navigateTo({ url: '/pages/ai/chat' }) },
-
     goToTaskList() { uni.switchTab({ url: '/pages/tasks/tasks' }) },
     manageChildren() { uni.navigateTo({ url: '/pages/profile/children' }) },
     inviteFriend() { uni.navigateTo({ url: '/pages/parent/invite/index' }) },
@@ -934,55 +918,6 @@ export default {
   color: var(--muted, #94A3B8);
 }
 
-/* AI 家庭助手入口 */
-.ai-entry-section {
-  margin-bottom: 48rpx;
-}
-
-.ai-entry-card .PlayfulCard-content {
-  padding: 24rpx;
-}
-
-.ai-entry {
-  display: flex;
-  align-items: center;
-}
-
-.ai-entry-icon {
-  width: 64rpx;
-  height: 64rpx;
-  font-size: 36rpx;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  flex-shrink: 0;
-}
-
-.ai-entry-body {
-  flex: 1;
-  margin: 0 16rpx;
-  min-width: 0;
-}
-
-.ai-entry-title {
-  font-size: 26rpx;
-  font-weight: 600;
-  color: var(--text, #1E293B);
-  display: block;
-}
-
-.ai-entry-desc {
-  font-size: 22rpx;
-  color: var(--text-secondary, #64748B);
-  margin-top: 4rpx;
-  display: block;
-}
-
-.ai-entry-arrow {
-  font-size: 32rpx;
-  color: var(--muted, #94A3B8);
-}
-
 /* ========================================
    d) 今日重点
    ======================================== */

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

@@ -230,6 +230,7 @@
     <DimensionActivities
       :dimensionCode="currentDimensionCode"
       :activities="dimensionActivities"
+      :isLoggedIn="isLoggedIn"
       @activityClick="goActivityDetail"
       @moreActivities="goMoreActivities" />
 
@@ -266,6 +267,7 @@
 
     <!-- 底部占位 -->
     <view class="bottom-spacer"></view>
+    <AIFloatingAvatar />
   </view>
 </template>
 
@@ -282,11 +284,12 @@ import DimensionProducts from '../../components/DimensionProducts.vue'
 import DimensionArticles from '../../components/DimensionArticles.vue'
 import FamilyRelationGraph from '../../components/FamilyRelationGraph.vue'
 import PsychCrisisBanner from '../../components/PsychCrisisBanner.vue'
+import AIFloatingAvatar from '../../components/AIFloatingAvatar.vue'
 import { getVisibleSections, getFeaturedArticles, getEmiReport, getDailyTip, getFamilyEnergySandbox, getTodayTasksByCategory, getActivityList, getProductsByDomain, getChildren, getVisibleFamilyMembers, getEnergyOverview } from '../../utils/api.js'
 import config from '../../config.js'
 
 export default {
-  components: { TabTransition, PageBanner, RadarChart, LoginGuideCard, FamilyEnergyBar, UserQuickEntry, DimensionTasks, DimensionActivities, DimensionProducts, DimensionArticles, FamilyRelationGraph, PsychCrisisBanner },
+  components: { TabTransition, PageBanner, RadarChart, LoginGuideCard, FamilyEnergyBar, UserQuickEntry, DimensionTasks, DimensionActivities, DimensionProducts, DimensionArticles, FamilyRelationGraph, PsychCrisisBanner, AIFloatingAvatar },
   data() {
     return {
       isLoggedIn: false,

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

@@ -49,6 +49,7 @@
       <!-- 菜单列表 -->
       <ProfileMenu :role="role" @invite-generate="onInviteGenerate" />
     </template>
+    <AIFloatingAvatar />
   </view>
 </template>
 
@@ -60,6 +61,7 @@ import ProfileStats from './components/ProfileStats.vue'
 import ProfileBadges from './components/ProfileBadges.vue'
 import ProfileGrowth from './components/ProfileGrowth.vue'
 import ProfileMenu from './components/ProfileMenu.vue'
+import AIFloatingAvatar from '../../components/AIFloatingAvatar.vue'
 
 export default {
   components: {
@@ -69,7 +71,8 @@ export default {
     ProfileStats,
     ProfileBadges,
     ProfileGrowth,
-    ProfileMenu
+    ProfileMenu,
+    AIFloatingAvatar
   },
   data() {
     return {

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

@@ -85,6 +85,7 @@
     <DimensionActivities
       dimensionCode="wisdom"
       :activities="dimensionActivities"
+      :isLoggedIn="isLoggedIn"
       @activityClick="goActivityDetail"
       @moreActivities="goMoreActivities" />
 
@@ -139,6 +140,7 @@
 
     <!-- 底部占位 -->
     <view class="bottom-spacer"></view>
+    <AIFloatingAvatar />
   </view>
 </template>
 
@@ -154,10 +156,11 @@ import DimensionActivities from '../../components/DimensionActivities.vue'
 import DimensionProducts from '../../components/DimensionProducts.vue'
 import DimensionArticles from '../../components/DimensionArticles.vue'
 import FamilyRelationGraph from '../../components/FamilyRelationGraph.vue'
+import AIFloatingAvatar from '../../components/AIFloatingAvatar.vue'
 import { getVisibleSections, getChildren, getTodayTasksByCategory, getActivityList, getProductsByDomain, getFamilyEnergySandbox, getAssessmentLatestResult, getFeaturedArticles } from '../../utils/api.js'
 
 export default {
-  components: { TabTransition, PageBanner, LoginGuideCard, RadarChart, FamilyEnergyBar, UserQuickEntry, DimensionTasks, DimensionActivities, DimensionProducts, DimensionArticles, FamilyRelationGraph },
+  components: { TabTransition, PageBanner, LoginGuideCard, RadarChart, FamilyEnergyBar, UserQuickEntry, DimensionTasks, DimensionActivities, DimensionProducts, DimensionArticles, FamilyRelationGraph, AIFloatingAvatar },
   data() {
     return {
       isLoggedIn: false,

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

@@ -632,11 +632,11 @@ export const deleteTrainingPlan = (planId) => {
 }
 
 // ===== 活动管理 =====
-export const getActivityList = (status) => {
+export const getGuideActivityList = (status) => {
   return request('/api/guide/activities/list', 'POST', status ? { status } : {})
 }
 
-export const getActivityDetail = (activityId) => {
+export const getGuideActivityDetail = (activityId) => {
   return request('/api/guide/activities/detail', 'POST', { activityId })
 }
 

+ 8 - 5
cfc-web/src/views/Layout.vue

@@ -315,6 +315,10 @@ export default {
       return this.displayRoles
     }
   },
+  mounted() {
+    // 清除可能的 beforeunload 处理,防止页面刷新弹出"离开网站?"提示
+    window.onbeforeunload = null
+  },
   methods: {
     hasPerm(required) {
       return hasPermission(this.effectivePerms, required)
@@ -426,10 +430,13 @@ export default {
       this.$message.success('已复制邀请码')
     },
     handleLogout() {
+      // 不使用 modal 遮罩(全局 .v-modal { display:none } 会与 $confirm 的遮罩冲突)
       this.$confirm('确定要退出登录吗?', '提示', {
         confirmButtonText: '确定',
         cancelButtonText: '取消',
-        type: 'warning'
+        type: 'warning',
+        modal: false,
+        customClass: 'cfc-logout-confirm'
       }).then(() => {
         localStorage.removeItem('token')
         localStorage.removeItem('role')
@@ -448,10 +455,6 @@ export default {
 .v-modal {
   display: none !important;
 }
-/* 保留 $confirm / MessageBox 的遮罩 */
-.el-message-box .v-modal {
-  display: block !important;
-}
 
 .layout-container {
   height: 100vh;