Files
one-pipe-system/src/router/guards/beforeEach.ts
luo c1a7118d35
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m40s
fix: 将导出列表分成三个模块
2026-07-09 15:20:28 +08:00

562 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<void> {
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<typeof useUserStore>,
next: NavigationGuardNext
): Promise<boolean> {
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<void> {
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<void> {
try {
if (useCommon().isFrontendMode.value) {
await processFrontendMenu(router) // 前端控制模式
} else {
await processBackendMenu(router) // 后端控制模式
}
} catch (error) {
handleMenuError(error)
}
}
/**
* 处理前端控制模式的菜单逻辑
*/
async function processFrontendMenu(router: Router): Promise<void> {
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<void> {
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<string, AppRouteRecord>
): 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<string, AppRouteRecord> {
const map = new Map<string, AppRouteRecord>()
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<string, AppRouteRecord>
): 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<void> {
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([])
}