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: `
页面开发中
组件路径: ${fullPath}
请联系开发人员创建对应的页面组件
`,
}),
{
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()
routes.forEach(route => routeMap.set(route.id, route))
// 找出有权限的路由
const authorizedRoutes = new Set()
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
})
}