- 扩展路由元数据字段,新增auth权限标识支持 - 完善路由转换器,支持从后端菜单数据提取权限标识 - 优化静态路由配置,为仪表板添加固定标签页属性 - 增强路由类型定义,支持更灵活的权限验证机制
289 lines
9.8 KiB
TypeScript
289 lines
9.8 KiB
TypeScript
import type { MenuOption } from 'naive-ui'
|
||
import type { RouteRecordRaw } from 'vue-router'
|
||
import { h } from 'vue'
|
||
import { usePermission } from '@/hooks'
|
||
import Layout from '@/layouts/index.vue'
|
||
import { arrayToTree, renderIcon } from '@/utils'
|
||
import { safeAsyncComponent } from '@/utils/component-guard'
|
||
import { clone, min, omit, pick } from 'radash'
|
||
import { RouterLink } from 'vue-router'
|
||
|
||
const metaFields: AppRoute.MetaKeys[]
|
||
= ['title', 'icon', 'requiresAuth', 'roles', 'auth', 'keepAlive', 'hide', 'order', 'href', 'activeMenu', 'withoutTab', 'pinTab', 'menuType']
|
||
|
||
// 将后端菜单数据转换为前端路由数据
|
||
function transformBackendToRoute(backendRoute: AppRoute.BackendRoute): AppRoute.RowRoute {
|
||
return {
|
||
id: backendRoute.menuId,
|
||
pid: backendRoute.parentId === 0 ? null : backendRoute.parentId,
|
||
name: backendRoute.name,
|
||
path: backendRoute.path,
|
||
componentPath: backendRoute.component || null,
|
||
redirect: backendRoute.redirect || undefined,
|
||
title: backendRoute.menuName,
|
||
icon: backendRoute.icon,
|
||
auth: backendRoute.auth, // 权限标识
|
||
requiresAuth: true, // 动态路由都需要认证
|
||
hide: backendRoute.isHide === '0', // 0-隐藏 1-显示
|
||
keepAlive: backendRoute.isKeepAlive === '0', // 0-是 1-否
|
||
pinTab: backendRoute.isAffix === '0', // 0-是 1-否
|
||
activeMenu: backendRoute.activeMenu || undefined,
|
||
menuType: backendRoute.menuType as AppRoute.MenuType,
|
||
href: backendRoute.isLink === '0' ? backendRoute.path : undefined, // 如果是外链
|
||
}
|
||
}
|
||
|
||
function standardizedRoutes(route: AppRoute.RowRoute[]) {
|
||
return clone(route).map((i) => {
|
||
const route = omit(i, metaFields)
|
||
|
||
Reflect.set(route, 'meta', pick(i, metaFields))
|
||
return route
|
||
}) as AppRoute.Route[]
|
||
}
|
||
|
||
// 处理路由数据的主函数 - 支持动态和静态路由以及混合模式
|
||
export function createRoutes(routeData: (AppRoute.BackendRoute | AppRoute.RowRoute)[]) {
|
||
const { hasPermission } = usePermission()
|
||
|
||
// 处理混合数据:分别处理后端数据和前端数据
|
||
const backendRoutes = routeData.filter(item => 'menuId' in item) as AppRoute.BackendRoute[]
|
||
const frontendRoutes = routeData.filter(item => !('menuId' in item)) as AppRoute.RowRoute[]
|
||
|
||
// 转换后端路由数据
|
||
const transformedBackendRoutes = backendRoutes.map(transformBackendToRoute)
|
||
|
||
// 合并所有路由
|
||
const routes = [...frontendRoutes, ...transformedBackendRoutes]
|
||
|
||
// Structure the meta field
|
||
let resultRouter = standardizedRoutes(routes)
|
||
|
||
// Route permission filtering
|
||
resultRouter = resultRouter.filter(i => hasPermission(i.meta.roles))
|
||
|
||
// Generate routes, no need to import files for those with redirect
|
||
const modules = import.meta.glob('@/views/**/*.vue')
|
||
resultRouter = resultRouter.map((item: AppRoute.Route) => {
|
||
if (item.componentPath && !item.redirect) {
|
||
// 对于动态路由,只有菜单类型才需要组件;对于静态路由,都需要组件
|
||
const needComponent = item.meta.menuType === '2' || !item.meta.menuType
|
||
if (needComponent) {
|
||
// 处理组件路径,确保正确的路径格式
|
||
let componentPath = item.componentPath
|
||
// 确保路径以 / 开头
|
||
if (!componentPath.startsWith('/')) {
|
||
componentPath = `/${componentPath}`
|
||
}
|
||
// 确保路径以 .vue 结尾
|
||
if (!componentPath.endsWith('.vue')) {
|
||
componentPath = `${componentPath}.vue`
|
||
}
|
||
const fullPath = `/src/views${componentPath}`
|
||
const originalComponent = modules[fullPath]
|
||
|
||
// 如果组件未找到,输出调试信息并提供默认组件
|
||
if (!originalComponent) {
|
||
// console.warn(`组件未找到: ${fullPath}`)
|
||
// console.warn('可用组件路径:', Object.keys(modules).slice(0, 10)) // 只显示前10个避免日志过长
|
||
|
||
// 为找不到组件的页面提供一个默认的空页面组件
|
||
item.component = safeAsyncComponent(
|
||
() => Promise.resolve({
|
||
template: `
|
||
<div class="p-4">
|
||
<div class="text-center text-gray-500">
|
||
<h3>页面开发中</h3>
|
||
<p>组件路径: ${fullPath}</p>
|
||
<p>请联系开发人员创建对应的页面组件</p>
|
||
</div>
|
||
</div>
|
||
`,
|
||
}),
|
||
{
|
||
delay: 0,
|
||
timeout: 5000,
|
||
},
|
||
)
|
||
}
|
||
else {
|
||
// 使用安全的异步组件加载器包装原有组件
|
||
item.component = safeAsyncComponent(
|
||
originalComponent as any,
|
||
{
|
||
delay: 100,
|
||
timeout: 10000,
|
||
onError: (error, retry, fail, attempts) => {
|
||
console.error(`组件加载失败: ${fullPath}`, error)
|
||
if (attempts <= 2) {
|
||
retry()
|
||
}
|
||
else {
|
||
fail()
|
||
}
|
||
},
|
||
},
|
||
)
|
||
}
|
||
}
|
||
else if (item.meta.menuType === '1') {
|
||
// 目录类型不需要组件,但需要确保有children
|
||
item.component = undefined
|
||
}
|
||
}
|
||
return item
|
||
})
|
||
|
||
// Generate route tree
|
||
resultRouter = arrayToTree(resultRouter) as AppRoute.Route[]
|
||
|
||
const appRootRoute: RouteRecordRaw = {
|
||
path: '/appRoot',
|
||
name: 'appRoot',
|
||
redirect: import.meta.env.VITE_HOME_PATH || '/dashboard/monitor',
|
||
component: Layout,
|
||
meta: {
|
||
title: '',
|
||
icon: 'icon-park-outline:home',
|
||
},
|
||
children: [],
|
||
}
|
||
|
||
// Set the correct redirect path for the route
|
||
setRedirect(resultRouter)
|
||
|
||
// Insert the processed route into the root route
|
||
appRootRoute.children = resultRouter as unknown as RouteRecordRaw[]
|
||
return appRootRoute
|
||
}
|
||
|
||
// Generate an array of route names that need to be kept alive
|
||
export function generateCacheRoutes(routes: AppRoute.RowRoute[]) {
|
||
return routes
|
||
.filter(i => i.keepAlive)
|
||
.map(i => i.name)
|
||
}
|
||
|
||
function setRedirect(routes: AppRoute.Route[]) {
|
||
routes.forEach((route) => {
|
||
if (route.children) {
|
||
if (!route.redirect) {
|
||
// Filter out a collection of child elements that are not hidden
|
||
const visibleChilds = route.children.filter(child => !child.meta.hide)
|
||
|
||
// Redirect page to the path of the first child element by default
|
||
let target = visibleChilds[0]
|
||
|
||
// Filter out pages with the order attribute
|
||
const orderChilds = visibleChilds.filter(child => child.meta.order)
|
||
|
||
if (orderChilds.length > 0)
|
||
target = min(orderChilds, i => i.meta.order!) as AppRoute.Route
|
||
|
||
if (target)
|
||
route.redirect = target.path
|
||
}
|
||
|
||
setRedirect(route.children)
|
||
}
|
||
})
|
||
}
|
||
|
||
/* 生成侧边菜单的数据 */
|
||
export function createMenus(routeData: (AppRoute.BackendRoute | AppRoute.RowRoute)[]) {
|
||
// 处理混合数据:分别处理后端数据和前端数据
|
||
const backendRoutes = routeData.filter(item => 'menuId' in item) as AppRoute.BackendRoute[]
|
||
const frontendRoutes = routeData.filter(item => !('menuId' in item)) as AppRoute.RowRoute[]
|
||
|
||
// 转换后端路由数据
|
||
const transformedBackendRoutes = backendRoutes.map(transformBackendToRoute)
|
||
|
||
// 合并所有路由
|
||
const userRoutes = [...frontendRoutes, ...transformedBackendRoutes]
|
||
|
||
const resultMenus = standardizedRoutes(userRoutes)
|
||
|
||
// filter menus that do not need to be displayed
|
||
const visibleMenus = resultMenus.filter(route => !route.meta.hide && route.meta.menuType !== '3') // 过滤按钮类型
|
||
|
||
// 处理权限过滤和父子关系
|
||
const menusWithPermission = processMenuPermissions(visibleMenus)
|
||
|
||
// generate side menu
|
||
return arrayToTree(transformAuthRoutesToMenus(menusWithPermission))
|
||
}
|
||
|
||
// 处理菜单权限,确保有子菜单权限时父菜单也可见
|
||
function processMenuPermissions(routes: AppRoute.Route[]): AppRoute.Route[] {
|
||
const { hasPermission } = usePermission()
|
||
|
||
// 创建路由映射表
|
||
const routeMap = new Map<number | null, AppRoute.Route>()
|
||
routes.forEach(route => routeMap.set(route.id, route))
|
||
|
||
// 找出有权限的路由
|
||
const authorizedRoutes = new Set<number | null>()
|
||
|
||
routes.forEach((route) => {
|
||
if (hasPermission(route.meta.roles)) {
|
||
authorizedRoutes.add(route.id)
|
||
|
||
// 如果是页面类型(menuType='2')或没有menuType的路由,确保其父菜单也被包含
|
||
if (!route.meta.menuType || route.meta.menuType === '2') {
|
||
let parentId = route.pid
|
||
while (parentId !== null && parentId !== undefined) {
|
||
const parentRoute = routeMap.get(parentId)
|
||
if (parentRoute) {
|
||
authorizedRoutes.add(parentId)
|
||
parentId = parentRoute.pid
|
||
}
|
||
else {
|
||
break
|
||
}
|
||
}
|
||
}
|
||
}
|
||
})
|
||
|
||
// 返回有权限的路由
|
||
return routes.filter(route => authorizedRoutes.has(route.id))
|
||
}
|
||
|
||
// render the returned routing table as a sidebar
|
||
function transformAuthRoutesToMenus(userRoutes: AppRoute.Route[]) {
|
||
return userRoutes
|
||
// Sort the menu according to the order size
|
||
.sort((a, b) => {
|
||
if (a.meta && a.meta.order && b.meta && b.meta.order)
|
||
return a.meta.order - b.meta.order
|
||
else if (a.meta && a.meta.order)
|
||
return -1
|
||
else if (b.meta && b.meta.order)
|
||
return 1
|
||
else return 0
|
||
})
|
||
// Convert to side menu data structure
|
||
.map((item) => {
|
||
const target: MenuOption = {
|
||
id: item.id,
|
||
pid: item.pid,
|
||
label:
|
||
(!item.meta.menuType || item.meta.menuType === '2')
|
||
? () =>
|
||
h(
|
||
RouterLink,
|
||
{
|
||
to: {
|
||
path: item.path,
|
||
},
|
||
},
|
||
{ default: () => item.meta.title },
|
||
)
|
||
: () => item.meta.title,
|
||
key: item.path,
|
||
icon: item.meta.icon ? renderIcon(item.meta.icon) : undefined,
|
||
}
|
||
return target
|
||
})
|
||
}
|