- 添加环境变量配置 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_ERROR_REPORTING=true
|
||||||
VITE_ENABLE_ANALYTICS=false
|
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_JWT_EXPIRE_TIME=86400
|
||||||
VITE_REFRESH_TOKEN_EXPIRE_TIME=604800
|
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_USE_MOCK_DATA=false
|
||||||
VITE_ENABLE_REQUEST_LOG=false
|
VITE_ENABLE_REQUEST_LOG=false
|
||||||
NODE_ENV=production
|
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_ERROR_REPORTING = import.meta.env.VITE_ENABLE_ERROR_REPORTING === 'true'
|
||||||
public readonly ENABLE_ANALYTICS = import.meta.env.VITE_ENABLE_ANALYTICS === '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 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天
|
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 { ElMessage } from 'element-plus'
|
||||||
import { authManager } from '@/utils/auth'
|
import { authManager } from '@/utils/auth'
|
||||||
import NotificationBell from '@/components/NotificationBell.vue'
|
import NotificationBell from '@/components/NotificationBell.vue'
|
||||||
|
import { env } from '@/config/env'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -261,7 +262,8 @@ const menuConfig = [
|
|||||||
{
|
{
|
||||||
path: '/trainee/duo-practice',
|
path: '/trainee/duo-practice',
|
||||||
title: '双人对练',
|
title: '双人对练',
|
||||||
icon: 'Connection'
|
icon: 'Connection',
|
||||||
|
feature: 'duo-practice' // 功能开关标识
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -377,9 +379,15 @@ const menuConfig = [
|
|||||||
|
|
||||||
// 获取菜单路由
|
// 获取菜单路由
|
||||||
const menuRoutes = computed(() => {
|
const menuRoutes = computed(() => {
|
||||||
// 仅保留当前用户可访问的菜单项
|
// 仅保留当前用户可访问的菜单项和启用的功能
|
||||||
const filterChildren = (children: any[] = []) =>
|
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
|
return menuConfig
|
||||||
.map((route: any) => {
|
.map((route: any) => {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { ElMessage } from 'element-plus'
|
|||||||
import { authManager } from '@/utils/auth'
|
import { authManager } from '@/utils/auth'
|
||||||
import { loadingManager } from '@/utils/loadingManager'
|
import { loadingManager } from '@/utils/loadingManager'
|
||||||
import { checkTeamMembership, checkCourseAccess, clearPermissionCache } from '@/utils/permissionChecker'
|
import { checkTeamMembership, checkCourseAccess, clearPermissionCache } from '@/utils/permissionChecker'
|
||||||
|
import { env } from '@/config/env'
|
||||||
|
|
||||||
// 白名单路由(不需要登录)
|
// 白名单路由(不需要登录)
|
||||||
const WHITE_LIST = ['/login', '/register', '/404']
|
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)) {
|
if (!checkRoutePermission(path)) {
|
||||||
ElMessage.error('您没有访问此页面的权限')
|
ElMessage.error('您没有访问此页面的权限')
|
||||||
@@ -302,5 +311,6 @@ declare module 'vue-router' {
|
|||||||
affix?: boolean
|
affix?: boolean
|
||||||
breadcrumb?: boolean
|
breadcrumb?: boolean
|
||||||
activeMenu?: string
|
activeMenu?: string
|
||||||
|
feature?: string // 功能开关标识
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,25 +137,25 @@ const routes: RouteRecordRaw[] = [
|
|||||||
path: 'duo-practice',
|
path: 'duo-practice',
|
||||||
name: 'DuoPractice',
|
name: 'DuoPractice',
|
||||||
component: () => import('@/views/trainee/duo-practice.vue'),
|
component: () => import('@/views/trainee/duo-practice.vue'),
|
||||||
meta: { title: '双人对练', icon: 'Connection' }
|
meta: { title: '双人对练', icon: 'Connection', feature: 'duo-practice' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'duo-practice/room/:code',
|
path: 'duo-practice/room/:code',
|
||||||
name: 'DuoPracticeRoom',
|
name: 'DuoPracticeRoom',
|
||||||
component: () => import('@/views/trainee/duo-practice-room.vue'),
|
component: () => import('@/views/trainee/duo-practice-room.vue'),
|
||||||
meta: { title: '对练房间', hidden: true }
|
meta: { title: '对练房间', hidden: true, feature: 'duo-practice' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'duo-practice/join/:code',
|
path: 'duo-practice/join/:code',
|
||||||
name: 'DuoPracticeJoin',
|
name: 'DuoPracticeJoin',
|
||||||
component: () => import('@/views/trainee/duo-practice-room.vue'),
|
component: () => import('@/views/trainee/duo-practice-room.vue'),
|
||||||
meta: { title: '加入对练', hidden: true }
|
meta: { title: '加入对练', hidden: true, feature: 'duo-practice' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'duo-practice/report/:id',
|
path: 'duo-practice/report/:id',
|
||||||
name: 'DuoPracticeReport',
|
name: 'DuoPracticeReport',
|
||||||
component: () => import('@/views/trainee/duo-practice-report.vue'),
|
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