From 85003089195b9e57a8e957c2a33f0f498429efeb Mon Sep 17 00:00:00 2001 From: yuliang_guo Date: Sat, 31 Jan 2026 14:26:52 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=BC=80=E5=85=B3=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加环境变量配置 VITE_FEATURE_DUO_PRACTICE 等 - env.ts 新增 isFeatureEnabled 方法 - 菜单根据功能开关动态显示/隐藏 - 路由守卫拦截未启用功能的直接访问 - 开发环境默认开启双人对练,生产环境默认关闭 --- frontend/.env.development | 5 +++++ frontend/.env.production | 5 +++++ frontend/src/config/env.ts | 17 +++++++++++++++++ frontend/src/layout/index.vue | 14 +++++++++++--- frontend/src/router/guard.ts | 10 ++++++++++ frontend/src/router/index.ts | 8 ++++---- 6 files changed, 52 insertions(+), 7 deletions(-) diff --git a/frontend/.env.development b/frontend/.env.development index dd50180..d1731e4 100644 --- a/frontend/.env.development +++ b/frontend/.env.development @@ -25,6 +25,11 @@ VITE_ENABLE_DEVTOOLS=true VITE_ENABLE_ERROR_REPORTING=true VITE_ENABLE_ANALYTICS=false +# 实验性功能开关(开发环境默认开启) +VITE_FEATURE_DUO_PRACTICE=true +VITE_FEATURE_AI_PRACTICE=true +VITE_FEATURE_GROWTH_PATH=true + # 安全配置 VITE_JWT_EXPIRE_TIME=86400 VITE_REFRESH_TOKEN_EXPIRE_TIME=604800 diff --git a/frontend/.env.production b/frontend/.env.production index 72cd9f3..3ea0122 100644 --- a/frontend/.env.production +++ b/frontend/.env.production @@ -5,3 +5,8 @@ VITE_WS_BASE_URL=wss://aiedu.ireborn.com.cn VITE_USE_MOCK_DATA=false VITE_ENABLE_REQUEST_LOG=false NODE_ENV=production + +# 实验性功能开关(生产环境默认关闭未上线功能) +VITE_FEATURE_DUO_PRACTICE=false +VITE_FEATURE_AI_PRACTICE=true +VITE_FEATURE_GROWTH_PATH=true diff --git a/frontend/src/config/env.ts b/frontend/src/config/env.ts index 825780d..536f515 100644 --- a/frontend/src/config/env.ts +++ b/frontend/src/config/env.ts @@ -110,6 +110,23 @@ class EnvConfig { public readonly ENABLE_ERROR_REPORTING = import.meta.env.VITE_ENABLE_ERROR_REPORTING === 'true' public readonly ENABLE_ANALYTICS = import.meta.env.VITE_ENABLE_ANALYTICS === 'true' + // 实验性功能开关(Feature Flags) + public readonly FEATURE_DUO_PRACTICE = import.meta.env.VITE_FEATURE_DUO_PRACTICE === 'true' + public readonly FEATURE_AI_PRACTICE = import.meta.env.VITE_FEATURE_AI_PRACTICE !== 'false' // 默认开启 + public readonly FEATURE_GROWTH_PATH = import.meta.env.VITE_FEATURE_GROWTH_PATH !== 'false' // 默认开启 + + /** + * 检查功能是否启用 + */ + public isFeatureEnabled(feature: string): boolean { + const featureMap: Record = { + 'duo-practice': this.FEATURE_DUO_PRACTICE, + 'ai-practice': this.FEATURE_AI_PRACTICE, + 'growth-path': this.FEATURE_GROWTH_PATH + } + return featureMap[feature] ?? false + } + // 安全配置 public readonly JWT_EXPIRE_TIME = parseInt(import.meta.env.VITE_JWT_EXPIRE_TIME || '86400') // 24小时 public readonly REFRESH_TOKEN_EXPIRE_TIME = parseInt(import.meta.env.VITE_REFRESH_TOKEN_EXPIRE_TIME || '604800') // 7天 diff --git a/frontend/src/layout/index.vue b/frontend/src/layout/index.vue index fcf342f..7897b89 100644 --- a/frontend/src/layout/index.vue +++ b/frontend/src/layout/index.vue @@ -190,6 +190,7 @@ import { useRouter, useRoute } from 'vue-router' import { ElMessage } from 'element-plus' import { authManager } from '@/utils/auth' import NotificationBell from '@/components/NotificationBell.vue' +import { env } from '@/config/env' const router = useRouter() const route = useRoute() @@ -261,7 +262,8 @@ const menuConfig = [ { path: '/trainee/duo-practice', title: '双人对练', - icon: 'Connection' + icon: 'Connection', + feature: 'duo-practice' // 功能开关标识 } ] }, @@ -377,9 +379,15 @@ const menuConfig = [ // 获取菜单路由 const menuRoutes = computed(() => { - // 仅保留当前用户可访问的菜单项 + // 仅保留当前用户可访问的菜单项和启用的功能 const filterChildren = (children: any[] = []) => - children.filter((child: any) => authManager.canAccessRoute(child.path)) + children.filter((child: any) => { + // 检查权限 + if (!authManager.canAccessRoute(child.path)) return false + // 检查功能开关 + if (child.feature && !env.isFeatureEnabled(child.feature)) return false + return true + }) return menuConfig .map((route: any) => { diff --git a/frontend/src/router/guard.ts b/frontend/src/router/guard.ts index b9a5c11..4f8de50 100644 --- a/frontend/src/router/guard.ts +++ b/frontend/src/router/guard.ts @@ -8,6 +8,7 @@ import { ElMessage } from 'element-plus' import { authManager } from '@/utils/auth' import { loadingManager } from '@/utils/loadingManager' import { checkTeamMembership, checkCourseAccess, clearPermissionCache } from '@/utils/permissionChecker' +import { env } from '@/config/env' // 白名单路由(不需要登录) const WHITE_LIST = ['/login', '/register', '/404'] @@ -102,6 +103,14 @@ async function handleRouteGuard( } } + // 检查功能开关 + const feature = to.meta?.feature as string | undefined + if (feature && !env.isFeatureEnabled(feature)) { + ElMessage.warning('此功能暂未开放') + next(authManager.getDefaultRoute()) + return + } + // 检查路由权限 if (!checkRoutePermission(path)) { ElMessage.error('您没有访问此页面的权限') @@ -302,5 +311,6 @@ declare module 'vue-router' { affix?: boolean breadcrumb?: boolean activeMenu?: string + feature?: string // 功能开关标识 } } diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 901c5a4..dee9349 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -137,25 +137,25 @@ const routes: RouteRecordRaw[] = [ path: 'duo-practice', name: 'DuoPractice', component: () => import('@/views/trainee/duo-practice.vue'), - meta: { title: '双人对练', icon: 'Connection' } + meta: { title: '双人对练', icon: 'Connection', feature: 'duo-practice' } }, { path: 'duo-practice/room/:code', name: 'DuoPracticeRoom', component: () => import('@/views/trainee/duo-practice-room.vue'), - meta: { title: '对练房间', hidden: true } + meta: { title: '对练房间', hidden: true, feature: 'duo-practice' } }, { path: 'duo-practice/join/:code', name: 'DuoPracticeJoin', component: () => import('@/views/trainee/duo-practice-room.vue'), - meta: { title: '加入对练', hidden: true } + meta: { title: '加入对练', hidden: true, feature: 'duo-practice' } }, { path: 'duo-practice/report/:id', name: 'DuoPracticeReport', component: () => import('@/views/trainee/duo-practice-report.vue'), - meta: { title: '对练报告', hidden: true } + meta: { title: '对练报告', hidden: true, feature: 'duo-practice' } } ] },