import { useUserStore } from '@/store/modules/user'
|
import { useAppMode } from '@/hooks/core/useAppMode'
|
import { fetchGetMenuList } from '@/api/auth'
|
import { asyncRoutes } from '../routes/asyncRoutes'
|
import { RoutesAlias } from '../routesAlias'
|
import { adaptBackendMenuTree } from '../adapters/backendMenuAdapter'
|
import { formatMenuTitle } from '@/utils'
|
|
const WORKBENCH_PATH = '/dashboard/console'
|
const WORKBENCH_ROOT_PATH = '/dashboard'
|
|
const WORKBENCH_MENU = Object.freeze({
|
name: 'Dashboard',
|
path: WORKBENCH_ROOT_PATH,
|
component: RoutesAlias.Layout,
|
redirect: WORKBENCH_PATH,
|
meta: {
|
title: 'menus.dashboard.title',
|
icon: 'ri:home-smile-2-line',
|
staticRoute: true
|
},
|
children: [
|
{
|
name: 'Console',
|
path: 'console',
|
component: WORKBENCH_PATH,
|
meta: {
|
title: 'menus.dashboard.console',
|
icon: 'ri:home-smile-2-line',
|
keepAlive: false,
|
fixedTab: true,
|
staticRoute: true
|
}
|
}
|
]
|
})
|
|
class MenuProcessor {
|
/**
|
* 获取菜单数据
|
*/
|
async getMenuList() {
|
const { isFrontendMode } = useAppMode()
|
let menuList
|
if (isFrontendMode.value) {
|
menuList = await this.processFrontendMenu()
|
} else {
|
menuList = await this.processBackendMenu()
|
}
|
menuList = this.prependWorkbenchMenu(menuList)
|
this.validateMenuPaths(menuList)
|
return this.normalizeMenuPaths(menuList)
|
}
|
/**
|
* 处理前端控制模式的菜单
|
*/
|
async processFrontendMenu() {
|
const userStore = useUserStore()
|
const roles = userStore.info?.roles
|
let menuList = [...asyncRoutes]
|
if (roles && roles.length > 0) {
|
menuList = this.filterMenuByRoles(menuList, roles)
|
}
|
return this.filterEmptyMenus(menuList)
|
}
|
/**
|
* 处理后端控制模式的菜单
|
*/
|
async processBackendMenu() {
|
const list = await fetchGetMenuList()
|
return adaptBackendMenuTree(list)
|
}
|
/**
|
* 根据角色过滤菜单
|
*/
|
filterMenuByRoles(menu, roles) {
|
return menu.reduce((acc, 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 = this.filterMenuByRoles(filteredItem.children, roles)
|
}
|
acc.push(filteredItem)
|
}
|
return acc
|
}, [])
|
}
|
/**
|
* 递归过滤空菜单项
|
*/
|
filterEmptyMenus(menuList) {
|
return menuList
|
.map((item) => {
|
if (item.children && item.children.length > 0) {
|
const filteredChildren = this.filterEmptyMenus(item.children)
|
return {
|
...item,
|
children: filteredChildren
|
}
|
}
|
return item
|
})
|
.filter((item) => {
|
if ('children' in item) {
|
return true
|
}
|
if (item.meta?.isIframe === true || item.meta?.link) {
|
return true
|
}
|
if (item.component && item.component !== '' && item.component !== RoutesAlias.Layout) {
|
return true
|
}
|
return false
|
})
|
}
|
/**
|
* 验证菜单列表是否有效
|
*/
|
validateMenuList(menuList) {
|
return Array.isArray(menuList) && menuList.length > 0
|
}
|
/**
|
* 规范化菜单路径
|
* 将相对路径转换为完整路径,确保菜单跳转正确
|
*/
|
normalizeMenuPaths(menuList, parentPath = '') {
|
return menuList.map((item) => {
|
const fullPath = this.buildFullPath(item.path || '', parentPath)
|
const children = item.children?.length
|
? this.normalizeMenuPaths(item.children, fullPath)
|
: item.children
|
const redirect = item.redirect || this.resolveDefaultRedirect(children)
|
return {
|
...item,
|
path: fullPath,
|
redirect,
|
children
|
}
|
})
|
}
|
/**
|
* 为目录型菜单推导默认跳转地址
|
*/
|
resolveDefaultRedirect(children) {
|
if (!children?.length) {
|
return void 0
|
}
|
for (const child of children) {
|
if (this.isNavigableRoute(child)) {
|
return child.path
|
}
|
const nestedRedirect = this.resolveDefaultRedirect(child.children)
|
if (nestedRedirect) {
|
return nestedRedirect
|
}
|
}
|
return void 0
|
}
|
/**
|
* 判断子路由是否可以作为默认落点
|
*/
|
isNavigableRoute(route) {
|
return Boolean(
|
route.path &&
|
route.path !== '/' &&
|
!route.meta?.link &&
|
route.meta?.isIframe !== true &&
|
route.component &&
|
route.component !== ''
|
)
|
}
|
/**
|
* 验证菜单路径配置
|
* 检测非一级菜单是否错误使用了 / 开头的路径
|
*/
|
/**
|
* 验证菜单路径配置
|
* 检测非一级菜单是否错误使用了 / 开头的路径
|
*/
|
validateMenuPaths(menuList, level = 1) {
|
menuList.forEach((route) => {
|
if (!route.children?.length) return
|
const parentName = String(route.name || route.path || '未知路由')
|
route.children.forEach((child) => {
|
const childPath = child.path || ''
|
if (this.isValidAbsolutePath(childPath)) return
|
if (childPath.startsWith('/')) {
|
this.logPathError(child, childPath, parentName, level)
|
}
|
})
|
this.validateMenuPaths(route.children, level + 1)
|
})
|
}
|
/**
|
* 判断是否为合法的绝对路径
|
*/
|
isValidAbsolutePath(path) {
|
return (
|
path.startsWith('/') ||
|
path.startsWith('http://') ||
|
path.startsWith('https://') ||
|
path.startsWith('/outside/iframe/')
|
)
|
}
|
/**
|
* 输出路径配置错误日志
|
*/
|
logPathError(route, path, parentName, level) {
|
const routeName = String(route.name || path || '未知路由')
|
const menuTitle = route.meta?.title || routeName
|
const suggestedPath = path.split('/').pop() || path.slice(1)
|
console.error(
|
`[路由配置错误] 菜单 "${formatMenuTitle(menuTitle)}" (name: ${routeName}, path: ${path}) 配置错误
|
位置: ${parentName} > ${routeName}
|
问题: ${level + 1}级菜单的 path 不能以 / 开头
|
当前配置: path: '${path}'
|
应该改为: path: '${suggestedPath}'`
|
)
|
}
|
/**
|
* 构建完整路径
|
*/
|
buildFullPath(path, parentPath) {
|
if (!path) return ''
|
if (path.startsWith('http://') || path.startsWith('https://')) {
|
return path
|
}
|
if (path.startsWith('/')) {
|
return path
|
}
|
if (parentPath) {
|
const cleanParent = parentPath.replace(/\/$/, '')
|
const cleanChild = path.replace(/^\//, '')
|
return `${cleanParent}/${cleanChild}`
|
}
|
return `/${path}`
|
}
|
|
prependWorkbenchMenu(menuList) {
|
if (!Array.isArray(menuList)) {
|
return [this.createWorkbenchMenu()]
|
}
|
|
const hasWorkbench = menuList.some((item) => this.containsWorkbenchMenu(item))
|
if (hasWorkbench) {
|
return menuList
|
}
|
|
return [this.createWorkbenchMenu(), ...menuList]
|
}
|
|
createWorkbenchMenu() {
|
return {
|
...WORKBENCH_MENU,
|
meta: {
|
...WORKBENCH_MENU.meta
|
},
|
children: WORKBENCH_MENU.children.map((child) => ({
|
...child,
|
meta: {
|
...child.meta
|
}
|
}))
|
}
|
}
|
|
isWorkbenchMenu(item) {
|
if (!item || typeof item !== 'object') {
|
return false
|
}
|
const path = String(item.path || '')
|
const component = String(item.component || '')
|
return (
|
path === WORKBENCH_ROOT_PATH ||
|
path === WORKBENCH_PATH ||
|
component === WORKBENCH_PATH
|
)
|
}
|
|
containsWorkbenchMenu(item) {
|
if (this.isWorkbenchMenu(item)) {
|
return true
|
}
|
if (!Array.isArray(item?.children) || item.children.length === 0) {
|
return false
|
}
|
return item.children.some((child) => this.containsWorkbenchMenu(child))
|
}
|
}
|
export { MenuProcessor }
|