All checks were successful
continuous-integration/drone/push Build is passing
- 添加环境变量配置 VITE_FEATURE_DUO_PRACTICE 等 - env.ts 新增 isFeatureEnabled 方法 - 菜单根据功能开关动态显示/隐藏 - 路由守卫拦截未启用功能的直接访问 - 开发环境默认开启双人对练,生产环境默认关闭
317 lines
7.6 KiB
TypeScript
317 lines
7.6 KiB
TypeScript
/**
|
||
* 路由守卫
|
||
* 处理路由权限验证、登录状态检查等
|
||
*/
|
||
|
||
import { Router, RouteLocationNormalized, NavigationGuardNext } from 'vue-router'
|
||
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']
|
||
|
||
// 需要特殊权限的路由映射
|
||
const ROUTE_PERMISSIONS: Record<string, string[]> = {
|
||
'/admin': ['admin'],
|
||
'/manager': ['manager', 'admin'],
|
||
'/analysis': ['manager', 'admin']
|
||
}
|
||
|
||
/**
|
||
* 设置路由守卫
|
||
*/
|
||
export function setupRouterGuard(router: Router) {
|
||
// 全局前置守卫
|
||
router.beforeEach(async (to, from, next) => {
|
||
// 显示页面加载状态
|
||
loadingManager.start('page-loading', {
|
||
message: '页面加载中...',
|
||
background: 'rgba(255, 255, 255, 0.8)'
|
||
})
|
||
|
||
try {
|
||
await handleRouteGuard(to, from, next)
|
||
} catch (error) {
|
||
console.error('Route guard error:', error)
|
||
ElMessage.error('页面加载失败,请重试')
|
||
next('/404')
|
||
}
|
||
})
|
||
|
||
// 全局后置守卫
|
||
router.afterEach((to, from) => {
|
||
// 隐藏页面加载状态
|
||
loadingManager.stop('page-loading')
|
||
|
||
// 设置页面标题
|
||
document.title = getPageTitle(to.meta?.title as string)
|
||
|
||
// 记录页面访问日志
|
||
if (process.env.NODE_ENV === 'development') {
|
||
console.log(`[Router] Navigate from ${from.path} to ${to.path}`)
|
||
}
|
||
})
|
||
|
||
// 路由错误处理
|
||
router.onError((error) => {
|
||
console.error('Router error:', error)
|
||
ElMessage.error('路由加载失败')
|
||
loadingManager.stop('page-loading')
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 处理路由守卫逻辑
|
||
*/
|
||
async function handleRouteGuard(
|
||
to: RouteLocationNormalized,
|
||
_from: RouteLocationNormalized,
|
||
next: NavigationGuardNext
|
||
) {
|
||
const { path } = to
|
||
|
||
// 白名单路由直接通过
|
||
if (WHITE_LIST.includes(path)) {
|
||
// 如果已登录用户访问登录页,重定向到首页
|
||
if (path === '/login' && authManager.isAuthenticated()) {
|
||
next(authManager.getDefaultRoute())
|
||
return
|
||
}
|
||
next()
|
||
return
|
||
}
|
||
|
||
// 检查登录状态
|
||
if (!authManager.isAuthenticated()) {
|
||
ElMessage.warning('请先登录')
|
||
next(`/login?redirect=${encodeURIComponent(path)}`)
|
||
return
|
||
}
|
||
|
||
// 检查token是否过期
|
||
if (authManager.isTokenExpired()) {
|
||
try {
|
||
// 尝试刷新token
|
||
await authManager.refreshToken()
|
||
} catch (error) {
|
||
ElMessage.error('登录已过期,请重新登录')
|
||
next(`/login?redirect=${encodeURIComponent(path)}`)
|
||
return
|
||
}
|
||
}
|
||
|
||
// 检查功能开关
|
||
const feature = to.meta?.feature as string | undefined
|
||
if (feature && !env.isFeatureEnabled(feature)) {
|
||
ElMessage.warning('此功能暂未开放')
|
||
next(authManager.getDefaultRoute())
|
||
return
|
||
}
|
||
|
||
// 检查路由权限
|
||
if (!checkRoutePermission(path)) {
|
||
ElMessage.error('您没有访问此页面的权限')
|
||
// 根据用户角色重定向到合适的页面
|
||
next(authManager.getDefaultRoute())
|
||
return
|
||
}
|
||
|
||
// 检查特殊路由规则(先进行同步检查)
|
||
if (!checkSpecialRouteRules(to)) {
|
||
ElMessage.error('访问被拒绝')
|
||
next(authManager.getDefaultRoute())
|
||
return
|
||
}
|
||
|
||
// 异步权限检查(团队和课程权限)
|
||
const hasSpecialAccess = await checkSpecialRouteRulesAsync(to)
|
||
if (!hasSpecialAccess) {
|
||
ElMessage.error('您没有访问此资源的权限')
|
||
next(authManager.getDefaultRoute())
|
||
return
|
||
}
|
||
|
||
next()
|
||
}
|
||
|
||
/**
|
||
* 检查路由权限
|
||
*/
|
||
function checkRoutePermission(path: string): boolean {
|
||
// 检查是否可以访问路由
|
||
if (!authManager.canAccessRoute(path)) {
|
||
return false
|
||
}
|
||
|
||
// 检查特定路由的权限要求
|
||
for (const [routePrefix, roles] of Object.entries(ROUTE_PERMISSIONS)) {
|
||
if (path.startsWith(routePrefix)) {
|
||
const userRole = authManager.getUserRole()
|
||
if (!userRole || !roles.includes(userRole)) {
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
|
||
return true
|
||
}
|
||
|
||
/**
|
||
* 检查特殊路由规则(异步版本)
|
||
*/
|
||
async function checkSpecialRouteRulesAsync(to: RouteLocationNormalized): Promise<boolean> {
|
||
const { path, params } = to
|
||
|
||
// 检查用户ID参数权限(只能访问自己的数据,管理员除外)
|
||
if (params.userId && !authManager.isAdmin()) {
|
||
const currentUser = authManager.getCurrentUser()
|
||
if (currentUser && String(params.userId) !== String(currentUser.id)) {
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 检查团队ID参数权限
|
||
if (params.teamId && !authManager.isAdmin()) {
|
||
const teamId = Number(params.teamId)
|
||
if (!isNaN(teamId)) {
|
||
const isMember = await checkTeamMembership(teamId)
|
||
if (!isMember) {
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
|
||
// 检查课程访问权限
|
||
if (path.includes('/course/') && params.courseId) {
|
||
const courseId = Number(params.courseId)
|
||
if (!isNaN(courseId)) {
|
||
const hasAccess = await checkCourseAccess(courseId)
|
||
if (!hasAccess) {
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
|
||
return true
|
||
}
|
||
|
||
/**
|
||
* 检查特殊路由规则(同步版本,用于简单检查)
|
||
*/
|
||
function checkSpecialRouteRules(to: RouteLocationNormalized): boolean {
|
||
const { params } = to
|
||
|
||
// 检查用户ID参数权限(只能访问自己的数据,管理员除外)
|
||
if (params.userId && !authManager.isAdmin()) {
|
||
const currentUser = authManager.getCurrentUser()
|
||
if (currentUser && String(params.userId) !== String(currentUser.id)) {
|
||
return false
|
||
}
|
||
}
|
||
|
||
return true
|
||
}
|
||
|
||
/**
|
||
* 获取页面标题
|
||
*/
|
||
function getPageTitle(title?: string): string {
|
||
const appTitle = '考培练系统'
|
||
return title ? `${title} - ${appTitle}` : appTitle
|
||
}
|
||
|
||
/**
|
||
* 动态添加路由(用于角色权限路由)
|
||
*/
|
||
export function addDynamicRoutes(router: Router) {
|
||
const userRole = authManager.getUserRole()
|
||
if (!userRole) return
|
||
|
||
// 根据角色动态添加路由
|
||
const dynamicRoutes = getDynamicRoutesByRole(userRole)
|
||
|
||
dynamicRoutes.forEach(route => {
|
||
router.addRoute(route)
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 根据角色获取动态路由
|
||
*/
|
||
function getDynamicRoutesByRole(role: string) {
|
||
const routes: any[] = []
|
||
|
||
// 根据角色添加不同的路由
|
||
switch (role) {
|
||
case 'admin':
|
||
// 管理员可以访问所有路由
|
||
break
|
||
case 'manager':
|
||
// 管理者路由
|
||
break
|
||
case 'trainee':
|
||
// 学员路由
|
||
break
|
||
}
|
||
|
||
return routes
|
||
}
|
||
|
||
/**
|
||
* 检查页面权限(组件内使用)
|
||
*/
|
||
export function checkPagePermission(permissions: string[]): boolean {
|
||
if (!permissions || permissions.length === 0) {
|
||
return true
|
||
}
|
||
|
||
const userRole = authManager.getUserRole()
|
||
if (!userRole) return false
|
||
|
||
return permissions.includes(userRole) || authManager.isAdmin()
|
||
}
|
||
|
||
/**
|
||
* 权限指令(v-permission)
|
||
*/
|
||
export const permissionDirective = {
|
||
mounted(el: HTMLElement, binding: any) {
|
||
const { value } = binding
|
||
|
||
if (value && !checkPagePermission(Array.isArray(value) ? value : [value])) {
|
||
el.style.display = 'none'
|
||
}
|
||
},
|
||
|
||
updated(el: HTMLElement, binding: any) {
|
||
const { value } = binding
|
||
|
||
if (value && !checkPagePermission(Array.isArray(value) ? value : [value])) {
|
||
el.style.display = 'none'
|
||
} else {
|
||
el.style.display = ''
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 路由元信息接口扩展
|
||
*/
|
||
declare module 'vue-router' {
|
||
interface RouteMeta {
|
||
title?: string
|
||
icon?: string
|
||
hidden?: boolean
|
||
roles?: string[]
|
||
permissions?: string[]
|
||
keepAlive?: boolean
|
||
affix?: boolean
|
||
breadcrumb?: boolean
|
||
activeMenu?: string
|
||
feature?: string // 功能开关标识
|
||
}
|
||
}
|