/** * 路由全局前置守卫模块 * * 提供完整的路由导航守卫功能 * * ## 主要功能 * * - 登录状态验证和重定向 * - 动态路由注册和权限控制 * - 菜单数据获取和处理(前端/后端模式) * - 用户信息获取和缓存 * - 页面标题设置 * - 工作标签页管理 * - 进度条和加载动画控制 * - 静态路由识别和处理 * - 错误处理和异常跳转 * * ## 使用场景 * * - 路由跳转前的权限验证 * - 动态菜单加载和路由注册 * - 用户登录状态管理 * - 页面访问控制 * - 路由级别的加载状态管理 * * ## 工作流程 * * 1. 检查登录状态,未登录跳转到登录页 * 2. 首次访问时获取用户信息和菜单数据 * 3. 根据权限动态注册路由 * 4. 设置页面标题和工作标签页 * 5. 处理根路径重定向到首页 * 6. 未匹配路由跳转到 404 页面 * * @module router/guards/beforeEach * @author Art Design Pro Team */ import type { Router, RouteLocationNormalized, NavigationGuardNext } from 'vue-router' import { nextTick } from 'vue' import NProgress from 'nprogress' import { useSettingStore } from '@/store/modules/setting' import { useUserStore } from '@/store/modules/user' import { useMenuStore } from '@/store/modules/menu' import { useDictStore } from '@/store/modules/dict' import { setWorktab } from '@/utils/navigation' import { setPageTitle } from '@/utils/router' import { loadPageLocale } from '@/locales/pageLocaleLoader' import { RoutesAlias } from '../routesAlias' import { staticRoutes } from '../routes/staticRoutes' import { loadingService } from '@/utils/ui' import { useCommon } from '@/hooks/core/useCommon' import { useWorktabStore } from '@/store/modules/worktab' import { fetchGetUserInfo, fetchGetDictList } from '@/api/auth' import { ApiStatus } from '@/utils/http/status' import { isHttpError } from '@/utils/http/error' import { RouteRegistry, MenuProcessor, IframeRouteManager, RoutePermissionValidator } from '../core' // 路由注册器实例 let routeRegistry: RouteRegistry | null = null // 菜单处理器实例 const menuProcessor = new MenuProcessor() // 跟踪是否需要关闭 loading let pendingLoading = false // 路由初始化失败标记,防止死循环 // 一旦设置为 true,只有刷新页面或重新登录才能重置 let routeInitFailed = false // 路由初始化进行中标记,防止并发请求 let routeInitInProgress = false /** * 获取 pendingLoading 状态 */ export function getPendingLoading(): boolean { return pendingLoading } /** * 重置 pendingLoading 状态 */ export function resetPendingLoading(): void { pendingLoading = false } /** * 获取路由初始化失败状态 */ export function getRouteInitFailed(): boolean { return routeInitFailed } /** * 重置路由初始化状态(用于重新登录场景) */ export function resetRouteInitState(): void { routeInitFailed = false routeInitInProgress = false } /** * 设置路由全局前置守卫 */ export function setupBeforeEachGuard(router: Router): void { // 初始化路由注册器 routeRegistry = new RouteRegistry(router) router.beforeEach( async ( to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext ) => { try { await handleRouteGuard(to, from, next, router) } catch (error) { console.error('[RouteGuard] 路由守卫处理失败:', error) closeLoading() next({ name: 'Exception500' }) } } ) } /** * 关闭 loading 效果 */ function closeLoading(): void { if (pendingLoading) { nextTick(() => { loadingService.hideLoading() pendingLoading = false }) } } /** * 处理路由守卫逻辑 */ async function handleRouteGuard( to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext, router: Router ): Promise { const settingStore = useSettingStore() const userStore = useUserStore() // 启动进度条 if (settingStore.showNprogress) { NProgress.start() } // 1. 检查登录状态 if (!handleLoginStatus(to, userStore, next)) { return } // 2. 检查路由初始化是否已失败(防止死循环) if (routeInitFailed) { // 已经失败过,直接放行到错误页面,不再重试 if (to.matched.length > 0) { next() } else { // 未匹配到路由,跳转到 500 页面 next({ name: 'Exception500', replace: true }) } return } // 3. 处理动态路由注册 if (!routeRegistry?.isRegistered() && userStore.isLogin) { // 防止并发请求(快速连续导航场景) if (routeInitInProgress) { // 正在初始化中,等待完成后重新导航 next(false) return } await handleDynamicRoutes(to, next, router) return } // 4. 处理根路径重定向 if (handleRootPathRedirect(to, next)) { return } // 5. 处理已匹配的路由 if (to.matched.length > 0) { setWorktab(to) setPageTitle(to) await loadPageLocale(to.path) next() return } // 6. 未匹配到路由,跳转到 404 next({ name: 'Exception404' }) } /** * 处理登录状态 * @returns true 表示可以继续,false 表示已处理跳转 */ function handleLoginStatus( to: RouteLocationNormalized, userStore: ReturnType, next: NavigationGuardNext ): boolean { // 已登录或访问登录页,直接放行 if (userStore.isLogin || to.path === RoutesAlias.Login) { return true } // 未登录时访问根路径(首页),重定向到登录页 if (to.path === '/') { userStore.logOut() next({ name: 'Login', query: { redirect: to.fullPath } }) return false } // 其他静态路由(注册、忘记密码、错误页等)放行 if (isStaticRoute(to.path)) { return true } // 未登录且访问需要权限的页面,跳转到登录页并携带 redirect 参数 userStore.logOut() next({ name: 'Login', query: { redirect: to.fullPath } }) return false } /** * 检查路由是否为静态路由 */ function isStaticRoute(path: string): boolean { const checkRoute = (routes: any[], targetPath: string): boolean => { return routes.some((route) => { // 处理动态路由参数匹配 const routePath = route.path const pattern = routePath.replace(/:[^/]+/g, '[^/]+').replace(/\*/g, '.*') const regex = new RegExp(`^${pattern}$`) if (regex.test(targetPath)) { return true } if (route.children && route.children.length > 0) { return checkRoute(route.children, targetPath) } return false }) } return checkRoute(staticRoutes, path) } /** * 处理动态路由注册 */ async function handleDynamicRoutes( to: RouteLocationNormalized, next: NavigationGuardNext, router: Router ): Promise { // 标记初始化进行中 routeInitInProgress = true // 显示 loading pendingLoading = true loadingService.showLoading() try { // 1. 获取用户信息 await fetchUserInfo() // + 获取字典数据 await fetchDictList() // 2. 获取菜单数据 const menuList = await menuProcessor.getMenuList() // 3. 验证菜单数据 if (!menuProcessor.validateMenuList(menuList)) { throw new Error('获取菜单列表失败,请重新登录') } // 4. 注册动态路由 routeRegistry?.register(menuList) // 5. 保存菜单数据到 store const menuStore = useMenuStore() menuStore.setMenuList(menuList) menuStore.addRemoveRouteFns(routeRegistry?.getRemoveRouteFns() || []) // 6. 保存 iframe 路由 IframeRouteManager.getInstance().save() // 7. 验证工作标签页 useWorktabStore().validateWorktabs(router) // 8. 验证目标路径权限 const { homePath } = useCommon() const { path: validatedPath, hasPermission } = RoutePermissionValidator.validatePath( to.path, menuList, homePath.value || '/' ) // 初始化成功,重置进行中标记 routeInitInProgress = false // 9. 重新导航到目标路由 if (!hasPermission) { // 无权限访问,跳转到首页 closeLoading() // 输出警告信息 console.warn(`[RouteGuard] 用户无权限访问路径: ${to.path},已跳转到首页`) // 直接跳转到首页 next({ path: validatedPath, replace: true }) } else { // 有权限,正常导航 next({ path: to.path, query: to.query, hash: to.hash, replace: true }) } } catch (error) { console.error('[RouteGuard] 动态路由注册失败:', error) // 关闭 loading closeLoading() // 401 错误:axios 拦截器已处理退出登录,取消当前导航 if (isUnauthorizedError(error)) { // 重置状态,允许重新登录后再次初始化 routeInitInProgress = false next(false) return } // 标记初始化失败,防止死循环 routeInitFailed = true routeInitInProgress = false // 输出详细错误信息,便于排查 if (isHttpError(error)) { console.error(`[RouteGuard] 错误码: ${error.code}, 消息: ${error.message}`) } // 跳转到 500 页面,使用 replace 避免产生历史记录 next({ name: 'Exception500', replace: true }) } } /** * 获取用户信息 */ async function fetchUserInfo(): Promise { const userStore = useUserStore() const data = await fetchGetUserInfo() userStore.setUserInfo(data) // 检查并清理工作台标签页(如果是不同用户登录) userStore.checkAndClearWorktabs() } /** * 获取字典数据 */ async function fetchDictList(): Promise { const dictStore = useDictStore() const data = await fetchGetDictList() dictStore.setDictList(data) } /** * 重置路由相关状态 */ export function resetRouterState(delay: number): void { setTimeout(() => { routeRegistry?.unregister() IframeRouteManager.getInstance().clear() const menuStore = useMenuStore() menuStore.removeAllDynamicRoutes() menuStore.setMenuList([]) // 重置路由初始化状态,允许重新登录后再次初始化 resetRouteInitState() }, delay) } /** * 处理根路径重定向到首页 * @returns true 表示已处理跳转,false 表示无需跳转 */ function handleRootPathRedirect(to: RouteLocationNormalized, next: NavigationGuardNext): boolean { if (to.path !== '/') { return false } const { homePath } = useCommon() if (homePath.value && homePath.value !== '/') { next({ path: homePath.value, replace: true }) return true } return false } /** * 判断是否为未授权错误(401) */ function isUnauthorizedError(error: unknown): boolean { return isHttpError(error) && error.code === ApiStatus.unauthorized }