This commit is contained in:
ygx
2026-03-08 22:41:42 +08:00
parent f7bbb5ee46
commit 180d980514
23 changed files with 1392 additions and 850 deletions

View File

@@ -13,6 +13,6 @@ export const DEFAULT_ROUTE_NAME = 'Workplace'
export const DEFAULT_ROUTE = {
title: 'menu.dashboard.workplace',
name: DEFAULT_ROUTE_NAME,
fullPath: '/dashboard/workplace',
name: 'overview',
fullPath: '/overview',
}

View File

@@ -1,40 +1,73 @@
import NProgress from 'nprogress' // progress bar
import type { Router, RouteRecordNormalized } from 'vue-router'
import type { Router, RouteLocationRaw } from 'vue-router'
import usePermission from '@/hooks/permission'
import { useAppStore, useUserStore } from '@/store'
import { NOT_FOUND, WHITE_LIST } from '../constants'
import { appRoutes } from '../routes'
// 标记菜单是否正在加载,防止重复加载
let isMenuLoading = false
// 标记菜单是否已加载
let isMenuLoaded = false
export default function setupPermissionGuard(router: Router) {
router.beforeEach(async (to, from, next) => {
const appStore = useAppStore()
const userStore = useUserStore()
const Permission = usePermission()
const permissionsAllow = Permission.accessRouter(to)
if (appStore.menuFromServer) {
// 针对来自服务端的菜单配置进行处理
// Handle routing configuration from the server
// 根据需要自行完善来源于服务端的菜单配置的permission逻辑
// Refine the permission logic from the server's menu configuration as needed
if (!appStore.appAsyncMenus.length && !WHITE_LIST.find((el) => el.name === to.name)) {
await appStore.fetchServerMenuConfig()
}
const serverMenuConfig = [...appStore.appAsyncMenus, ...WHITE_LIST]
let exist = false
while (serverMenuConfig.length && !exist) {
const element = serverMenuConfig.shift()
if (element?.name === to.name) exist = true
if (element?.children) {
serverMenuConfig.push(...(element.children as unknown as RouteRecordNormalized[]))
}
}
if (exist && permissionsAllow) {
// 检查是否在白名单中
if (WHITE_LIST.find((el) => el.name === to.name)) {
next()
} else next(NOT_FOUND)
NProgress.done()
return
}
console.log('[Permission Guard] Menu not loaded, loading...')
// 检查动态路由是否已加载(使用标志位而非菜单长度,更可靠)
if (!isMenuLoaded && !isMenuLoading) {
console.log('[Permission Guard] Menu not loaded, loading...')
// 设置加载标志
isMenuLoading = true
try {
// 动态路由未加载,先获取菜单配置并注册路由
await appStore.fetchServerMenuConfig()
// 标记加载完成
isMenuLoaded = true
console.log('[Permission Guard] Menu loaded, redirecting to:', to.path)
// 路由注册后需要重新导航到当前路径
next({ path: to.path, query: to.query, params: to.params, replace: true } as RouteLocationRaw)
} catch (error) {
console.error('[Permission Guard] Failed to load menu:', error)
isMenuLoading = false
next(NOT_FOUND)
} finally {
isMenuLoading = false
}
NProgress.done()
return
}
// 如果正在加载菜单,等待加载完成
if (isMenuLoading) {
next({ path: to.path, query: to.query, params: to.params, replace: true } as RouteLocationRaw)
NProgress.done()
return
}
// 动态路由已加载,直接放行
// 因为路由已经通过 addRoute 注册Vue Router 会自动匹配
if (permissionsAllow) {
next()
} else {
next(NOT_FOUND)
}
} else {
// eslint-disable-next-line no-lonely-if
if (permissionsAllow) next()

View File

@@ -4,7 +4,7 @@ import { createRouter, createWebHashHistory } from 'vue-router'
import createRouteGuard from './guard'
import { appRoutes } from './routes'
import { NOT_FOUND_ROUTE, REDIRECT_MAIN } from './routes/base'
import { DEFAULT_LAYOUT, NOT_FOUND_ROUTE, REDIRECT_MAIN } from './routes/base'
NProgress.configure({ showSpinner: false }) // NProgress Configuration
@@ -25,7 +25,7 @@ const router = createRouter({
},
...appRoutes,
REDIRECT_MAIN,
NOT_FOUND_ROUTE,
// NOT_FOUND_ROUTE,
],
scrollBehavior() {
return { top: 0 }

View File

@@ -1,5 +1,194 @@
import { DEFAULT_LAYOUT } from './routes/base'
import type { AppRouteRecordRaw } from './routes/types'
import type { TreeNodeBase } from '@/utils/tree'
/**
* 服务器返回的菜单数据结构
*/
export interface ServerMenuItem extends TreeNodeBase {
id: number | string
parent_id: number | string | null
name?: string
title?: string // 菜单标题
title_en?: string // 英文标题
code?: string // 菜单编码
menu_path?: string // 菜单路径,如 '/overview'
component?: string // 组件路径,如 'ops/pages/overview'
icon?: string
locale?: string
sort_key?: number // 排序字段
order?: number
hideInMenu?: boolean
hideChildrenInMenu?: boolean
requiresAuth?: boolean
roles?: string[]
children?: ServerMenuItem[]
[key: string]: any
}
// 预定义的视图模块映射(用于 Vite 动态导入)
const viewModules = import.meta.glob('@/views/**/*.vue')
/**
* 动态加载视图组件
* @param componentPath 组件路径,如 'ops/pages/overview' 或 'ops/pages/overview/index'
* @returns 动态导入的组件
*/
export function loadViewComponent(componentPath: string) {
// 将路径转换为完整的视图路径
// 如果路径不以 /index 结尾,自动补全
let fullPath = componentPath
if (!fullPath.endsWith('/index') && !fullPath.endsWith('.vue')) {
fullPath = `${fullPath}/index`
}
// 构建完整的文件路径
const filePath = `/src/views/${fullPath}.vue`
// 从预加载的模块中查找
const modulePath = Object.keys(viewModules).find((path) => path.endsWith(filePath) || path === filePath)
if (modulePath && viewModules[modulePath]) {
return viewModules[modulePath]
}
// 如果找不到,返回一个默认组件或抛出错误
console.warn(`View component not found: ${filePath}`)
return () => import('@/views/redirect/index.vue')
}
/**
* 将服务器菜单数据转换为路由配置
* @param menuItems 服务器返回的菜单项(树状结构)
* @returns 路由配置数组
*/
export function transformMenuToRoutes(menuItems: ServerMenuItem[]): AppRouteRecordRaw[] {
const routes: AppRouteRecordRaw[] = []
for (const item of menuItems) {
const route: AppRouteRecordRaw = {
path: item.menu_path || '',
name: item.title || item.name || `menu_${item.id}`,
meta: {
locale: item.locale || item.title,
requiresAuth: item.requiresAuth !== false,
icon: item.icon,
order: item.sort_key ?? item.order,
hideInMenu: item.hideInMenu,
hideChildrenInMenu: item.hideChildrenInMenu,
roles: item.roles,
isNewTab: item.is_new_tab
},
component: DEFAULT_LAYOUT,
}
// 处理子菜单
if (item.children && item.children.length > 0) {
// 传递父级的 component 和 path 给子路由处理函数
route.children = transformChildRoutes(item.children, item.component, item.menu_path)
} else if (item.component) {
// 一级菜单没有 children 但有 component创建一个空路径的子路由
const routeName = route.name
route.children = [
{
path: item.menu_path || '',
name: typeof routeName === 'string' ? `${routeName}Index` : `menu_${item.id}_index`,
component: loadViewComponent(item.component),
meta: {
locale: item.locale || item.title,
requiresAuth: item.requiresAuth !== false,
isNewTab: item.is_new_tab
},
},
]
}
routes.push(route)
}
return routes
}
/**
* 将路径转换为相对路径(去掉开头的 /
* @param path 路径
* @returns 相对路径
*/
function toRelativePath(path: string): string {
if (!path) return ''
// 去掉开头的 /
return path.startsWith('/') ? path.slice(1) : path
}
/**
* 从完整路径中提取子路由的相对路径
* 例如:父路径 '/dashboard',子路径 '/dashboard/workplace' -> 'workplace'
* @param childPath 子菜单的完整路径
* @param parentPath 父菜单的路径
* @returns 相对路径
*/
function extractRelativePath(childPath: string, parentPath: string): string {
if (!childPath) return ''
// 如果子路径以父路径开头,提取相对部分
if (parentPath && childPath.startsWith(parentPath)) {
let relativePath = childPath.slice(parentPath.length)
// 去掉开头的 /
if (relativePath.startsWith('/')) {
relativePath = relativePath.slice(1)
}
return relativePath
}
// 否则转换为相对路径
return toRelativePath(childPath)
}
/**
* 转换子路由配置
* @param children 子菜单项
* @param parentComponent 父级菜单的 component 字段(用于子菜单没有 component 时继承)
* @param parentPath 父级菜单的路径(用于计算相对路径)
* @returns 子路由配置数组
*/
function transformChildRoutes(
children: ServerMenuItem[],
parentComponent?: string,
parentPath?: string
): AppRouteRecordRaw[] {
return children.map((child) => {
// 优先使用子菜单自己的 component否则继承父级的 component
const componentPath = child.component || parentComponent
// 计算子路由的相对路径
const childFullPath = child.menu_path || child.path || ''
const relativePath = extractRelativePath(childFullPath, parentPath || '')
const route: AppRouteRecordRaw = {
path: relativePath,
name: child.title || child.name || `menu_${child.id}`,
meta: {
locale: child.locale || child.title,
requiresAuth: child.requiresAuth !== false,
roles: child.roles,
},
component: componentPath
? loadViewComponent(componentPath)
: () => import('@/views/redirect/index.vue'),
}
// 递归处理子菜单的子菜单
if (child.children && child.children.length > 0) {
route.children = transformChildRoutes(
child.children,
child.component || parentComponent,
childFullPath // 传递当前子菜单的完整路径作为下一层的父路径
)
}
return route
})
}
// 本地菜单数据 - 接口未准备好时使用
export const localMenuData: AppRouteRecordRaw[] = [{
@@ -20,10 +209,8 @@ export const localMenuData: AppRouteRecordRaw[] = [{
meta: {
locale: 'menu.dashboard.workplace',
requiresAuth: true,
roles: ['*'],
},
},
/** simple */
{
path: 'monitor',
name: 'Monitor',
@@ -31,10 +218,8 @@ export const localMenuData: AppRouteRecordRaw[] = [{
meta: {
locale: 'menu.dashboard.monitor',
requiresAuth: true,
roles: ['admin'],
},
},
/** simple end */
],
},
{
@@ -46,687 +231,15 @@ export const localMenuData: AppRouteRecordRaw[] = [{
requiresAuth: true,
icon: 'icon-home',
order: 1,
hideChildrenInMenu: true,
},
children: [
{
path: '',
name: 'OverviewIndex',
component: () => import('@/views/redirect/index.vue'),
component: () => import('@/views/ops/pages/overview/index.vue'),
meta: {
locale: '系统概况',
requiresAuth: true,
roles: ['*'],
},
},
],
},
{
path: '/visual',
name: 'VisualDashboard',
component: DEFAULT_LAYOUT,
meta: {
locale: '可视化大屏管理',
requiresAuth: true,
icon: 'icon-apps',
order: 2,
},
children: [
{
path: 'component',
name: 'VisualComponent',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: '大屏管理',
requiresAuth: true,
roles: ['*'],
},
},
{
path: 'view-publish',
name: 'ViewPublish',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: '已发布大屏列表',
requiresAuth: true,
roles: ['*'],
},
},
],
},
{
path: '/dc',
name: 'DataCenter',
component: DEFAULT_LAYOUT,
meta: {
locale: '服务器及PC管理',
requiresAuth: true,
icon: 'icon-storage',
order: 3,
},
children: [
{
path: 'pc',
name: 'OfficePC',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: '办公PC管理',
requiresAuth: true,
roles: ['*'],
},
},
{
path: 'server',
name: 'ServerManagement',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: '服务器管理',
requiresAuth: true,
roles: ['*'],
},
},
],
},
{
path: '/dc/cluster',
name: 'ClusterCollection',
component: DEFAULT_LAYOUT,
meta: {
locale: '集群采集控制中心',
requiresAuth: true,
icon: 'icon-settings',
order: 4,
},
children: [
{
path: 'database',
name: 'DatabaseCollection',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: '数据库采集管理',
requiresAuth: true,
roles: ['*'],
},
},
{
path: 'middleware',
name: 'MiddlewareCollection',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: '中间件采集管理',
requiresAuth: true,
roles: ['*'],
},
},
{
path: 'network',
name: 'NetworkDeviceCollection',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: '网络设备采集管理',
requiresAuth: true,
roles: ['*'],
},
},
],
},
{
path: '/monitor',
name: 'Monitor',
component: DEFAULT_LAYOUT,
meta: {
locale: '综合监控',
requiresAuth: true,
icon: 'icon-desktop',
order: 5,
},
children: [
{
path: 'log',
name: 'LogMonitor',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: '日志监控',
requiresAuth: true,
roles: ['*'],
},
},
{
path: 'virtualization',
name: 'VirtualizationMonitor',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: '虚拟化监控',
requiresAuth: true,
roles: ['*'],
},
},
{
path: 'storage',
name: 'StorageMonitor',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: '存储设备监控',
requiresAuth: true,
roles: ['*'],
},
},
{
path: 'network',
name: 'NetworkMonitor',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: '网络设备监控',
requiresAuth: true,
roles: ['*'],
},
},
{
path: 'power',
name: 'PowerMonitor',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: '电力/UPS/空调/温湿度',
requiresAuth: true,
roles: ['*'],
},
},
{
path: 'url',
name: 'URLMonitor',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: 'URL监控',
requiresAuth: true,
roles: ['*'],
},
},
{
path: 'guard',
name: 'GuardMonitor',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: '消防/门禁/漏水/有害气体',
requiresAuth: true,
roles: ['*'],
},
},
{
path: 'security',
name: 'SecurityMonitor',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: '安全设备监控',
requiresAuth: true,
roles: ['*'],
},
},
],
},
{
path: '/netarch',
name: 'NetworkArchitecture',
component: DEFAULT_LAYOUT,
meta: {
locale: '网络架构管理',
requiresAuth: true,
icon: 'icon-nav',
order: 6,
},
children: [
{
path: 'auto-topo',
name: 'AutoTopo',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: '自动感知拓扑图',
requiresAuth: true,
roles: ['*'],
},
},
{
path: 'topo-group',
name: 'TopoGroup',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: '拓扑管理',
requiresAuth: true,
roles: ['*'],
},
},
{
path: 'traffic',
name: 'TrafficAnalysis',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: '流量分析管理',
requiresAuth: true,
roles: ['*'],
},
},
{
path: 'ip',
name: 'IPAddress',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: 'IP地址管理',
requiresAuth: true,
roles: ['*'],
},
},
],
},
{
path: '/alert',
name: 'Alert',
component: DEFAULT_LAYOUT,
meta: {
locale: '告警管理',
requiresAuth: true,
icon: 'icon-bulb',
order: 7,
},
children: [
{
path: 'setting',
name: 'AlertSetting',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: '告警策略管理',
requiresAuth: true,
roles: ['*'],
},
},
{
path: 'tackle',
name: 'AlertTackle',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: '告警受理处理',
requiresAuth: true,
roles: ['*'],
},
},
{
path: 'history',
name: 'AlertHistory',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: '告警历史',
requiresAuth: true,
roles: ['*'],
},
},
{
path: 'template',
name: 'AlertTemplate',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: '告警模版',
requiresAuth: true,
roles: ['*'],
},
},
{
path: 'notice',
name: 'AlertNotice',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: '告警通知设置',
requiresAuth: true,
roles: ['*'],
},
},
{
path: 'level',
name: 'AlertLevel',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: '告警级别管理',
requiresAuth: true,
roles: ['*'],
},
},
],
},
{
path: '/feedback',
name: 'Feedback',
component: DEFAULT_LAYOUT,
meta: {
locale: '工单管理',
requiresAuth: true,
icon: 'icon-list',
order: 8,
},
children: [
{
path: 'all',
name: 'AllTickets',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: '所有工单',
requiresAuth: true,
roles: ['*'],
},
},
{
path: 'undo',
name: 'PendingTickets',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: '我的工单',
requiresAuth: true,
roles: ['*'],
},
},
],
},
{
path: '/datacenter',
name: 'DataCenterManagement',
component: DEFAULT_LAYOUT,
meta: {
locale: '数据中心管理',
requiresAuth: true,
icon: 'icon-drive-file',
order: 9,
},
children: [
{
path: 'rack',
name: 'RackManagement',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: '机柜管理',
requiresAuth: true,
roles: ['*'],
},
},
{
path: 'house',
name: 'DataCenterHouse',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: '数据中心',
requiresAuth: true,
roles: ['*'],
},
},
{
path: 'floor',
name: 'FloorManagement',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: '楼层管理',
requiresAuth: true,
roles: ['*'],
},
},
],
},
{
path: '/assets',
name: 'Assets',
component: DEFAULT_LAYOUT,
meta: {
locale: '资产管理',
requiresAuth: true,
icon: 'icon-apps',
order: 10,
},
children: [
{
path: 'classify',
name: 'AssetClassify',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: '设备分类管理',
requiresAuth: true,
roles: ['*'],
},
},
{
path: 'device',
name: 'AssetDevice',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: '设备管理',
requiresAuth: true,
roles: ['*'],
},
},
{
path: 'supplier',
name: 'AssetSupplier',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: '供应商管理',
requiresAuth: true,
roles: ['*'],
},
},
],
},
{
path: '/kb',
name: 'KnowledgeBase',
component: DEFAULT_LAYOUT,
meta: {
locale: '知识库管理',
requiresAuth: true,
icon: 'icon-file',
order: 11,
},
children: [
{
path: 'items',
name: 'KnowledgeItems',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: '知识管理',
requiresAuth: true,
roles: ['*'],
},
},
{
path: 'tags',
name: 'KnowledgeTags',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: '标签管理',
requiresAuth: true,
roles: ['*'],
},
},
{
path: 'review',
name: 'KnowledgeReview',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: '我的审核',
requiresAuth: true,
roles: ['*'],
},
},
{
path: 'favorite',
name: 'KnowledgeFavorite',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: '我的收藏',
requiresAuth: true,
roles: ['*'],
},
},
{
path: 'recycle',
name: 'KnowledgeRecycle',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: '回收站',
requiresAuth: true,
roles: ['*'],
},
},
],
},
{
path: '/report',
name: 'Report',
component: DEFAULT_LAYOUT,
meta: {
locale: '报告管理',
requiresAuth: true,
icon: 'icon-nav',
order: 12,
},
children: [
{
path: 'host',
name: 'ServerReport',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: '服务器报告',
requiresAuth: true,
roles: ['*'],
},
},
{
path: 'fault',
name: 'FaultReport',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: '故障报告',
requiresAuth: true,
roles: ['*'],
},
},
{
path: 'device',
name: 'DeviceReport',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: '网络设备报告',
requiresAuth: true,
roles: ['*'],
},
},
{
path: 'traffic',
name: 'TrafficReport',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: '流量统计报告',
requiresAuth: true,
roles: ['*'],
},
},
{
path: 'history',
name: 'HistoryReport',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: '历史报告',
requiresAuth: true,
roles: ['*'],
},
},
{
path: 'statistics',
name: 'StatisticsReport',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: '统计报告',
requiresAuth: true,
roles: ['*'],
},
},
],
},
{
path: '/system-settings',
name: 'SystemSettings',
component: DEFAULT_LAYOUT,
meta: {
locale: '系统设置',
requiresAuth: true,
icon: 'icon-settings',
order: 13,
},
children: [
{
path: 'system-monitoring',
name: 'SystemMonitoring',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: '系统监控',
requiresAuth: true,
roles: ['*'],
},
},
{
path: 'system-logs',
name: 'SystemLogs',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: '系统日志',
requiresAuth: true,
roles: ['*'],
},
},
{
path: 'account-management',
name: 'AccountManagement',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: '用户管理',
requiresAuth: true,
roles: ['*'],
},
},
{
path: 'menu-management',
name: 'MenuManagement',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: '菜单设置',
requiresAuth: true,
roles: ['*'],
},
},
{
path: 'license-center',
name: 'LicenseCenter',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: '许可授权中心',
requiresAuth: true,
roles: ['*'],
},
},
],
},
{
path: '/help',
name: 'HelpCenter',
component: DEFAULT_LAYOUT,
meta: {
locale: '帮助中心',
requiresAuth: true,
icon: 'icon-bulb',
order: 14,
hideChildrenInMenu: true,
},
children: [
{
path: '',
name: 'HelpCenterIndex',
component: () => import('@/views/redirect/index.vue'),
meta: {
locale: '帮助中心',
requiresAuth: true,
roles: ['*'],
},
},
],