/** * 权限验证相关工具函数 */ import type { RouteLocationNormalized } from 'vue-router' import type { UserInfo } from '@/types/api' import { useUserStore } from '@/store/modules/user' /** * 不需要登录的路由白名单 */ export const LOGIN_WHITE_LIST = [ '/auth/login', '/auth/register', '/auth/forget-password', '/exception/403', '/exception/404', '/exception/500' ] /** * 检查路由是否在白名单中 */ export const isInWhiteList = (path: string): boolean => { return LOGIN_WHITE_LIST.some((whitePath) => { if (whitePath.endsWith('*')) { // 支持通配符匹配 const prefix = whitePath.slice(0, -1) return path.startsWith(prefix) } return path === whitePath }) } /** * 检查用户是否有权限访问路由 * 根据文档要求:菜单访问基于 URL,按钮访问基于权限标识 */ export const hasRoutePermission = ( route: RouteLocationNormalized, userInfo: Partial ): boolean => { const { roles = [], permissions = [] } = userInfo // 如果是超级管理员,直接通过 if (userInfo.user_type === 1) { return true } // 默认始终可访问的路由(不需要菜单权限) const defaultAllowedPaths = ['/dashboard'] const isDefaultRoute = defaultAllowedPaths.some( (allowedPath) => route.path === allowedPath || route.path.startsWith(allowedPath + '/') ) // 检查菜单访问权限(基于 URL) const userStore = useUserStore() const userMenus = userStore.menus || [] // 如果是默认路由,跳过菜单权限检查 if (!isDefaultRoute && userMenus.length > 0) { const hasMenuAccess = checkMenuAccess(route.path, userMenus) if (!hasMenuAccess) { return false } } // 如果路由没有设置额外的权限要求,直接通过 if (!route.meta?.roles && !route.meta?.permissions) { return true } // 检查角色权限 if (route.meta.roles) { const routeRoles = route.meta.roles as string[] const hasRole = routeRoles.some((role) => roles.includes(role as any)) if (!hasRole) { return false } } // 检查操作权限 if (route.meta.permissions) { const routePermissions = route.meta.permissions as string[] const hasPermission = routePermissions.some((permission) => { if (permissions.includes('*:*:*')) { return true } return permissions.some((userPermission) => { if (userPermission.endsWith('*')) { const prefix = userPermission.slice(0, -1) return permission.startsWith(prefix) } return userPermission === permission }) }) if (!hasPermission) { return false } } return true } /** * 递归检查菜单访问权限(基于 URL) */ function checkMenuAccess(path: string, menus: any[]): boolean { for (const menu of menus) { // 检查当前菜单的 URL 是否匹配(支持动态参数) if (menu.url && matchMenuPath(path, menu.url)) { console.log(`[菜单权限检查] 路径 ${path} 匹配菜单 ${menu.url}`) return true } // 递归检查子菜单 if (menu.children && menu.children.length > 0) { if (checkMenuAccess(path, menu.children)) { return true } } } console.log(`[菜单权限检查] 路径 ${path} 未找到匹配的菜单`) return false } /** * 匹配菜单路径,支持动态参数 * @param actualPath 实际访问路径,如 /account-management/enterprise-customer/customer-accounts/3000 * @param menuPath 菜单定义路径,如 /account-management/enterprise-customer/customer-accounts/:id */ function matchMenuPath(actualPath: string, menuPath: string): boolean { // 移除查询参数 const cleanActualPath = actualPath.split('?')[0] const cleanMenuPath = menuPath.split('?')[0] // 如果完全匹配,直接返回 if (cleanActualPath === cleanMenuPath) { return true } // 将路径分割成段 const actualSegments = cleanActualPath.split('/').filter(Boolean) const menuSegments = cleanMenuPath.split('/').filter(Boolean) // 段数必须相同 if (actualSegments.length !== menuSegments.length) { return false } // 逐段比较 for (let i = 0; i < menuSegments.length; i++) { const menuSegment = menuSegments[i] const actualSegment = actualSegments[i] // 如果是动态参数(以 : 开头),跳过比较 if (menuSegment.startsWith(':')) { continue } // 如果不是动态参数,必须完全匹配 if (menuSegment !== actualSegment) { return false } } return true } /** * 检查 Token 是否有效 * 简单检查,真实项目中应该验证 JWT 或者调用后端接口 */ export const isTokenValid = (token: string): boolean => { if (!token) return false // Mock Token 格式: mock_token_{key}_{timestamp} if (token.startsWith('mock_token_')) { // Mock Token 永不过期(开发环境) return true } // 真实 Token 可以在这里添加 JWT 解析和过期检查 // 例如: // try { // const decoded = jwt_decode(token) // const isExpired = decoded.exp * 1000 < Date.now() // return !isExpired // } catch { // return false // } return true } /** * 获取重定向路径 * 登录后跳转到之前访问的页面 */ export const getRedirectPath = (route: RouteLocationNormalized): string => { const redirect = route.query.redirect as string if (redirect && !isInWhiteList(redirect)) { return redirect } return '/' } /** * 构建登录重定向 URL */ export const buildLoginRedirect = (currentPath: string): string => { if (isInWhiteList(currentPath)) { return '/auth/login' } return `/auth/login?redirect=${encodeURIComponent(currentPath)}` }