coder-common-thin-frontend/src/store/router/helper.ts
Leo 7de53bcc6d feat(router): 优化路由系统并支持权限标识
- 扩展路由元数据字段,新增auth权限标识支持
- 完善路由转换器,支持从后端菜单数据提取权限标识
- 优化静态路由配置,为仪表板添加固定标签页属性
- 增强路由类型定义,支持更灵活的权限验证机制
2025-07-08 10:54:10 +08:00

289 lines
9.8 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 { 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
})
}