diff --git a/src/api/module/pmn.ts b/src/api/module/pmn.ts
index 575bf80..59646a3 100644
--- a/src/api/module/pmn.ts
+++ b/src/api/module/pmn.ts
@@ -18,6 +18,8 @@ export interface MenuItem {
children?: MenuItem[];
is_web_page?: boolean; // 是否为网页
web_url?: string; // 嵌入的网页URL
+ component?: string; // 路由文件地址
+ is_new_tab?: boolean; // 是否在新窗口打开
}
/** 获取菜单 */
diff --git a/src/api/module/user.ts b/src/api/module/user.ts
index 0cd83d7..300b3a4 100644
--- a/src/api/module/user.ts
+++ b/src/api/module/user.ts
@@ -18,7 +18,7 @@ export const modifyUser = (data: UserItem) => request.post("/rbac2/v1/user/modif
/** 删除用户 */
export const deleteUser = (data: { id: UserItem['id'] }) => request.post("/rbac2/v1/user/del", data);
/** 获取所有权限【树形】 */
-export const getUserPmnTree = (data: { id: UserItem['id'] }) => request.post("/rbac2/v1/user/pmn_tree", data);
+export const getUserPmnTree = (data: { id: UserItem['id'], workspace: string }) => request.post("/rbac2/v1/user/pmn_tree", data);
/** 用户-获取所有权限【平面】 */
export const userPmn = (data: { id: UserItem['id'], workspace: string }) => request.post("/rbac2/v1/user/pmn", data, { needWorkspace: true });
@@ -39,3 +39,11 @@ export const resetUserPassword = (data: {
/** 手机号 */
phone: string;
}) => request.post("/rbac2/v1/reset", data);
+
+/** 管理员重置用户密码 */
+export const adminResetUserPassword = (data: {
+ /** 用户ID */
+ id: number;
+ /** 新密码 */
+ password: string;
+}) => request.post("/rbac2/v1/user/reset_password", data);
diff --git a/src/api/types.ts b/src/api/types.ts
index ea4bbd0..d9604ed 100644
--- a/src/api/types.ts
+++ b/src/api/types.ts
@@ -15,12 +15,16 @@ export interface LoginData {
/** 用户信息 */
export interface UserItem {
id?: number
+ account?: string
+ name?: string
username?: string
nickname?: string
email?: string
phone?: string
avatar?: string
status?: number
+ role_id?: number
+ role_name?: string
created_at?: string
updated_at?: string
}
diff --git a/src/components/menu/index.vue b/src/components/menu/index.vue
index ca57b1f..77d97bb 100644
--- a/src/components/menu/index.vue
+++ b/src/components/menu/index.vue
@@ -34,8 +34,8 @@ export default defineComponent({
const goto = (item: RouteRecordRaw) => {
// Open external link
- if (regexUrl.test(item.path)) {
- openWindow(item.path)
+ if (regexUrl.test(item.path) || item.meta?.isNewTab) {
+ openWindow(`${window.location.origin}/#${item.path}`)
selectedKey.value = [item.name as string]
return
}
@@ -45,6 +45,7 @@ export default defineComponent({
selectedKey.value = [item.name as string]
return
}
+ console.log('item', item)
// Trigger router change
router.push({
name: item.name,
@@ -114,14 +115,14 @@ export default defineComponent({
key={element?.name}
v-slots={{
icon,
- title: () => h(compile(t(element?.meta?.locale || ''))),
+ title: () => element?.meta?.locale,
}}
>
{travel(element?.children)}
) : (
goto(element)}>
- {t(element?.meta?.locale || '')}
+ {element?.meta?.locale}
)
nodes.push(node as never)
diff --git a/src/components/navbar/index.vue b/src/components/navbar/index.vue
index d8a7502..42cc185 100644
--- a/src/components/navbar/index.vue
+++ b/src/components/navbar/index.vue
@@ -19,7 +19,7 @@
-->
-
+
diff --git a/src/layout/default-layout.vue b/src/layout/default-layout.vue
index f248751..8ff2405 100644
--- a/src/layout/default-layout.vue
+++ b/src/layout/default-layout.vue
@@ -1,13 +1,13 @@
-
+
-
+
@@ -71,14 +71,17 @@ const router = useRouter()
const route = useRoute()
const permission = usePermission()
useResponsive(true)
-const navbarHeight = `60px`
+const navbarHeight = route?.meta?.isNewTab ? '0' : `60px`
const navbar = computed(() => appStore.navbar)
const renderMenu = computed(() => appStore.menu && !appStore.topMenu)
const hideMenu = computed(() => appStore.hideMenu)
const footer = computed(() => appStore.footer)
const menuWidth = computed(() => {
+ if (route?.meta?.isNewTab) return 0
return appStore.menuCollapse ? 48 : appStore.menuWidth
})
+
+console.log('route', route)
const paddingStyle = computed(() => {
const paddingLeft = renderMenu.value && !hideMenu.value ? { paddingLeft: `${menuWidth.value}px` } : {}
const paddingTop = navbar.value ? { paddingTop: navbarHeight } : {}
diff --git a/src/router/constants.ts b/src/router/constants.ts
index ee8bf00..e21073e 100644
--- a/src/router/constants.ts
+++ b/src/router/constants.ts
@@ -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',
}
diff --git a/src/router/guard/permission.ts b/src/router/guard/permission.ts
index a2c4b1c..2cfd4df 100644
--- a/src/router/guard/permission.ts
+++ b/src/router/guard/permission.ts
@@ -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()
diff --git a/src/router/index.ts b/src/router/index.ts
index 01c5568..1150ef2 100644
--- a/src/router/index.ts
+++ b/src/router/index.ts
@@ -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 }
diff --git a/src/router/menu-data.ts b/src/router/menu-data.ts
index d58b197..c349549 100644
--- a/src/router/menu-data.ts
+++ b/src/router/menu-data.ts
@@ -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: ['*'],
},
},
],
diff --git a/src/store/modules/app/index.ts b/src/store/modules/app/index.ts
index 8c8cc3b..36541a2 100644
--- a/src/store/modules/app/index.ts
+++ b/src/store/modules/app/index.ts
@@ -4,9 +4,10 @@ import type { NotificationReturn } from '@arco-design/web-vue/es/notification/in
import type { RouteRecordNormalized } from 'vue-router'
import defaultSettings from '@/config/settings.json'
import { userPmn } from '@/api/module/user'
-import { localMenuData } from '@/router/menu-data'
+import { localMenuData, transformMenuToRoutes, type ServerMenuItem } from '@/router/menu-data'
import { buildTree } from '@/utils/tree'
import SafeStorage, { AppStorageKey } from "@/utils/safeStorage";
+import router from '@/router'
import { AppState } from './types'
@@ -57,19 +58,54 @@ const useAppStore = defineStore('app', {
const res = await userPmn({ id: userInfo.user_id, workspace: import.meta.env.VITE_APP_WORKSPACE })
console.log('res', res)
if (res.code === 0 && res?.details?.length) {
- console.log('buildTree', buildTree(res.details[0].permissions))
+ // 使用 buildTree 将扁平数据构建为树结构
+ const treeResult = buildTree(res.details[0].permissions as ServerMenuItem[], {
+ orderKey: 'order'
+ })
+ console.log('buildTree', treeResult)
+
+ // 使用 transformMenuToRoutes 将树结构转换为路由配置
+ const routes = transformMenuToRoutes(treeResult.rootItems as ServerMenuItem[])
+ console.log('transformMenuToRoutes', routes)
+
+ // 动态注册路由
+ routes.forEach((route) => {
+ // 打印路由结构以便调试
+ // console.log('Registering route:', JSON.stringify(route, (key, value) => {
+ // if (typeof value === 'function') return '[Function]'
+ // return value
+ // }, 2))
+ router.addRoute(route as any)
+ })
+
+ this.serverMenu = routes as unknown as RouteRecordNormalized[]
+ } else {
+ // 如果接口返回数据为空,使用本地数据
+ localMenuData.forEach((route) => {
+ router.addRoute(route as any)
+ })
+ this.serverMenu = localMenuData as unknown as RouteRecordNormalized[]
}
- // this.serverMenu = data
-
- // 使用本地数据
- this.serverMenu = localMenuData as unknown as RouteRecordNormalized[]
} catch (error) {
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
-
+ // 接口失败时使用本地数据
+ console.error('fetchServerMenuConfig error:', error)
+ localMenuData.forEach((route) => {
+ router.addRoute(route as any)
+ })
+ this.serverMenu = localMenuData as unknown as RouteRecordNormalized[]
}
},
clearServerMenu() {
+ // 清除动态注册的路由
+ if (this.serverMenu && this.serverMenu.length > 0) {
+ this.serverMenu.forEach((route) => {
+ const routeName = (route as any).name
+ if (routeName && typeof routeName === 'string') {
+ router.removeRoute(routeName)
+ }
+ })
+ }
this.serverMenu = []
},
},
diff --git a/src/store/modules/app/types.ts b/src/store/modules/app/types.ts
index c971b7f..d5fc9dd 100644
--- a/src/store/modules/app/types.ts
+++ b/src/store/modules/app/types.ts
@@ -16,5 +16,6 @@ export interface AppState {
tabBar: boolean
menuFromServer: boolean
serverMenu: RouteRecordNormalized[]
+ workspace?: string
[key: string]: unknown
}
diff --git a/src/views/login/components/login-form.vue b/src/views/login/components/login-form.vue
index 6a8e784..093b401 100644
--- a/src/views/login/components/login-form.vue
+++ b/src/views/login/components/login-form.vue
@@ -46,7 +46,7 @@
+
+
diff --git a/src/views/ops/pages/system-settings/account-management/components/PermissionSettingDialog.vue b/src/views/ops/pages/system-settings/account-management/components/PermissionSettingDialog.vue
new file mode 100644
index 0000000..464dc01
--- /dev/null
+++ b/src/views/ops/pages/system-settings/account-management/components/PermissionSettingDialog.vue
@@ -0,0 +1,172 @@
+
+
+
+
+
+
+
+
+
+ 取消
+
+ 确定
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/ops/pages/system-settings/account-management/components/PermissionTree.vue b/src/views/ops/pages/system-settings/account-management/components/PermissionTree.vue
new file mode 100644
index 0000000..ada258e
--- /dev/null
+++ b/src/views/ops/pages/system-settings/account-management/components/PermissionTree.vue
@@ -0,0 +1,241 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ item.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/ops/pages/system-settings/account-management/index.vue b/src/views/ops/pages/system-settings/account-management/index.vue
new file mode 100644
index 0000000..bb2201d
--- /dev/null
+++ b/src/views/ops/pages/system-settings/account-management/index.vue
@@ -0,0 +1,409 @@
+
+
+
+
+
+
+
+
+
+ 新增用户
+
+
+
+
+
+
+
+ 编辑
+
+
+ 权限
+
+
+ 改密
+
+
+ 删除
+
+
+
+
+
+
+
+ {{ record.status === 1 ? '启用' : '禁用' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 正常
+ 禁用
+
+
+
+
+
+
+
+ 确定要删除用户 "{{ userToDelete?.name || userToDelete?.account }}" 吗?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/ops/pages/system-settings/menu-management/components/MenuForm.vue b/src/views/ops/pages/system-settings/menu-management/components/MenuForm.vue
index 31c341d..2e6e8ca 100644
--- a/src/views/ops/pages/system-settings/menu-management/components/MenuForm.vue
+++ b/src/views/ops/pages/system-settings/menu-management/components/MenuForm.vue
@@ -43,19 +43,34 @@
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
@@ -66,16 +81,29 @@
+
+ 路由配置
+
+
+
+
+
+ 在新窗口打开
+
+
+
+
+
+
+ 嵌入网页
+
+
+
+
网页嵌入配置
-
-
-
- 是否为嵌入网页
-
-
@@ -154,12 +182,14 @@ const formData = ref({
description: '',
menu_icon: '',
menu_path: '',
+ component: '',
parent_id: undefined,
title: '',
title_en: '',
type: 1,
is_web_page: false,
web_url: '',
+ is_new_tab: false,
// 编辑时需要的额外字段
id: undefined,
identity: undefined,
@@ -194,12 +224,14 @@ const resetForm = () => {
description: '',
menu_icon: '',
menu_path: '',
+ component: '',
parent_id: props.parentId || undefined,
title: '',
title_en: '',
type: 1,
is_web_page: false,
web_url: '',
+ is_new_tab: false,
id: undefined,
identity: undefined,
app_id: undefined,
@@ -227,12 +259,14 @@ watch(
description: initialValues.description || '',
menu_icon: initialValues.menu_icon || '',
menu_path: initialValues.menu_path || '',
+ component: initialValues.component || '',
parent_id: initialValues.parent_id,
title: initialValues.title || '',
title_en: initialValues.title_en || '',
type: initialValues.type || 1,
is_web_page: initialValues.is_web_page || false,
web_url: initialValues.web_url || '',
+ is_new_tab: initialValues.is_new_tab || false,
}
}
},
@@ -253,6 +287,11 @@ const selectIcon = (icon: string) => {
iconSearch.value = ''
}
+// 清空图标
+const clearIcon = () => {
+ formData.value.menu_icon = ''
+}
+
// 提交表单
const handleSubmit = async () => {
try {
@@ -296,10 +335,11 @@ export default {
.icon-grid {
display: grid;
- grid-template-columns: repeat(6, 1fr);
+ grid-template-columns: repeat(5, 1fr);
gap: 8px;
max-height: 400px;
overflow-y: auto;
+ overflow-x: hidden;
}
.icon-item {
diff --git a/src/views/ops/pages/system-settings/menu-management/components/MenuTree.vue b/src/views/ops/pages/system-settings/menu-management/components/MenuTree.vue
index f63ffb3..b62bfc2 100644
--- a/src/views/ops/pages/system-settings/menu-management/components/MenuTree.vue
+++ b/src/views/ops/pages/system-settings/menu-management/components/MenuTree.vue
@@ -16,66 +16,29 @@
@drag-end="$emit('drag-end')"
@drop="$emit('drop', $event)"
/>
-
-
-
- 拖放到此处设为根级菜单
-