From 8137808d44e94306161567b0838d12905106f206 Mon Sep 17 00:00:00 2001 From: zhangsz Date: Fri, 25 Jul 2025 11:42:00 +0800 Subject: [PATCH] fix: router error display 403 if the first children menu unavalaible --- src/router/index.ts | 331 ++++++++++++++++++++++++++++++++++-- src/store/modules/router.ts | 45 ++++- 2 files changed, 354 insertions(+), 22 deletions(-) diff --git a/src/router/index.ts b/src/router/index.ts index cda59165..9bfff740 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -145,10 +145,17 @@ const router = createRouter({ }); /**全局路由-后置守卫 */ +// 在路由后置守卫中清理计数器 router.afterEach((to, from, failure) => { NProgress.done(); + + // 清理成功导航的重定向计数 + if (!failure) { + const redirectKey = `${from.path}->${to.path}`; + redirectCount.delete(redirectKey); + } + const title = to.meta?.title; - // 设置标题 if (!failure && title) { useAppStore().setTitle(to.meta.title); } @@ -164,10 +171,27 @@ const WHITE_LIST: string[] = [ '/trace-task-hlr', ]; +const redirectCount = new Map(); +const MAX_REDIRECT_COUNT = 3; // 最大重定向次数 + /**全局路由-前置守卫 */ router.beforeEach(async (to, from, next) => { NProgress.start(); + // 检查是否是 F5 刷新(from.name 为 null 且 to.path 不是根路径) + const isRefresh = !from.name && from.path === '/'; + + // 重定向计数检查 + const redirectKey = `${from.path}->${to.path}`; + const currentCount = redirectCount.get(redirectKey) || 0; + + if (currentCount > MAX_REDIRECT_COUNT) { + console.warn(`检测到重定向循环: ${redirectKey},强制跳转到首页`); + redirectCount.delete(redirectKey); + next({ name: 'Index', replace: true }); + return; + } + // 获取系统配置信息 const appStore = useAppStore(); if (!appStore.loginBackground) { @@ -203,48 +227,321 @@ router.beforeEach(async (to, from, next) => { // 有Token if (token) { - // 防止重复访问登录页面 if (to.path === '/login') { next({ name: 'Index' }); } else { - // 判断当前用户是否有角色信息 const user = useUserStore(); if (user.roles && user.roles.length === 0) { try { - // 获取网元信息 - await useNeInfoStore().fnNelist(); - // 获取用户信息 + await useNeInfoStore().fnNelist(); await user.fnGetInfo(); - // 获取路由信息 const accessRoutes = await useRouterStore().generateRoutes(); - // 根据后台配置生成可访问的路由表 + if (accessRoutes && accessRoutes.length !== 0) { for (const route of accessRoutes) { - // 动态添加可访问路由表,http开头会异常 if (!validHttp(route.path)) { router.addRoute(route); } } + + // F5 刷新时,如果目标路由有效,直接跳转,不进行重定向 + if (isRefresh && await isRouteAccessible(to, accessRoutes)) { + console.log(`F5 刷新,目标路由有效,直接跳转: ${to.path}`); + next({ ...to, replace: true }); + return; + } + + if (await isRouteAccessible(to, accessRoutes)) { + next({ ...to, replace: true }); + } else { + // F5 刷新时,如果目标路由无效,优先跳转到首页 + if (isRefresh) { + console.log(`F5 刷新,目标路由无效,跳转到首页: ${to.path}`); + next({ name: 'Index', replace: true }); + return; + } + + const validRedirect = findValidRedirect(to, accessRoutes); + if (validRedirect && validRedirect !== to.path) { + redirectCount.set(redirectKey, currentCount + 1); + console.log(`重定向到有效路由: ${to.path} -> ${validRedirect}`); + next({ path: validRedirect, replace: true }); + } else { + next({ name: 'Index', replace: true }); + } + } + } else { + next({ name: 'Index', replace: true }); } - // 刷新替换原先路由,确保addRoutes已完成 - next({ ...to, replace: true }); - } catch (error: any) { - console.error(`[${to.path}]: ${error.message}`); - await user.fnLogOut(); - next({ name: 'Login' }); + } catch (error) { + console.error('Route guard error:', error); + next(`/login?redirect=${to.fullPath}`); } } else if ( to.meta.neType && - //Array.isArray(to.meta.neType) && to.meta.neType.length > 0 && !useNeInfoStore().fnHasNe(to.meta.neType) ) { - next({ name: 'NotPermission' }); + // 找到有效的替代路由 + const validRedirect = findValidAlternative(to); + if (validRedirect && validRedirect !== to.path) { + redirectCount.set(redirectKey, currentCount + 1); + console.log(`403 重定向: ${to.path} -> ${validRedirect}`); + next({ path: validRedirect, replace: true }); + } else { + console.log(`无有效替代路由,跳转到权限错误页面: ${to.path}`); + next({ name: 'NotPermission' }); + } } else { + // 清除重定向计数 + redirectCount.clear(); next(); } } } }); +/** + * 检查路由是否可访问 + */ +async function isRouteAccessible(to: any, accessRoutes: any[]): Promise { + // 检查路由是否存在于 accessRoutes 中 + const routeExists = findRouteInAccessRoutes(to.path, accessRoutes); + + console.log(`检查路由可访问性: ${to.path}`, routeExists ? '找到' : '未找到'); + + if (!routeExists) return false; + + // 检查网元类型 + if (to.meta?.neType && to.meta.neType.length > 0) { + const hasNe = useNeInfoStore().fnHasNe(to.meta.neType); + console.log(`网元类型检查: ${to.meta.neType}`, hasNe ? '有效' : '无效'); + return hasNe; + } + + return true; +} + +/** + * 在访问路由中查找指定路径 + */ +function findRouteInAccessRoutes(targetPath: string, routes: any[], parentPath: string = ''): any { + for (const route of routes) { + // 构建完整路径 + let fullPath = route.path; + + // 如果不是绝对路径,需要拼接父路径 + if (!fullPath.startsWith('/')) { + const cleanParentPath = parentPath.replace(/\/$/, ''); // 移除末尾斜杠 + const cleanRoutePath = fullPath.replace(/^\//, ''); // 移除开头斜杠 + fullPath = cleanParentPath + '/' + cleanRoutePath; + } + + // 标准化路径,移除多余的斜杠 + fullPath = fullPath.replace(/\/+/g, '/'); + + console.log(`匹配路径: ${targetPath} vs ${fullPath}`); + + if (fullPath === targetPath) { + return route; + } + + // 递归查找子路由 + if (route.children && route.children.length > 0) { + const found = findRouteInAccessRoutes(targetPath, route.children, fullPath); + if (found) return found; + } + } + return null; +} + +/** + * 查找有效的重定向目标 + */ +function findValidRedirect(to: any, accessRoutes: any[]): string | null { + const neStore = useNeInfoStore(); + + console.log(`查找重定向目标: ${to.path}`); + + // 1. 查找父路由的 redirect + const parentRoute = findParentRouteWithRedirect(to.path, accessRoutes); + + if (parentRoute?.redirect) { + console.log(`找到父路由重定向: ${parentRoute.path} -> ${parentRoute.redirect}`); + + // 验证 redirect 目标是否有效 + const redirectTarget = findRouteInAccessRoutes(parentRoute.redirect, accessRoutes); + if (redirectTarget && + (!redirectTarget.meta?.neType || neStore.fnHasNe(redirectTarget.meta.neType))) { + return parentRoute.redirect; + } + } + + // 2. 查找同级的第一个有效路由 + const siblingRoute = findFirstValidSibling(to.path, accessRoutes); + if (siblingRoute) { + console.log(`找到同级有效路由: ${siblingRoute}`); + return siblingRoute; + } + + // 3. 查找根级别的第一个有效路由 + const rootRoute = findFirstValidRootRoute(accessRoutes); + if (rootRoute) { + console.log(`找到根级有效路由: ${rootRoute}`); + return rootRoute; + } + + return null; +} + +/** + * 查找具有 redirect 的父路由 + */ +function findParentRouteWithRedirect(targetPath: string, routes: any[], parentPath: string = ''): any { + for (const route of routes) { + let fullPath = route.path; + + if (!fullPath.startsWith('/')) { + const cleanParentPath = parentPath.replace(/\/$/, ''); + const cleanRoutePath = fullPath.replace(/^\//, ''); + fullPath = cleanParentPath + '/' + cleanRoutePath; + } + + fullPath = fullPath.replace(/\/+/g, '/'); + + // 检查是否是父路径且有 redirect + if (targetPath.startsWith(fullPath) && + route.redirect && + fullPath !== targetPath) { + return { ...route, path: fullPath }; + } + + // 递归查找 + if (route.children && route.children.length > 0) { + const found = findParentRouteWithRedirect(targetPath, route.children, fullPath); + if (found) return found; + } + } + return null; +} + +/** + * 查找同级的第一个有效路由 + */ +function findFirstValidSibling(targetPath: string, routes: any[], parentPath: string = ''): string | null { + const neStore = useNeInfoStore(); + + // 获取父路径 + const parentRouteResult = findParentOfTarget(targetPath, routes, parentPath); + if (!parentRouteResult) return null; + + const { parentRoute, parentFullPath } = parentRouteResult; + + if (parentRoute.children) { + for (const sibling of parentRoute.children) { + let siblingFullPath = sibling.path; + + if (!siblingFullPath.startsWith('/')) { + const cleanParentPath = parentFullPath.replace(/\/$/, ''); + const cleanSiblingPath = siblingFullPath.replace(/^\//, ''); + siblingFullPath = cleanParentPath + '/' + cleanSiblingPath; + } + + siblingFullPath = siblingFullPath.replace(/\/+/g, '/'); + + if (siblingFullPath !== targetPath && + (!sibling.meta?.neType || neStore.fnHasNe(sibling.meta.neType))) { + return siblingFullPath; + } + } + } + + return null; +} + +/** + * 查找目标路径的父路由 + */ +function findParentOfTarget(targetPath: string, routes: any[], parentPath: string = ''): { parentRoute: any, parentFullPath: string } | null { + for (const route of routes) { + let fullPath = route.path; + + if (!fullPath.startsWith('/')) { + const cleanParentPath = parentPath.replace(/\/$/, ''); + const cleanRoutePath = fullPath.replace(/^\//, ''); + fullPath = cleanParentPath + '/' + cleanRoutePath; + } + + fullPath = fullPath.replace(/\/+/g, '/'); + + // 检查子路由 + if (route.children && route.children.length > 0) { + for (const child of route.children) { + let childFullPath = child.path; + + if (!childFullPath.startsWith('/')) { + const cleanParentPath = fullPath.replace(/\/$/, ''); + const cleanChildPath = childFullPath.replace(/^\//, ''); + childFullPath = cleanParentPath + '/' + cleanChildPath; + } + + childFullPath = childFullPath.replace(/\/+/g, '/'); + + if (childFullPath === targetPath) { + return { parentRoute: route, parentFullPath: fullPath }; + } + } + + // 递归查找 + const found = findParentOfTarget(targetPath, route.children, fullPath); + if (found) return found; + } + } + return null; +} + +/** + * 查找根级别的第一个有效路由 + */ +function findFirstValidRootRoute(routes: any[]): string | null { + const neStore = useNeInfoStore(); + + for (const route of routes) { + if (!route.meta?.neType || neStore.fnHasNe(route.meta.neType)) { + if (route.children && route.children.length > 0) { + // 如果有子路由,返回第一个有效子路由的完整路径 + for (const child of route.children) { + let childFullPath = child.path; + + if (!childFullPath.startsWith('/')) { + const cleanParentPath = route.path.replace(/\/$/, ''); + const cleanChildPath = childFullPath.replace(/^\//, ''); + childFullPath = cleanParentPath + '/' + cleanChildPath; + } + + childFullPath = childFullPath.replace(/\/+/g, '/'); + + if (!child.meta?.neType || neStore.fnHasNe(child.meta.neType)) { + return childFullPath; + } + } + } else { + return route.path; + } + } + } + + return null; +} + +/** + * 查找有效的替代路由(用于 403 情况) + */ +function findValidAlternative(to: any): string | null { + const routerStore = useRouterStore(); + const buildRoutes = routerStore.buildRouterData; + + return findValidRedirect(to, buildRoutes); +} + export default router; diff --git a/src/store/modules/router.ts b/src/store/modules/router.ts index 11050f4b..3f1c1ff1 100644 --- a/src/store/modules/router.ts +++ b/src/store/modules/router.ts @@ -7,6 +7,7 @@ import type { RouteRecordRaw, } from 'vue-router'; import { getRouters } from '@/api/router'; +import useNeInfoStore from '@/store/modules/neinfo'; import BasicLayout from '@/layouts/BasicLayout.vue'; import BlankLayout from '@/layouts/BlankLayout.vue'; import LinkLayout from '@/layouts/LinkLayout.vue'; @@ -49,7 +50,12 @@ const useRouterStore = defineStore('router', { async generateRoutes() { const res = await getRouters(); if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) { - const buildRoutes = buildRouters(res.data.concat()); + // 获取当前网元类型 + const neTypes = useNeInfoStore().getNeSelectOtions.map(v => v.value); + // 先过滤 + const filteredRoutes = this.clearMenuItemByNeList(res.data.concat(), neTypes); + // 再 build + const buildRoutes = buildRouters(filteredRoutes); this.buildRouterData = buildRoutes; return buildRoutes; } @@ -103,6 +109,21 @@ const useRouterStore = defineStore('router', { return null; } finalItem.children = children; + + // 只重定向到第一个可用的子菜单 + // finalItem.redirect = finalItem.children[0].path; + // console.log(`finalItem.redirect`, finalItem.redirect); + // 如果有子菜单,且没有重定向,则设置重定向到第一个子菜单 + if (children.length > 0) { + let childPath = children[0].path; + if (!childPath.startsWith('/')) { + // 确保父路径以 / 结尾,子路径不以 / 开头 + const parentPath = finalItem.path.replace(/\/$/, ''); + childPath = parentPath + '/' + childPath.replace(/^\//, ''); + } + finalItem.redirect = childPath; + } + return finalItem; } @@ -117,7 +138,7 @@ const useRouterStore = defineStore('router', { /**异步路由类型 */ type RecordRaws = { path: string; - name: string; + name?: string; meta: RouteMeta; redirect: RouteLocationRaw; component: string; @@ -132,17 +153,18 @@ type RecordRaws = { * @param recordRaws 异步路由列表 * @returns 可添加的路由列表 */ -function buildRouters(recordRaws: RecordRaws[]): RouteRecordRaw[] { +function buildRouters(recordRaws: RouteRecordRaw[]): RouteRecordRaw[] { const routers: RouteRecordRaw[] = []; for (const item of recordRaws) { // 过滤旧前端菜单 是layui的菜单跳过 if (['', '/page"'].includes(item.path)) { continue; } + // 路由页面组件 let component: RouteComponent = {}; if (item.component) { - const comp = item.component; + const comp = item.component as unknown as string; // 添加类型断言 if (comp === MENU_COMPONENT_LAYOUT_BASIC) { component = BasicLayout; } else if (comp === MENU_COMPONENT_LAYOUT_BLANK) { @@ -159,6 +181,17 @@ function buildRouters(recordRaws: RecordRaws[]): RouteRecordRaw[] { let children: RouteRecordRaw[] = []; if (item.children && item.children.length > 0) { children = buildRouters(item.children); + + // 如果没有 redirect 但有子菜单,设置 redirect 到第一个子菜单 + if (!item.redirect && children.length > 0) { + let childPath = children[0].path; + if (!childPath?.startsWith('/')) { + childPath = item.path.replace(/\/$/, '') + '/' + (childPath || '').replace(/^\//, ''); + } + + // 修改 item 的 redirect(需要类型断言) + (item as any).redirect = childPath; + } } // 对元数据特殊参数进行处理 @@ -166,7 +199,9 @@ function buildRouters(recordRaws: RecordRaws[]): RouteRecordRaw[] { if (!metaIcon.startsWith('icon-')) { metaIcon = ''; } - item.meta = Object.assign(item.meta, { + + // 更新 meta(需要类型断言) + (item as any).meta = Object.assign(item.meta || {}, { icon: metaIcon, });