- 添加环境变量配置 VITE_FEATURE_DUO_PRACTICE 等 - env.ts 新增 isFeatureEnabled 方法 - 菜单根据功能开关动态显示/隐藏 - 路由守卫拦截未启用功能的直接访问 - 开发环境默认开启双人对练,生产环境默认关闭
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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天
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 // 功能开关标识
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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' }
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user