路由优化
This commit is contained in:
+176
-247
@@ -1,192 +1,139 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/modules/user'
|
||||
import { ElNotification } from 'element-plus'
|
||||
import systemRouter from './systemRouter'
|
||||
import userRoutes from '@/config/routes'
|
||||
import NProgress from 'nprogress'
|
||||
import tool from '@/utils/tool'
|
||||
import i18n from '@/i18n'
|
||||
import { beforeEach, afterEach } from './scrollBehavior'
|
||||
import '@/assets/css/nprogress.css'
|
||||
import 'nprogress/nprogress.css'
|
||||
import config from '../config'
|
||||
import userRoutes from '../config/routes'
|
||||
import { useUserStore } from '../stores/modules/user'
|
||||
import systemRoutes from './systemRoutes'
|
||||
|
||||
// 配置 NProgress
|
||||
NProgress.configure({
|
||||
easing: 'ease',
|
||||
speed: 500,
|
||||
showSpinner: false,
|
||||
trickleSpeed: 200,
|
||||
minimum: 0.3
|
||||
})
|
||||
|
||||
// 匹配pages里面所有的.vue文件
|
||||
const modules = import.meta.glob('@/pages/**/*.vue')
|
||||
|
||||
// 特殊路由模块
|
||||
const otherModules = {
|
||||
'404': () => import('@/layouts/other/404.vue'),
|
||||
empty: () => import('@/layouts/other/empty.vue')
|
||||
/**
|
||||
* 404 路由
|
||||
*/
|
||||
const notFoundRoute = {
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'NotFound',
|
||||
component: () => import('../layouts/other/404.vue'),
|
||||
meta: {
|
||||
title: '404',
|
||||
hidden: true
|
||||
}
|
||||
}
|
||||
|
||||
// 系统路由
|
||||
const routes = systemRouter
|
||||
|
||||
// 是否已加载过动态/静态路由
|
||||
let isGetRouter = false
|
||||
|
||||
// 404路由移除函数
|
||||
let routes_404_r = null
|
||||
|
||||
// 创建路由实例
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: routes
|
||||
routes: systemRoutes
|
||||
})
|
||||
|
||||
/**
|
||||
* 设置页面标题
|
||||
* @param {Object} meta - 路由元信息
|
||||
* 组件导入映射
|
||||
*/
|
||||
const setPageTitle = (meta) => {
|
||||
const title = 'VueAdmin'
|
||||
if (meta?.title) {
|
||||
try {
|
||||
const translatedTitle = i18n.global.te(meta.title) ? i18n.global.t(meta.title) : meta.title
|
||||
document.title = `${translatedTitle} - ${title}`
|
||||
} catch (error) {
|
||||
document.title = `${meta.title} - ${title}`
|
||||
}
|
||||
} else {
|
||||
document.title = title
|
||||
const modules = import.meta.glob('../pages/**/*.vue')
|
||||
|
||||
/**
|
||||
* 动态加载组件
|
||||
* @param {string} componentPath - 组件路径
|
||||
* @returns {Promise} 组件
|
||||
*/
|
||||
function loadComponent(componentPath) {
|
||||
// 如果组件路径以 'views/' 或 'pages/' 开头,则从相应目录加载
|
||||
if (componentPath.startsWith('views/')) {
|
||||
const path = componentPath.replace('views/', '../pages/')
|
||||
return modules[`${path}.vue`]
|
||||
}
|
||||
|
||||
// 如果是简单的组件名称,从 pages 目录加载
|
||||
return modules[`../pages/${componentPath}/index.vue`]
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查路由是否需要认证
|
||||
* @param {Object} to - 目标路由
|
||||
* @returns {boolean}
|
||||
* 将后端菜单转换为路由格式
|
||||
* @param {Array} menus - 后端返回的菜单数据
|
||||
* @returns {Array} 路由数组
|
||||
*/
|
||||
const checkAuthRequired = (to) => {
|
||||
return to.matched.some((record) => record.meta.requiresAuth !== false)
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除404路由
|
||||
*/
|
||||
const remove404Route = () => {
|
||||
if (routes_404_r) {
|
||||
router.removeRoute('404')
|
||||
routes_404_r = null
|
||||
function transformMenusToRoutes(menus) {
|
||||
if (!menus || !Array.isArray(menus)) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加404路由
|
||||
*/
|
||||
const add404Route = () => {
|
||||
if (!routes_404_r) {
|
||||
routes_404_r = router.addRoute({
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: '404',
|
||||
hidden: true,
|
||||
component: otherModules['404']
|
||||
return menus
|
||||
.filter(menu => menu && menu.path)
|
||||
.map(menu => {
|
||||
const route = {
|
||||
path: menu.path,
|
||||
name: menu.name || menu.path.replace(/\//g, '-'),
|
||||
meta: {
|
||||
title: menu.meta?.title || menu.title,
|
||||
icon: menu.meta?.icon || menu.icon,
|
||||
hidden: menu.hidden || menu.meta?.hidden,
|
||||
keepAlive: menu.meta?.keepAlive || false,
|
||||
role: menu.meta?.role || []
|
||||
}
|
||||
}
|
||||
|
||||
// 处理组件
|
||||
if (menu.component) {
|
||||
route.component = loadComponent(menu.component)
|
||||
}
|
||||
|
||||
// 处理子路由
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
route.children = transformMenusToRoutes(menu.children)
|
||||
}
|
||||
|
||||
// 处理重定向
|
||||
if (menu.redirect) {
|
||||
route.redirect = menu.redirect
|
||||
}
|
||||
|
||||
return route
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载动态路由
|
||||
* @param {Object} to - 目标路由对象
|
||||
* @returns {boolean} 是否加载成功
|
||||
* 添加路由到路由器
|
||||
* @param {Array} routes - 要添加的路由数组
|
||||
*/
|
||||
const loadDynamicRoutes = async (to) => {
|
||||
try {
|
||||
// 从 store 获取菜单和用户信息
|
||||
const userStore = useUserStore()
|
||||
|
||||
const apiMenu = userStore.getMenu() || []
|
||||
const userInfo = userStore.userInfo
|
||||
|
||||
// 如果没有用户信息,不做处理
|
||||
if (!userInfo) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 根据用户角色过滤静态路由
|
||||
const userMenu = treeFilter(userRoutes, (node) => {
|
||||
return node.meta.role
|
||||
? node.meta.role.some((item) => userInfo.role?.includes(item))
|
||||
: true
|
||||
})
|
||||
|
||||
// 合并静态路由和API菜单
|
||||
const menu = [...userMenu, ...apiMenu]
|
||||
|
||||
// 转换异步路由
|
||||
const menuRouter = filterAsyncRouter(menu)
|
||||
|
||||
// 将树形路由转平铺
|
||||
const flatMenuRouter = tool.tree_to_list(menuRouter)
|
||||
|
||||
// 添加所有路由
|
||||
flatMenuRouter.forEach((item) => {
|
||||
router.addRoute('layout', item)
|
||||
})
|
||||
|
||||
// 添加404路由(必须在所有路由之后)
|
||||
add404Route()
|
||||
|
||||
isGetRouter = true
|
||||
|
||||
// 检查目标路由是否存在
|
||||
const hasRoute = router.hasRoute(to.name) || to.matched.length > 0
|
||||
|
||||
if (!hasRoute && to.name !== '404') {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('加载动态路由失败:', error)
|
||||
return false
|
||||
}
|
||||
function addRoutes(routes) {
|
||||
routes.forEach(route => {
|
||||
router.addRoute(route)
|
||||
})
|
||||
}
|
||||
|
||||
// 路由拦截器 - 全局前置守卫
|
||||
/**
|
||||
* 路由守卫
|
||||
*/
|
||||
let isDynamicRouteLoaded = false
|
||||
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
// 开始进度条
|
||||
NProgress.start()
|
||||
|
||||
// 设置页面标题
|
||||
setPageTitle(to.meta)
|
||||
document.title = to.meta.title
|
||||
? `${to.meta.title} - ${config.app_title}`
|
||||
: config.app_title
|
||||
|
||||
const userStore = useUserStore()
|
||||
const isLoggedIn = userStore.isLoggedIn()
|
||||
const requiresAuth = checkAuthRequired(to)
|
||||
const whiteList = config.whiteList || []
|
||||
|
||||
// 处理404页面
|
||||
if (to.name === '404') {
|
||||
// 1. 如果在白名单中,直接放行
|
||||
if (whiteList.includes(to.path)) {
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
// 处理登录页
|
||||
if (to.path === '/login') {
|
||||
isGetRouter = false
|
||||
remove404Route()
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
// 如果是系统路由,直接放行
|
||||
if (routes.some((r) => r.path === to.path)) {
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
// 需要认证但未登录,重定向到登录页
|
||||
if (requiresAuth && !isLoggedIn) {
|
||||
NProgress.done()
|
||||
isGetRouter = false
|
||||
userStore.clearMenu()
|
||||
// 2. 如果未登录,跳转到登录页
|
||||
if (!isLoggedIn) {
|
||||
// 保存目标路由,登录后跳转
|
||||
next({
|
||||
path: '/login',
|
||||
query: { redirect: to.fullPath }
|
||||
@@ -194,123 +141,105 @@ router.beforeEach(async (to, from, next) => {
|
||||
return
|
||||
}
|
||||
|
||||
// 整页路由处理
|
||||
if (to.meta.fullpage) {
|
||||
to.matched = [to.matched[to.matched.length - 1]]
|
||||
// 3. 已登录情况
|
||||
// 如果访问登录页,重定向到首页
|
||||
if (to.path === '/login') {
|
||||
next({ path: config.DASHBOARD_URL })
|
||||
return
|
||||
}
|
||||
|
||||
// 调用前置守卫
|
||||
beforeEach(to, from)
|
||||
// 4. 动态路由加载
|
||||
if (!isDynamicRouteLoaded) {
|
||||
try {
|
||||
// 获取静态菜单配置
|
||||
const staticMenus = userRoutes || []
|
||||
|
||||
// 加载动态/静态路由
|
||||
if (!isGetRouter) {
|
||||
const loadSuccess = await loadDynamicRoutes(to)
|
||||
// 获取后端返回的用户菜单
|
||||
const backendMenus = userStore.getMenu()
|
||||
|
||||
// 如果路由加载失败,跳转到404
|
||||
if (!loadSuccess) {
|
||||
next({ name: '404', replace: true })
|
||||
return
|
||||
// 合并静态菜单和后端菜单
|
||||
// 如果后端菜单为空,只使用静态菜单
|
||||
// 如果后端菜单不为空,合并两个菜单,后端菜单优先
|
||||
let mergedMenus = [...staticMenus]
|
||||
|
||||
if (backendMenus && backendMenus.length > 0) {
|
||||
// 创建菜单映射,用于去重(以路径为唯一标识)
|
||||
const menuMap = new Map()
|
||||
|
||||
// 先添加静态菜单
|
||||
staticMenus.forEach(menu => {
|
||||
if (menu.path) {
|
||||
menuMap.set(menu.path, menu)
|
||||
}
|
||||
})
|
||||
|
||||
// 添加后端菜单,如果路径重复则覆盖
|
||||
backendMenus.forEach(menu => {
|
||||
if (menu.path) {
|
||||
menuMap.set(menu.path, menu)
|
||||
}
|
||||
})
|
||||
|
||||
// 转换为数组
|
||||
mergedMenus = Array.from(menuMap.values())
|
||||
}
|
||||
|
||||
if (mergedMenus && mergedMenus.length > 0) {
|
||||
// 将合并后的菜单转换为路由
|
||||
const dynamicRoutes = transformMenusToRoutes(mergedMenus)
|
||||
|
||||
// 添加动态路由到 Layout 的子路由
|
||||
dynamicRoutes.forEach(route => {
|
||||
router.addRoute('Layout', route)
|
||||
})
|
||||
|
||||
// 添加 404 路由(必须在最后添加)
|
||||
router.addRoute(notFoundRoute)
|
||||
|
||||
isDynamicRouteLoaded = true
|
||||
|
||||
// 重新导航,确保新添加的路由被正确匹配
|
||||
next({ ...to, replace: true })
|
||||
} else {
|
||||
// 如果没有菜单数据,重置并跳转到登录页
|
||||
userStore.logout()
|
||||
next({ path: '/login', query: { redirect: to.fullPath } })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('动态路由加载失败:', error)
|
||||
|
||||
// 加载失败,清除用户信息并跳转到登录页
|
||||
userStore.logout()
|
||||
next({
|
||||
path: '/login',
|
||||
query: { redirect: to.fullPath }
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// 动态路由已加载,直接放行
|
||||
next()
|
||||
}
|
||||
|
||||
// 检查路由是否存在(动态路由已加载后)
|
||||
if (isGetRouter) {
|
||||
const hasRoute = router.hasRoute(to.name)
|
||||
// 如果路由不存在且不是404页面,跳转到404
|
||||
if (!hasRoute && to.name !== '404' && to.matched.length === 0) {
|
||||
next({ name: '404', replace: true })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
// 全局后置钩子
|
||||
router.afterEach((to, from) => {
|
||||
// 调用后置钩子
|
||||
afterEach(to, from)
|
||||
|
||||
router.afterEach(() => {
|
||||
// 结束进度条
|
||||
NProgress.done()
|
||||
})
|
||||
|
||||
// 路由错误处理
|
||||
router.onError((error) => {
|
||||
NProgress.done()
|
||||
ElNotification.error({
|
||||
title: '路由错误',
|
||||
message: error.message
|
||||
/**
|
||||
* 重置路由(用于登出时)
|
||||
*/
|
||||
export function resetRouter() {
|
||||
// 移除所有动态添加的路由
|
||||
isDynamicRouteLoaded = false
|
||||
|
||||
// 重置为初始路由
|
||||
const newRouter = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: systemRoutes
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* 动态加载组件
|
||||
* @param {string} component - 组件路径
|
||||
* @returns {Function}
|
||||
*/
|
||||
function loadComponent(component) {
|
||||
if (!component) {
|
||||
return otherModules.empty
|
||||
}
|
||||
|
||||
for (const path in modules) {
|
||||
const dir = path.split('pages/')[1]?.split('.vue')[0]
|
||||
if (dir === component || dir === `${component}/index`) {
|
||||
return () => modules[path]()
|
||||
}
|
||||
}
|
||||
|
||||
return otherModules.empty
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换异步路由
|
||||
* @param {Array} routerMap - 路由映射
|
||||
* @returns {Array} 转换后的路由数组
|
||||
*/
|
||||
function filterAsyncRouter(routerMap) {
|
||||
if (!routerMap || !Array.isArray(routerMap)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return routerMap.map((item) => {
|
||||
const meta = item.meta || {}
|
||||
|
||||
// 处理外部链接特殊路由
|
||||
if (meta.type === 'iframe') {
|
||||
meta.url = item.path
|
||||
item.path = `/i/${item.name}`
|
||||
}
|
||||
|
||||
return {
|
||||
path: item.path,
|
||||
name: item.name,
|
||||
meta: meta,
|
||||
redirect: item.redirect,
|
||||
children: item.children ? filterAsyncRouter(item.children) : undefined,
|
||||
component: loadComponent(item.component)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤树结构
|
||||
* @param {Array} tree - 树结构数据
|
||||
* @param {Function} func - 过滤函数
|
||||
* @returns {Array} 过滤后的树结构
|
||||
*/
|
||||
function treeFilter(tree, func) {
|
||||
if (!tree || !Array.isArray(tree)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return tree
|
||||
.map((node) => ({ ...node }))
|
||||
.filter((node) => {
|
||||
node.children = node.children ? treeFilter(node.children, func) : undefined
|
||||
return func(node) || (node.children && node.children.length > 0)
|
||||
})
|
||||
router.matcher = newRouter.matcher
|
||||
}
|
||||
|
||||
export default router
|
||||
|
||||
Reference in New Issue
Block a user