import type { Router, RouteLocationNormalized, NavigationGuardNext } from 'vue-router' import NProgress from 'nprogress' import { useSettingStore } from '@/store/modules/setting' import { useUserStore } from '@/store/modules/user' import { useMenuStore } from '@/store/modules/menu' import { setWorktab } from '@/utils/navigation' import { setPageTitle, setSystemTheme } from '../utils/utils' import { registerDynamicRoutes } from '../utils/registerRoutes' import { AppRouteRecord } from '@/types/router' import { RoutesAlias } from '../routesAlias' import { menuDataToRouter } from '../utils/menuToRouter' import { asyncRoutes } from '../routes/asyncRoutes' import { loadingService } from '@/utils/ui' import { useCommon } from '@/composables/useCommon' import { useWorktabStore } from '@/store/modules/worktab' import { isInWhiteList, hasRoutePermission, isTokenValid, buildLoginRedirect } from './permission' // 是否已注册动态路由 const isRouteRegistered = ref(false) // 临时开发模式:跳过所有权限验证(开发静态页面时使用) const DEV_MODE_SKIP_AUTH = false /** * 路由全局前置守卫 * 处理进度条、获取菜单列表、动态路由注册、404 检查、工作标签页及页面标题设置 */ export function setupBeforeEachGuard(router: Router): void { router.beforeEach( async ( to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext ) => { try { // 开发模式:直接放行,无需登录 if (DEV_MODE_SKIP_AUTH) { const settingStore = useSettingStore() if (settingStore.showNprogress) { NProgress.start() } setSystemTheme(to) setPageTitle(to) // 开发模式下,首次访问时注册所有路由 if (!isRouteRegistered.value) { const menuList = asyncRoutes.map((route) => menuDataToRouter(route)) const menuStore = useMenuStore() menuStore.setMenuList(menuList) registerDynamicRoutes(router, menuList) isRouteRegistered.value = true // 重新导航到目标路由 next({ path: to.path, query: to.query, hash: to.hash, replace: true }) return } // 设置工作标签页 setWorktab(to) next() return } await handleRouteGuard(to, from, next, router) } catch (error) { console.error('路由守卫处理失败:', error) next('/exception/500') } } ) } /** * 处理路由守卫逻辑 */ async function handleRouteGuard( to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext, router: Router ): Promise { const settingStore = useSettingStore() const userStore = useUserStore() // 处理进度条 if (settingStore.showNprogress) { NProgress.start() } // 设置系统主题 setSystemTheme(to) // 处理登录状态 if (!(await handleLoginStatus(to, userStore, next))) { return } // 处理动态路由注册 if (!isRouteRegistered.value && userStore.isLogin) { await handleDynamicRoutes(to, router, next) return } // 处理已知的匹配路由 if (to.matched.length > 0) { setWorktab(to) setPageTitle(to) next() return } // 路由未匹配时,判断是 404 还是 403 if (userStore.isLogin) { // 检查路由是否存在于 asyncRoutes 中 const routeExistsInAsync = checkRouteExistsInAsyncRoutes(to.path, asyncRoutes) console.log(`[路由检查] 目标路径: ${to.path}, 是否存在于asyncRoutes: ${routeExistsInAsync}`) if (routeExistsInAsync) { // 路由存在于 asyncRoutes 但未注册,说明用户没有权限 console.warn('路由存在但用户无权限访问:', to.path) next(RoutesAlias.Exception403 || '/exception/403') return } } // 路由不存在,跳转到 404 console.log(`[路由检查] 路径 ${to.path} 不存在于asyncRoutes,跳转404`) next(RoutesAlias.Exception404) } /** * 处理登录状态 */ async function handleLoginStatus( to: RouteLocationNormalized, userStore: ReturnType, next: NavigationGuardNext ): Promise { const { isLogin, accessToken, info } = userStore // 如果访问的是白名单路由,直接放行 if (isInWhiteList(to.path) || to.meta.noLogin) { // 如果已登录且访问登录页,重定向到首页 if (isLogin && to.path === RoutesAlias.Login) { next(RoutesAlias.Home || '/') return false } return true } // 检查是否已登录 if (!isLogin || !accessToken) { userStore.logOut() next(buildLoginRedirect(to.fullPath)) return false } // 检查 Token 是否有效 if (!isTokenValid(accessToken)) { console.warn('Token 已过期,需要重新登录') userStore.logOut() next(buildLoginRedirect(to.fullPath)) return false } // 检查页面级权限 if (!hasRoutePermission(to, info)) { console.warn('无权限访问该页面:', to.path) next(RoutesAlias.Exception403 || '/exception/403') return false } return true } /** * 处理动态路由注册 */ async function handleDynamicRoutes( to: RouteLocationNormalized, router: Router, next: NavigationGuardNext ): Promise { try { await getMenuData(router) next({ path: to.path, query: to.query, hash: to.hash, replace: true }) } catch (error) { console.error('动态路由注册失败:', error) next('/exception/500') } } /** * 获取菜单数据 * @param router 路由实例 */ async function getMenuData(router: Router): Promise { try { if (useCommon().isFrontendMode.value) { await processFrontendMenu(router) // 前端控制模式 } else { await processBackendMenu(router) // 后端控制模式 } } catch (error) { handleMenuError(error) } } /** * 处理前端控制模式的菜单逻辑 */ async function processFrontendMenu(router: Router): Promise { const closeLoading = loadingService.showLoading() try { const menuList = asyncRoutes.map((route) => menuDataToRouter(route)) const userStore = useUserStore() const roles = userStore.info.roles if (!roles || roles.length === 0) { console.warn('用户角色信息不存在,清除登录状态') closeLoading() userStore.logOut() throw new Error('获取用户角色失败') } const filteredMenuList = filterMenuByRoles(menuList, roles) await new Promise((resolve) => setTimeout(resolve, 300)) await registerAndStoreMenu(router, filteredMenuList, closeLoading) } catch (error) { closeLoading() throw error } } /** * 处理后端控制模式的菜单 */ async function processBackendMenu(router: Router): Promise { const closeLoading = loadingService.showLoading() try { const userStore = useUserStore() // 如果是超级管理员(user_type === 1),直接使用所有 asyncRoutes if (userStore.isSuperAdmin) { const menuList = asyncRoutes.map((route) => menuDataToRouter(route)) await registerAndStoreMenu(router, menuList, closeLoading) return } // 普通用户:使用后端返回的菜单 const backendMenus = userStore.menus || [] const routeMap = buildRouteMap(asyncRoutes) const menuList = backendMenus .map((menu) => convertBackendMenuToRoute(menu, routeMap)) .filter((route) => route !== null) const finalMenuList = mergeDefaultMenus(menuList, routeMap) await registerAndStoreMenu(router, finalMenuList, closeLoading) } catch (error) { closeLoading() throw error } } /** * 合并默认菜单 * 将配置的默认菜单合并到后端返回的菜单中 * 注意: /dashboard 及其子路由(console/analysis/ecommerce)不再作为默认菜单强制显示 * 这些路由只有在后端 menus 中返回时才会显示 */ function mergeDefaultMenus( backendMenus: AppRouteRecord[], routeMap: Map ): AppRouteRecord[] { // 移除 /dashboard 作为默认菜单,现在仪表台路由完全由后端控制 const defaultMenuPaths: string[] = [] const defaultMenus: AppRouteRecord[] = defaultMenuPaths .map((path) => { const route = routeMap.get(path) return route ? menuDataToRouter(route) : null }) .filter((menu): menu is AppRouteRecord => menu !== null) const backendPaths = new Set(backendMenus.map((m) => m.path)) const filteredDefaultMenus = defaultMenus.filter((m) => !backendPaths.has(m.path)) return [...filteredDefaultMenus, ...backendMenus] } /** * 构建 URL 到路由的映射表 */ function buildRouteMap(routes: AppRouteRecord[], parentPath = ''): Map { const map = new Map() routes.forEach((route) => { // 构建完整路径 const fullPath = route.path.startsWith('/') ? route.path : parentPath ? `${parentPath}/${route.path}`.replace(/\/+/g, '/') : `/${route.path}` // 存储路由映射 map.set(fullPath, route) // 递归处理子路由 if (route.children && route.children.length > 0) { const childMap = buildRouteMap(route.children, fullPath) childMap.forEach((childRoute, childPath) => { map.set(childPath, childRoute) }) } }) return map } /** * 将后端菜单数据转换为路由格式 * 递归处理所有层级的 children */ function convertBackendMenuToRoute( menu: any, routeMap: Map ): AppRouteRecord | null { const menuUrl = menu.url || '/' const matchedRoute = routeMap.get(menuUrl) if (!matchedRoute) { console.warn(`未找到与菜单 URL "${menuUrl}" 匹配的路由定义: ${menu.name}`) // 如果当前菜单没有匹配的路由,但有 children,尝试递归处理 children if (menu.children && menu.children.length > 0) { const children = menu.children .map((child: any) => convertBackendMenuToRoute(child, routeMap)) .filter((child: AppRouteRecord | null) => child !== null) // 如果子菜单有有效的路由,返回一个包含子菜单的占位路由 if (children.length > 0) { return { path: menuUrl, name: menu.name || menuUrl.replace(/\//g, '_'), meta: { title: menu.name, permission: menu.perm_code, sort: menu.sort || 0, icon: menu.icon, isHide: false, roles: undefined, permissions: undefined }, children: children } as AppRouteRecord } } return null } const route: AppRouteRecord = { path: menuUrl, name: matchedRoute.name, component: matchedRoute.component, meta: { ...matchedRoute.meta, title: menu.name, permission: menu.perm_code, sort: menu.sort || matchedRoute.meta?.sort || 0, icon: menu.icon || matchedRoute.meta?.icon, // 清除前端定义的 roles 和 permissions,使用后端权限控制 roles: undefined, permissions: undefined } } // 递归处理后端返回的 children if (menu.children && menu.children.length > 0) { const children = menu.children .map((child: any) => convertBackendMenuToRoute(child, routeMap)) .filter((child: AppRouteRecord | null) => child !== null) if (children.length > 0) { route.children = children } } return route } /** * 注册路由并存储菜单数据 */ async function registerAndStoreMenu( router: Router, menuList: AppRouteRecord[], closeLoading: () => void ): Promise { if (!isValidMenuList(menuList)) { closeLoading() throw new Error('获取菜单列表失败,请重新登录') } const menuStore = useMenuStore() menuStore.setMenuList(menuList) registerDynamicRoutes(router, menuList) isRouteRegistered.value = true const worktabStore = useWorktabStore() worktabStore.validateWorktabs(router) // 刷新后只保留固定标签页和当前标签页 worktabStore.initializeAfterRefresh() closeLoading() } /** * 处理菜单相关错误 */ function handleMenuError(error: unknown): void { console.error('菜单处理失败:', error) // 确保 loading 被关闭 loadingService.hideLoading() useUserStore().logOut() throw error instanceof Error ? error : new Error('获取菜单列表失败,请重新登录') } /** * 根据角色过滤菜单 */ const filterMenuByRoles = (menu: AppRouteRecord[], roles: string[]): AppRouteRecord[] => { return menu.reduce((acc: AppRouteRecord[], item) => { const itemRoles = item.meta?.roles const hasPermission = !itemRoles || itemRoles.some((role) => roles?.includes(role)) if (hasPermission) { const filteredItem = { ...item } if (filteredItem.children?.length) { filteredItem.children = filterMenuByRoles(filteredItem.children, roles) } acc.push(filteredItem) } return acc }, []) } /** * 验证菜单列表是否有效 */ function isValidMenuList(menuList: AppRouteRecord[]): boolean { return Array.isArray(menuList) && menuList.length > 0 } /** * 检查路由是否存在于 asyncRoutes 中 * 支持动态参数匹配,如 /account-management/enterprise-customer/customer-accounts/3000 匹配 enterprise-customer/customer-accounts/:id * @param targetPath 目标路由路径,如 /account-management/enterprise-customer/customer-accounts/3000 * @param routes 路由配置数组 * @param parentPath 父路由路径 */ function checkRouteExistsInAsyncRoutes( targetPath: string, routes: AppRouteRecord[], parentPath = '' ): boolean { for (const route of routes) { // 构建完整路径 const fullPath = route.path.startsWith('/') ? route.path : parentPath ? `${parentPath}/${route.path}`.replace(/\/+/g, '/') : `/${route.path}` // 检查当前路由是否匹配(支持动态参数) const isMatch = matchRoutePath(targetPath, fullPath) if (isMatch) { console.log(`[路由匹配成功] 目标: ${targetPath}, 匹配到: ${fullPath}`) return true } // 递归检查子路由 if (route.children && route.children.length > 0) { if (checkRouteExistsInAsyncRoutes(targetPath, route.children, fullPath)) { return true } } } return false } /** * 匹配路由路径,支持动态参数 * @param targetPath 实际路径,如 /account-management/enterprise-customer/customer-accounts/3000 * @param routePath 路由定义路径,如 /account-management/enterprise-customer/customer-accounts/:id */ function matchRoutePath(targetPath: string, routePath: string): boolean { // 移除查询参数 const cleanTargetPath = targetPath.split('?')[0] const cleanRoutePath = routePath.split('?')[0] // 如果完全匹配,直接返回 if (cleanTargetPath === cleanRoutePath) { return true } // 将路径分割成段 const targetSegments = cleanTargetPath.split('/').filter(Boolean) const routeSegments = cleanRoutePath.split('/').filter(Boolean) // 段数必须相同 if (targetSegments.length !== routeSegments.length) { return false } // 逐段比较 for (let i = 0; i < routeSegments.length; i++) { const routeSegment = routeSegments[i] const targetSegment = targetSegments[i] // 如果是动态参数(以 : 开头),跳过比较 if (routeSegment.startsWith(':')) { continue } // 如果不是动态参数,必须完全匹配 if (routeSegment !== targetSegment) { return false } } return true } /** * 重置路由相关状态 */ export function resetRouterState(router: Router): void { isRouteRegistered.value = false // 清理动态注册的路由 router.getRoutes().forEach((route) => { if (route.meta?.dynamic) { router.removeRoute(route.name as string) } }) // 清空菜单数据 const menuStore = useMenuStore() menuStore.setMenuList([]) }