feat: 添加功能开关机制
All checks were successful
continuous-integration/drone/push Build is passing

- 添加环境变量配置 VITE_FEATURE_DUO_PRACTICE 等
- env.ts 新增 isFeatureEnabled 方法
- 菜单根据功能开关动态显示/隐藏
- 路由守卫拦截未启用功能的直接访问
- 开发环境默认开启双人对练,生产环境默认关闭
This commit is contained in:
yuliang_guo
2026-01-31 14:26:52 +08:00
parent d2e6abfc80
commit 8500308919
6 changed files with 52 additions and 7 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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<string, boolean> = {
'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天

View File

@@ -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) => {

View File

@@ -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 // 功能开关标识
}
}

View File

@@ -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' }
}
]
},