feat
This commit is contained in:
@@ -18,6 +18,8 @@ export interface MenuItem {
|
|||||||
children?: MenuItem[];
|
children?: MenuItem[];
|
||||||
is_web_page?: boolean; // 是否为网页
|
is_web_page?: boolean; // 是否为网页
|
||||||
web_url?: string; // 嵌入的网页URL
|
web_url?: string; // 嵌入的网页URL
|
||||||
|
component?: string; // 路由文件地址
|
||||||
|
is_new_tab?: boolean; // 是否在新窗口打开
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取菜单 */
|
/** 获取菜单 */
|
||||||
|
|||||||
@@ -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 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 });
|
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;
|
phone: string;
|
||||||
}) => request.post("/rbac2/v1/reset", data);
|
}) => request.post("/rbac2/v1/reset", data);
|
||||||
|
|
||||||
|
/** 管理员重置用户密码 */
|
||||||
|
export const adminResetUserPassword = (data: {
|
||||||
|
/** 用户ID */
|
||||||
|
id: number;
|
||||||
|
/** 新密码 */
|
||||||
|
password: string;
|
||||||
|
}) => request.post("/rbac2/v1/user/reset_password", data);
|
||||||
|
|||||||
@@ -15,12 +15,16 @@ export interface LoginData {
|
|||||||
/** 用户信息 */
|
/** 用户信息 */
|
||||||
export interface UserItem {
|
export interface UserItem {
|
||||||
id?: number
|
id?: number
|
||||||
|
account?: string
|
||||||
|
name?: string
|
||||||
username?: string
|
username?: string
|
||||||
nickname?: string
|
nickname?: string
|
||||||
email?: string
|
email?: string
|
||||||
phone?: string
|
phone?: string
|
||||||
avatar?: string
|
avatar?: string
|
||||||
status?: number
|
status?: number
|
||||||
|
role_id?: number
|
||||||
|
role_name?: string
|
||||||
created_at?: string
|
created_at?: string
|
||||||
updated_at?: string
|
updated_at?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ export default defineComponent({
|
|||||||
|
|
||||||
const goto = (item: RouteRecordRaw) => {
|
const goto = (item: RouteRecordRaw) => {
|
||||||
// Open external link
|
// Open external link
|
||||||
if (regexUrl.test(item.path)) {
|
if (regexUrl.test(item.path) || item.meta?.isNewTab) {
|
||||||
openWindow(item.path)
|
openWindow(`${window.location.origin}/#${item.path}`)
|
||||||
selectedKey.value = [item.name as string]
|
selectedKey.value = [item.name as string]
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -45,6 +45,7 @@ export default defineComponent({
|
|||||||
selectedKey.value = [item.name as string]
|
selectedKey.value = [item.name as string]
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
console.log('item', item)
|
||||||
// Trigger router change
|
// Trigger router change
|
||||||
router.push({
|
router.push({
|
||||||
name: item.name,
|
name: item.name,
|
||||||
@@ -114,14 +115,14 @@ export default defineComponent({
|
|||||||
key={element?.name}
|
key={element?.name}
|
||||||
v-slots={{
|
v-slots={{
|
||||||
icon,
|
icon,
|
||||||
title: () => h(compile(t(element?.meta?.locale || ''))),
|
title: () => element?.meta?.locale,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{travel(element?.children)}
|
{travel(element?.children)}
|
||||||
</a-sub-menu>
|
</a-sub-menu>
|
||||||
) : (
|
) : (
|
||||||
<a-menu-item key={element?.name} v-slots={{ icon }} onClick={() => goto(element)}>
|
<a-menu-item key={element?.name} v-slots={{ icon }} onClick={() => goto(element)}>
|
||||||
{t(element?.meta?.locale || '')}
|
{element?.meta?.locale}
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
)
|
)
|
||||||
nodes.push(node as never)
|
nodes.push(node as never)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
</a-button>
|
</a-button>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</li> -->
|
</li> -->
|
||||||
<li>
|
<!-- <li>
|
||||||
<a-tooltip :content="$t('settings.language')">
|
<a-tooltip :content="$t('settings.language')">
|
||||||
<a-button class="nav-btn" type="outline" :shape="'circle'" @click="setDropDownVisible">
|
<a-button class="nav-btn" type="outline" :shape="'circle'" @click="setDropDownVisible">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
</a-doption>
|
</a-doption>
|
||||||
</template>
|
</template>
|
||||||
</a-dropdown>
|
</a-dropdown>
|
||||||
</li>
|
</li> -->
|
||||||
<li>
|
<li>
|
||||||
<a-tooltip :content="theme === 'light' ? $t('settings.navbar.theme.toDark') : $t('settings.navbar.theme.toLight')">
|
<a-tooltip :content="theme === 'light' ? $t('settings.navbar.theme.toDark') : $t('settings.navbar.theme.toLight')">
|
||||||
<a-button class="nav-btn" type="outline" :shape="'circle'" @click="handleToggleTheme">
|
<a-button class="nav-btn" type="outline" :shape="'circle'" @click="handleToggleTheme">
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<a-layout class="layout" :class="{ mobile: appStore.hideMenu }">
|
<a-layout class="layout" :class="{ mobile: appStore.hideMenu }">
|
||||||
<div v-if="navbar" class="layout-navbar">
|
<div v-if="navbar && !route?.meta?.isNewTab" class="layout-navbar">
|
||||||
<NavBar />
|
<NavBar />
|
||||||
</div>
|
</div>
|
||||||
<a-layout>
|
<a-layout>
|
||||||
<a-layout>
|
<a-layout>
|
||||||
<a-layout-sider
|
<a-layout-sider
|
||||||
v-if="renderMenu"
|
v-if="renderMenu"
|
||||||
v-show="!hideMenu"
|
v-show="!hideMenu && !route?.meta?.isNewTab"
|
||||||
class="layout-sider"
|
class="layout-sider"
|
||||||
:breakpoint="'xl'"
|
:breakpoint="'xl'"
|
||||||
:collapsible="true"
|
:collapsible="true"
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
<Menu />
|
<Menu />
|
||||||
</a-drawer>
|
</a-drawer>
|
||||||
<a-layout class="layout-content" :style="paddingStyle">
|
<a-layout class="layout-content" :style="paddingStyle">
|
||||||
<TabBar v-if="appStore.tabBar" />
|
<!-- <TabBar v-if="appStore.tabBar" /> -->
|
||||||
<a-layout-content>
|
<a-layout-content>
|
||||||
<PageLayout />
|
<PageLayout />
|
||||||
</a-layout-content>
|
</a-layout-content>
|
||||||
@@ -71,14 +71,17 @@ const router = useRouter()
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const permission = usePermission()
|
const permission = usePermission()
|
||||||
useResponsive(true)
|
useResponsive(true)
|
||||||
const navbarHeight = `60px`
|
const navbarHeight = route?.meta?.isNewTab ? '0' : `60px`
|
||||||
const navbar = computed(() => appStore.navbar)
|
const navbar = computed(() => appStore.navbar)
|
||||||
const renderMenu = computed(() => appStore.menu && !appStore.topMenu)
|
const renderMenu = computed(() => appStore.menu && !appStore.topMenu)
|
||||||
const hideMenu = computed(() => appStore.hideMenu)
|
const hideMenu = computed(() => appStore.hideMenu)
|
||||||
const footer = computed(() => appStore.footer)
|
const footer = computed(() => appStore.footer)
|
||||||
const menuWidth = computed(() => {
|
const menuWidth = computed(() => {
|
||||||
|
if (route?.meta?.isNewTab) return 0
|
||||||
return appStore.menuCollapse ? 48 : appStore.menuWidth
|
return appStore.menuCollapse ? 48 : appStore.menuWidth
|
||||||
})
|
})
|
||||||
|
|
||||||
|
console.log('route', route)
|
||||||
const paddingStyle = computed(() => {
|
const paddingStyle = computed(() => {
|
||||||
const paddingLeft = renderMenu.value && !hideMenu.value ? { paddingLeft: `${menuWidth.value}px` } : {}
|
const paddingLeft = renderMenu.value && !hideMenu.value ? { paddingLeft: `${menuWidth.value}px` } : {}
|
||||||
const paddingTop = navbar.value ? { paddingTop: navbarHeight } : {}
|
const paddingTop = navbar.value ? { paddingTop: navbarHeight } : {}
|
||||||
|
|||||||
@@ -13,6 +13,6 @@ export const DEFAULT_ROUTE_NAME = 'Workplace'
|
|||||||
|
|
||||||
export const DEFAULT_ROUTE = {
|
export const DEFAULT_ROUTE = {
|
||||||
title: 'menu.dashboard.workplace',
|
title: 'menu.dashboard.workplace',
|
||||||
name: DEFAULT_ROUTE_NAME,
|
name: 'overview',
|
||||||
fullPath: '/dashboard/workplace',
|
fullPath: '/overview',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +1,73 @@
|
|||||||
import NProgress from 'nprogress' // progress bar
|
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 usePermission from '@/hooks/permission'
|
||||||
import { useAppStore, useUserStore } from '@/store'
|
import { useAppStore, useUserStore } from '@/store'
|
||||||
import { NOT_FOUND, WHITE_LIST } from '../constants'
|
import { NOT_FOUND, WHITE_LIST } from '../constants'
|
||||||
import { appRoutes } from '../routes'
|
import { appRoutes } from '../routes'
|
||||||
|
|
||||||
|
// 标记菜单是否正在加载,防止重复加载
|
||||||
|
let isMenuLoading = false
|
||||||
|
// 标记菜单是否已加载
|
||||||
|
let isMenuLoaded = false
|
||||||
|
|
||||||
export default function setupPermissionGuard(router: Router) {
|
export default function setupPermissionGuard(router: Router) {
|
||||||
router.beforeEach(async (to, from, next) => {
|
router.beforeEach(async (to, from, next) => {
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const Permission = usePermission()
|
const Permission = usePermission()
|
||||||
const permissionsAllow = Permission.accessRouter(to)
|
const permissionsAllow = Permission.accessRouter(to)
|
||||||
|
|
||||||
if (appStore.menuFromServer) {
|
if (appStore.menuFromServer) {
|
||||||
// 针对来自服务端的菜单配置进行处理
|
// 针对来自服务端的菜单配置进行处理
|
||||||
// Handle routing configuration from the server
|
// Handle routing configuration from the server
|
||||||
|
|
||||||
// 根据需要自行完善来源于服务端的菜单配置的permission逻辑
|
// 检查是否在白名单中
|
||||||
// Refine the permission logic from the server's menu configuration as needed
|
if (WHITE_LIST.find((el) => el.name === to.name)) {
|
||||||
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) {
|
|
||||||
next()
|
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 {
|
} else {
|
||||||
// eslint-disable-next-line no-lonely-if
|
// eslint-disable-next-line no-lonely-if
|
||||||
if (permissionsAllow) next()
|
if (permissionsAllow) next()
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { createRouter, createWebHashHistory } from 'vue-router'
|
|||||||
|
|
||||||
import createRouteGuard from './guard'
|
import createRouteGuard from './guard'
|
||||||
import { appRoutes } from './routes'
|
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
|
NProgress.configure({ showSpinner: false }) // NProgress Configuration
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ const router = createRouter({
|
|||||||
},
|
},
|
||||||
...appRoutes,
|
...appRoutes,
|
||||||
REDIRECT_MAIN,
|
REDIRECT_MAIN,
|
||||||
NOT_FOUND_ROUTE,
|
// NOT_FOUND_ROUTE,
|
||||||
],
|
],
|
||||||
scrollBehavior() {
|
scrollBehavior() {
|
||||||
return { top: 0 }
|
return { top: 0 }
|
||||||
|
|||||||
@@ -1,5 +1,194 @@
|
|||||||
import { DEFAULT_LAYOUT } from './routes/base'
|
import { DEFAULT_LAYOUT } from './routes/base'
|
||||||
import type { AppRouteRecordRaw } from './routes/types'
|
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[] = [{
|
export const localMenuData: AppRouteRecordRaw[] = [{
|
||||||
@@ -20,10 +209,8 @@ export const localMenuData: AppRouteRecordRaw[] = [{
|
|||||||
meta: {
|
meta: {
|
||||||
locale: 'menu.dashboard.workplace',
|
locale: 'menu.dashboard.workplace',
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
roles: ['*'],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
/** simple */
|
|
||||||
{
|
{
|
||||||
path: 'monitor',
|
path: 'monitor',
|
||||||
name: 'Monitor',
|
name: 'Monitor',
|
||||||
@@ -31,10 +218,8 @@ export const localMenuData: AppRouteRecordRaw[] = [{
|
|||||||
meta: {
|
meta: {
|
||||||
locale: 'menu.dashboard.monitor',
|
locale: 'menu.dashboard.monitor',
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
roles: ['admin'],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
/** simple end */
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -46,687 +231,15 @@ export const localMenuData: AppRouteRecordRaw[] = [{
|
|||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
icon: 'icon-home',
|
icon: 'icon-home',
|
||||||
order: 1,
|
order: 1,
|
||||||
hideChildrenInMenu: true,
|
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
name: 'OverviewIndex',
|
name: 'OverviewIndex',
|
||||||
component: () => import('@/views/redirect/index.vue'),
|
component: () => import('@/views/ops/pages/overview/index.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
locale: '系统概况',
|
locale: '系统概况',
|
||||||
requiresAuth: true,
|
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: ['*'],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ import type { NotificationReturn } from '@arco-design/web-vue/es/notification/in
|
|||||||
import type { RouteRecordNormalized } from 'vue-router'
|
import type { RouteRecordNormalized } from 'vue-router'
|
||||||
import defaultSettings from '@/config/settings.json'
|
import defaultSettings from '@/config/settings.json'
|
||||||
import { userPmn } from '@/api/module/user'
|
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 { buildTree } from '@/utils/tree'
|
||||||
import SafeStorage, { AppStorageKey } from "@/utils/safeStorage";
|
import SafeStorage, { AppStorageKey } from "@/utils/safeStorage";
|
||||||
|
import router from '@/router'
|
||||||
import { AppState } from './types'
|
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 })
|
const res = await userPmn({ id: userInfo.user_id, workspace: import.meta.env.VITE_APP_WORKSPACE })
|
||||||
console.log('res', res)
|
console.log('res', res)
|
||||||
if (res.code === 0 && res?.details?.length) {
|
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[], {
|
||||||
// this.serverMenu = data
|
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 = localMenuData as unknown as RouteRecordNormalized[]
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} 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() {
|
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 = []
|
this.serverMenu = []
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,5 +16,6 @@ export interface AppState {
|
|||||||
tabBar: boolean
|
tabBar: boolean
|
||||||
menuFromServer: boolean
|
menuFromServer: boolean
|
||||||
serverMenu: RouteRecordNormalized[]
|
serverMenu: RouteRecordNormalized[]
|
||||||
|
workspace?: string
|
||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { LoginData } from '@/api/types'
|
import type { LoginData } from '@/api/types'
|
||||||
import useLoading from '@/hooks/loading'
|
import useLoading from '@/hooks/loading'
|
||||||
import { useUserStore } from '@/store'
|
import { useUserStore, useAppStore } from '@/store'
|
||||||
import { Message } from '@arco-design/web-vue'
|
import { Message } from '@arco-design/web-vue'
|
||||||
import { ValidatedError } from '@arco-design/web-vue/es/form/interface'
|
import { ValidatedError } from '@arco-design/web-vue/es/form/interface'
|
||||||
import { useStorage } from '@vueuse/core'
|
import { useStorage } from '@vueuse/core'
|
||||||
@@ -59,6 +59,7 @@ const { t } = useI18n()
|
|||||||
const errorMessage = ref('')
|
const errorMessage = ref('')
|
||||||
const { loading, setLoading } = useLoading()
|
const { loading, setLoading } = useLoading()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
const appStore = useAppStore()
|
||||||
|
|
||||||
const loginConfig = useStorage('login-config', {
|
const loginConfig = useStorage('login-config', {
|
||||||
rememberPassword: true,
|
rememberPassword: true,
|
||||||
@@ -76,12 +77,10 @@ const handleSubmit = async ({ errors, values }: { errors: Record<string, Validat
|
|||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
await userStore.login(values as LoginData)
|
await userStore.login(values as LoginData)
|
||||||
|
await appStore.fetchServerMenuConfig()
|
||||||
const { redirect, ...othersQuery } = router.currentRoute.value.query
|
const { redirect, ...othersQuery } = router.currentRoute.value.query
|
||||||
router.push({
|
router.push({
|
||||||
name: (redirect as string) || 'Workplace',
|
path: '/overview'
|
||||||
query: {
|
|
||||||
...othersQuery,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
Message.success(t('login.form.login.success'))
|
Message.success(t('login.form.login.success'))
|
||||||
const { rememberPassword } = loginConfig.value
|
const { rememberPassword } = loginConfig.value
|
||||||
|
|||||||
5
src/views/ops/pages/help/index.vue
Normal file
5
src/views/ops/pages/help/index.vue
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<div class="menu-container">
|
||||||
|
帮助中心
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
5
src/views/ops/pages/overview/index.vue
Normal file
5
src/views/ops/pages/overview/index.vue
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<div class="pmn-container">
|
||||||
|
overview
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
<template>
|
||||||
|
<a-modal
|
||||||
|
v-model:visible="visible"
|
||||||
|
title="修改用户密码"
|
||||||
|
:mask-closable="false"
|
||||||
|
:width="500"
|
||||||
|
@cancel="handleClose"
|
||||||
|
>
|
||||||
|
<a-form
|
||||||
|
ref="formRef"
|
||||||
|
:model="formData"
|
||||||
|
:rules="rules"
|
||||||
|
layout="vertical"
|
||||||
|
>
|
||||||
|
<a-form-item label="用户账号">
|
||||||
|
<a-input :model-value="user?.account" disabled />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item field="password" label="新密码">
|
||||||
|
<a-input-password
|
||||||
|
v-model="formData.password"
|
||||||
|
placeholder="请输入密码"
|
||||||
|
allow-clear
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item field="confirmPassword" label="确认新密码">
|
||||||
|
<a-input-password
|
||||||
|
v-model="formData.confirmPassword"
|
||||||
|
placeholder="请确认密码"
|
||||||
|
allow-clear
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<a-space>
|
||||||
|
<a-button @click="handleClose">取消</a-button>
|
||||||
|
<a-button type="primary" :loading="loading" @click="handleSubmit">
|
||||||
|
确定
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
</a-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, reactive, computed, watch } from 'vue'
|
||||||
|
import { Message } from '@arco-design/web-vue'
|
||||||
|
import { adminResetUserPassword } from '@/api/module/user'
|
||||||
|
import type { UserItem } from '@/api/types'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
user: UserItem | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:visible', visible: boolean): void
|
||||||
|
(e: 'success'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const visible = computed({
|
||||||
|
get: () => !!props.user,
|
||||||
|
set: (val) => emit('update:visible', val),
|
||||||
|
})
|
||||||
|
|
||||||
|
const formRef = ref()
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const formData = reactive({
|
||||||
|
password: '',
|
||||||
|
confirmPassword: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
password: [
|
||||||
|
{ required: true, message: '请输入密码' },
|
||||||
|
{ minLength: 6, message: '密码至少6个字符' },
|
||||||
|
],
|
||||||
|
confirmPassword: [
|
||||||
|
{ required: true, message: '请确认密码' },
|
||||||
|
{
|
||||||
|
validator: (value: string, callback: (error?: string) => void) => {
|
||||||
|
if (value !== formData.password) {
|
||||||
|
callback('两次密码输入不一致')
|
||||||
|
} else {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置表单
|
||||||
|
watch(
|
||||||
|
() => props.user,
|
||||||
|
(val) => {
|
||||||
|
if (val) {
|
||||||
|
formData.password = ''
|
||||||
|
formData.confirmPassword = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
formRef.value?.resetFields()
|
||||||
|
emit('update:visible', false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
const valid = await formRef.value?.validate()
|
||||||
|
if (valid) return
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
const res = await adminResetUserPassword({
|
||||||
|
id: props.user!.id!,
|
||||||
|
password: formData.password,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.code === 0) {
|
||||||
|
Message.success(res.message || '密码修改成功')
|
||||||
|
handleClose()
|
||||||
|
emit('success')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('修改密码失败', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default {
|
||||||
|
name: 'PasswordChangeDialog',
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
<template>
|
||||||
|
<a-modal
|
||||||
|
v-model:visible="visible"
|
||||||
|
title="设置权限"
|
||||||
|
:mask-closable="false"
|
||||||
|
:width="600"
|
||||||
|
@cancel="handleClose"
|
||||||
|
>
|
||||||
|
<a-spin :loading="loading" style="width: 100%">
|
||||||
|
<div class="permission-header">
|
||||||
|
<span>用户账号: {{ user?.account }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="permission-content">
|
||||||
|
<PermissionTreeItem
|
||||||
|
v-if="permissionTree.length > 0"
|
||||||
|
:items="permissionTree"
|
||||||
|
:selected-permissions="selectedPermissions"
|
||||||
|
:expanded-keys="expandedKeys"
|
||||||
|
@change="handlePermissionChange"
|
||||||
|
/>
|
||||||
|
<a-empty v-else description="暂无权限数据" />
|
||||||
|
</div>
|
||||||
|
</a-spin>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<a-space>
|
||||||
|
<a-button @click="handleClose">取消</a-button>
|
||||||
|
<a-button type="primary" :loading="submitting" @click="handleSubmit">
|
||||||
|
确定
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
</a-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import { Message } from '@arco-design/web-vue'
|
||||||
|
import PermissionTreeItem from './PermissionTree.vue'
|
||||||
|
import {
|
||||||
|
getUserPmnTree,
|
||||||
|
userPmn,
|
||||||
|
userModifyPmn,
|
||||||
|
} from '@/api/module/user'
|
||||||
|
import type { UserItem } from '@/api/types'
|
||||||
|
import { useAppStore } from '@/store'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
user: UserItem | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:visible', visible: boolean): void
|
||||||
|
(e: 'success'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const appStore = useAppStore()
|
||||||
|
|
||||||
|
const visible = computed({
|
||||||
|
get: () => !!props.user,
|
||||||
|
set: (val) => emit('update:visible', val),
|
||||||
|
})
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const submitting = ref(false)
|
||||||
|
const permissionTree = ref<any[]>([])
|
||||||
|
const selectedPermissions = ref<number[]>([])
|
||||||
|
const originalPermissions = ref<number[]>([])
|
||||||
|
const expandedKeys = ref<Set<number>>(new Set())
|
||||||
|
|
||||||
|
// 获取权限数据
|
||||||
|
const fetchPermissions = async () => {
|
||||||
|
if (!props.user?.id) return
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
// 并行获取权限树和用户当前权限
|
||||||
|
const [treeRes, pmnRes] = await Promise.all([
|
||||||
|
getUserPmnTree({ id: props.user.id, workspace: import.meta.env.VITE_APP_WORKSPACE || '' }),
|
||||||
|
userPmn({
|
||||||
|
id: props.user.id,
|
||||||
|
workspace: import.meta.env.VITE_APP_WORKSPACE || '',
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
|
||||||
|
console.log('treeRes', treeRes)
|
||||||
|
if (treeRes.details) {
|
||||||
|
permissionTree.value = treeRes.data
|
||||||
|
// 默认展开第一层
|
||||||
|
treeRes.data.forEach((item: any) => {
|
||||||
|
expandedKeys.value.add(item.id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pmnRes.data) {
|
||||||
|
selectedPermissions.value = pmnRes.data.map((item: any) => item.pmn_id || item)
|
||||||
|
originalPermissions.value = [...selectedPermissions.value]
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取权限数据失败', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听 user 变化
|
||||||
|
watch(
|
||||||
|
() => props.user,
|
||||||
|
(val) => {
|
||||||
|
if (val?.id) {
|
||||||
|
fetchPermissions()
|
||||||
|
} else {
|
||||||
|
permissionTree.value = []
|
||||||
|
selectedPermissions.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 处理权限变化
|
||||||
|
const handlePermissionChange = (permissions: number[]) => {
|
||||||
|
selectedPermissions.value = permissions
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
emit('update:visible', false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!props.user?.id) return
|
||||||
|
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
const res = await userModifyPmn({
|
||||||
|
id: props.user.id,
|
||||||
|
list: selectedPermissions.value,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.code === 0) {
|
||||||
|
Message.success('权限设置成功')
|
||||||
|
handleClose()
|
||||||
|
emit('success')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('设置权限失败', error)
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default {
|
||||||
|
name: 'PermissionSettingDialog',
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.permission-header {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 12px;
|
||||||
|
background-color: var(--color-fill-1);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.permission-content {
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
<template>
|
||||||
|
<div class="permission-tree">
|
||||||
|
<div v-for="item in items" :key="item.id" class="tree-node">
|
||||||
|
<div class="node-content" :style="{ paddingLeft: `${level * 24}px` }">
|
||||||
|
<!-- 展开/折叠按钮 -->
|
||||||
|
<span
|
||||||
|
v-if="hasChildren(item)"
|
||||||
|
class="expand-icon"
|
||||||
|
@click="toggleExpand(item.id)"
|
||||||
|
>
|
||||||
|
<icon-right v-if="!isExpanded(item.id)" />
|
||||||
|
<icon-down v-else />
|
||||||
|
</span>
|
||||||
|
<span v-else class="expand-icon-placeholder" />
|
||||||
|
|
||||||
|
<!-- 复选框 -->
|
||||||
|
<a-checkbox
|
||||||
|
:model-value="isChecked(item.id)"
|
||||||
|
:indeterminate="isIndeterminate(item)"
|
||||||
|
@change="handleCheck(item.id, $event)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 名称 -->
|
||||||
|
<span class="node-name">{{ item.name }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 子节点 -->
|
||||||
|
<div v-if="hasChildren(item) && isExpanded(item.id)" class="children">
|
||||||
|
<PermissionTree
|
||||||
|
:items="getChildren(item)"
|
||||||
|
:selected-permissions="selectedPermissions"
|
||||||
|
:level="level + 1"
|
||||||
|
:expanded-keys="expandedKeys"
|
||||||
|
@change="handleChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
export interface PermissionItem {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
children?: PermissionItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
items: PermissionItem[]
|
||||||
|
selectedPermissions: number[]
|
||||||
|
level?: number
|
||||||
|
expandedKeys?: Set<number>
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
level: 0,
|
||||||
|
expandedKeys: () => new Set(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'change', permissions: number[]): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// 本地展开状态
|
||||||
|
const localExpandedKeys = ref<Set<number>>(new Set())
|
||||||
|
const expandedKeys = computed(() => props.expandedKeys.size > 0 ? props.expandedKeys : localExpandedKeys.value)
|
||||||
|
|
||||||
|
// 检查是否有子节点
|
||||||
|
const hasChildren = (item: PermissionItem) => {
|
||||||
|
return item.children && item.children.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取子节点
|
||||||
|
const getChildren = (item: PermissionItem): PermissionItem[] => {
|
||||||
|
return item.children || []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否展开
|
||||||
|
const isExpanded = (id: number) => {
|
||||||
|
return expandedKeys.value.has(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换展开状态
|
||||||
|
const toggleExpand = (id: number) => {
|
||||||
|
if (expandedKeys.value.has(id)) {
|
||||||
|
expandedKeys.value.delete(id)
|
||||||
|
} else {
|
||||||
|
expandedKeys.value.add(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否选中
|
||||||
|
const isChecked = (id: number) => {
|
||||||
|
return props.selectedPermissions.includes(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否半选状态
|
||||||
|
const isIndeterminate = (item: PermissionItem) => {
|
||||||
|
if (!hasChildren(item)) return false
|
||||||
|
const childIds = getAllChildIds(item)
|
||||||
|
const selectedCount = childIds.filter((id) => props.selectedPermissions.includes(id)).length
|
||||||
|
return selectedCount > 0 && selectedCount < childIds.length
|
||||||
|
}
|
||||||
|
|
||||||
|
// 递归获取所有子节点ID
|
||||||
|
const getAllChildIds = (item: PermissionItem): number[] => {
|
||||||
|
const ids: number[] = []
|
||||||
|
if (item.children) {
|
||||||
|
item.children.forEach((child) => {
|
||||||
|
ids.push(child.id)
|
||||||
|
ids.push(...getAllChildIds(child))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return ids
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据ID查找权限项
|
||||||
|
const findItemById = (items: PermissionItem[], id: number): PermissionItem | null => {
|
||||||
|
for (const item of items) {
|
||||||
|
if (item.id === id) return item
|
||||||
|
if (item.children) {
|
||||||
|
const found = findItemById(item.children, id)
|
||||||
|
if (found) return found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取父节点ID
|
||||||
|
const getParentIds = (
|
||||||
|
items: PermissionItem[],
|
||||||
|
targetId: number,
|
||||||
|
parentIds: number[] = []
|
||||||
|
): number[] => {
|
||||||
|
for (const item of items) {
|
||||||
|
if (item.children) {
|
||||||
|
if (item.children.some((child) => child.id === targetId)) {
|
||||||
|
return [...parentIds, item.id]
|
||||||
|
}
|
||||||
|
const result = getParentIds(item.children, targetId, [...parentIds, item.id])
|
||||||
|
if (result.length > 0) return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理选中状态变化
|
||||||
|
const handleCheck = (id: number, checked: boolean) => {
|
||||||
|
let newPermissions = [...props.selectedPermissions]
|
||||||
|
|
||||||
|
if (checked) {
|
||||||
|
// 添加当前权限
|
||||||
|
if (!newPermissions.includes(id)) {
|
||||||
|
newPermissions.push(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加所有子权限
|
||||||
|
const item = findItemById(props.items, id)
|
||||||
|
if (item) {
|
||||||
|
const childIds = getAllChildIds(item)
|
||||||
|
childIds.forEach((childId) => {
|
||||||
|
if (!newPermissions.includes(childId)) {
|
||||||
|
newPermissions.push(childId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 移除当前权限
|
||||||
|
newPermissions = newPermissions.filter((pId) => pId !== id)
|
||||||
|
|
||||||
|
// 移除所有子权限
|
||||||
|
const item = findItemById(props.items, id)
|
||||||
|
if (item) {
|
||||||
|
const childIds = getAllChildIds(item)
|
||||||
|
newPermissions = newPermissions.filter((pId) => !childIds.includes(pId))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除父权限
|
||||||
|
const parentIds = getParentIds(props.items, id)
|
||||||
|
parentIds.forEach((parentId) => {
|
||||||
|
newPermissions = newPermissions.filter((pId) => pId !== parentId)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('change', newPermissions)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理子组件变化
|
||||||
|
const handleChange = (permissions: number[]) => {
|
||||||
|
emit('change', permissions)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default {
|
||||||
|
name: 'PermissionTree',
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.permission-tree {
|
||||||
|
.tree-node {
|
||||||
|
.node-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px 0;
|
||||||
|
|
||||||
|
.expand-icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--color-text-2);
|
||||||
|
transition: color 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: rgb(var(--primary-6));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-icon-placeholder {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-name {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.children {
|
||||||
|
// 子节点样式
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
409
src/views/ops/pages/system-settings/account-management/index.vue
Normal file
409
src/views/ops/pages/system-settings/account-management/index.vue
Normal file
@@ -0,0 +1,409 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container">
|
||||||
|
<Breadcrumb :items="['用户管理']" />
|
||||||
|
|
||||||
|
<SearchTable
|
||||||
|
title="用户管理"
|
||||||
|
:form-model="searchForm"
|
||||||
|
:form-items="formItems"
|
||||||
|
:data="tableData"
|
||||||
|
:columns="columns"
|
||||||
|
:loading="loading"
|
||||||
|
:pagination="pagination"
|
||||||
|
@search="handleSearch"
|
||||||
|
@reset="handleReset"
|
||||||
|
@page-change="handlePageChange"
|
||||||
|
@refresh="fetchData"
|
||||||
|
>
|
||||||
|
<!-- 工具栏左侧 - 新增按钮 -->
|
||||||
|
<template #toolbar-left>
|
||||||
|
<a-button type="primary" @click="handleAdd">
|
||||||
|
<template #icon><icon-plus /></template>
|
||||||
|
新增用户
|
||||||
|
</a-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 操作列 -->
|
||||||
|
<template #operation="{ record }">
|
||||||
|
<a-space>
|
||||||
|
<a-button type="text" size="small" @click="handleEdit(record)">
|
||||||
|
编辑
|
||||||
|
</a-button>
|
||||||
|
<a-button type="text" size="small" @click="handleSetPermission(record)">
|
||||||
|
权限
|
||||||
|
</a-button>
|
||||||
|
<a-button type="text" size="small" @click="handleChangePassword(record)">
|
||||||
|
改密
|
||||||
|
</a-button>
|
||||||
|
<a-button type="text" size="small" status="danger" @click="handleDelete(record)">
|
||||||
|
删除
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 状态列 -->
|
||||||
|
<template #status="{ record }">
|
||||||
|
<a-tag :color="record.status === 1 ? 'green' : 'red'">
|
||||||
|
{{ record.status === 1 ? '启用' : '禁用' }}
|
||||||
|
</a-tag>
|
||||||
|
</template>
|
||||||
|
</SearchTable>
|
||||||
|
|
||||||
|
<!-- 新增/编辑用户弹窗 -->
|
||||||
|
<a-modal
|
||||||
|
v-model:visible="modalVisible"
|
||||||
|
:title="modalTitle"
|
||||||
|
:mask-closable="false"
|
||||||
|
:width="520"
|
||||||
|
@ok="handleModalOk"
|
||||||
|
@cancel="handleModalCancel"
|
||||||
|
>
|
||||||
|
<a-form
|
||||||
|
ref="formRef"
|
||||||
|
:model="formData"
|
||||||
|
:rules="formRules"
|
||||||
|
:label-col-props="{ span: 6 }"
|
||||||
|
:wrapper-col-props="{ span: 16 }"
|
||||||
|
>
|
||||||
|
<a-form-item field="account" label="账号" validate-trigger="blur">
|
||||||
|
<a-input
|
||||||
|
v-model="formData.account"
|
||||||
|
placeholder="请输入账号"
|
||||||
|
:disabled="isEdit"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item field="name" label="姓名" validate-trigger="blur">
|
||||||
|
<a-input v-model="formData.name" placeholder="请输入姓名" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item field="email" label="邮箱" validate-trigger="blur">
|
||||||
|
<a-input v-model="formData.email" placeholder="请输入邮箱" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item field="phone" label="手机号" validate-trigger="blur">
|
||||||
|
<a-input v-model="formData.phone" placeholder="请输入手机号" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item field="status" label="状态">
|
||||||
|
<a-select v-model="formData.status" placeholder="请选择状态">
|
||||||
|
<a-option :value="1">正常</a-option>
|
||||||
|
<a-option :value="-1">禁用</a-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</a-modal>
|
||||||
|
|
||||||
|
<!-- 删除确认对话框 -->
|
||||||
|
<a-modal
|
||||||
|
v-model:visible="deleteConfirmVisible"
|
||||||
|
title="删除确认"
|
||||||
|
@ok="handleConfirmDelete"
|
||||||
|
@cancel="deleteConfirmVisible = false"
|
||||||
|
>
|
||||||
|
<p>确定要删除用户 "{{ userToDelete?.name || userToDelete?.account }}" 吗?</p>
|
||||||
|
</a-modal>
|
||||||
|
|
||||||
|
<!-- 修改密码弹窗 -->
|
||||||
|
<PasswordChangeDialog
|
||||||
|
:user="passwordChangeUser"
|
||||||
|
@update:visible="passwordChangeUser = null"
|
||||||
|
@success="fetchData"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 权限设置弹窗 -->
|
||||||
|
<PermissionSettingDialog
|
||||||
|
:user="permissionUser"
|
||||||
|
@update:visible="handlePermissionDialogClose"
|
||||||
|
@success="fetchData"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, reactive, computed, onMounted } from 'vue'
|
||||||
|
import { Message } from '@arco-design/web-vue'
|
||||||
|
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
||||||
|
import SearchTable from '@/components/search-table/index.vue'
|
||||||
|
import type { FormItem } from '@/components/search-form/types'
|
||||||
|
import type { UserItem } from '@/api/types'
|
||||||
|
import {
|
||||||
|
fetchUserList,
|
||||||
|
createUser,
|
||||||
|
modifyUser,
|
||||||
|
deleteUser,
|
||||||
|
} from '@/api/module/user'
|
||||||
|
import PasswordChangeDialog from './components/PasswordChangeDialog.vue'
|
||||||
|
import PermissionSettingDialog from './components/PermissionSettingDialog.vue'
|
||||||
|
|
||||||
|
// 搜索表单配置
|
||||||
|
const formItems: FormItem[] = [
|
||||||
|
{
|
||||||
|
field: 'keyword',
|
||||||
|
label: '关键词',
|
||||||
|
type: 'input',
|
||||||
|
placeholder: '请输入用户名/昵称/手机号',
|
||||||
|
span: 8,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// 表格列配置
|
||||||
|
const columns: TableColumnData[] = [
|
||||||
|
{
|
||||||
|
title: 'ID',
|
||||||
|
dataIndex: 'id',
|
||||||
|
width: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '账号',
|
||||||
|
dataIndex: 'account',
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '姓名',
|
||||||
|
dataIndex: 'name',
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '邮箱',
|
||||||
|
dataIndex: 'email',
|
||||||
|
width: 180,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '手机号',
|
||||||
|
dataIndex: 'phone',
|
||||||
|
width: 140,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
dataIndex: 'status',
|
||||||
|
slotName: 'status',
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '创建时间',
|
||||||
|
dataIndex: 'created_at',
|
||||||
|
width: 180,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
slotName: 'operation',
|
||||||
|
width: 150,
|
||||||
|
fixed: 'right',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// 搜索表单数据
|
||||||
|
const searchForm = reactive({
|
||||||
|
keyword: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
// 分页配置
|
||||||
|
const pagination = reactive({
|
||||||
|
current: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
total: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 表格数据
|
||||||
|
const tableData = ref<UserItem[]>([])
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
// 弹窗相关
|
||||||
|
const modalVisible = ref(false)
|
||||||
|
const isEdit = ref(false)
|
||||||
|
const formRef = ref()
|
||||||
|
const formData = reactive<UserItem>({
|
||||||
|
id: undefined,
|
||||||
|
account: '',
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
phone: '',
|
||||||
|
status: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 表单验证规则
|
||||||
|
const formRules = {
|
||||||
|
account: [
|
||||||
|
{ required: true, message: '请输入账号' },
|
||||||
|
{ minLength: 3, message: '账号至少3个字符' },
|
||||||
|
],
|
||||||
|
name: [
|
||||||
|
{ required: true, message: '请输入姓名' },
|
||||||
|
],
|
||||||
|
email: [
|
||||||
|
{ match: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/, message: '请输入正确的邮箱格式' },
|
||||||
|
],
|
||||||
|
phone: [
|
||||||
|
{ match: /^1[3-9]\d{9}$/, message: '请输入正确的手机号' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除确认
|
||||||
|
const deleteConfirmVisible = ref(false)
|
||||||
|
const userToDelete = ref<UserItem | null>(null)
|
||||||
|
|
||||||
|
// 修改密码
|
||||||
|
const passwordChangeUser = ref<UserItem | null>(null)
|
||||||
|
|
||||||
|
// 权限设置
|
||||||
|
const permissionUser = ref<UserItem | null>(null)
|
||||||
|
|
||||||
|
// 关闭权限设置弹窗
|
||||||
|
const handlePermissionDialogClose = (visible: boolean) => {
|
||||||
|
if (!visible) {
|
||||||
|
permissionUser.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 弹窗标题
|
||||||
|
const modalTitle = computed(() => (isEdit.value ? '编辑用户' : '新增用户'))
|
||||||
|
|
||||||
|
// 获取数据
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const res = await fetchUserList({
|
||||||
|
page: pagination.current,
|
||||||
|
size: pagination.pageSize,
|
||||||
|
keyword: searchForm.keyword || undefined,
|
||||||
|
})
|
||||||
|
if (res?.code === 0) {
|
||||||
|
tableData.value = res.details?.data || []
|
||||||
|
pagination.total = res.details?.total || 0
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch user list:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const handleSearch = () => {
|
||||||
|
pagination.current = 1
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置
|
||||||
|
const handleReset = () => {
|
||||||
|
searchForm.keyword = ''
|
||||||
|
pagination.current = 1
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页变化
|
||||||
|
const handlePageChange = (current: number) => {
|
||||||
|
pagination.current = current
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增用户
|
||||||
|
const handleAdd = () => {
|
||||||
|
isEdit.value = false
|
||||||
|
resetFormData()
|
||||||
|
modalVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑用户
|
||||||
|
const handleEdit = (record: UserItem) => {
|
||||||
|
isEdit.value = true
|
||||||
|
Object.assign(formData, {
|
||||||
|
id: record.id,
|
||||||
|
account: record.account,
|
||||||
|
name: record.name,
|
||||||
|
email: record.email,
|
||||||
|
phone: record.phone,
|
||||||
|
status: record.status ?? 1,
|
||||||
|
})
|
||||||
|
modalVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除用户
|
||||||
|
const handleDelete = (record: UserItem) => {
|
||||||
|
userToDelete.value = record
|
||||||
|
deleteConfirmVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改密码
|
||||||
|
const handleChangePassword = (record: UserItem) => {
|
||||||
|
passwordChangeUser.value = record
|
||||||
|
}
|
||||||
|
|
||||||
|
// 权限设置
|
||||||
|
const handleSetPermission = (record: UserItem) => {
|
||||||
|
permissionUser.value = record
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认删除
|
||||||
|
const handleConfirmDelete = async () => {
|
||||||
|
if (!userToDelete.value?.id) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
await deleteUser({ id: userToDelete.value.id })
|
||||||
|
Message.success('删除成功')
|
||||||
|
deleteConfirmVisible.value = false
|
||||||
|
userToDelete.value = null
|
||||||
|
await fetchData()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to delete user:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 弹窗确认
|
||||||
|
const handleModalOk = async () => {
|
||||||
|
try {
|
||||||
|
const valid = await formRef.value?.validate()
|
||||||
|
if (valid) return
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
if (isEdit.value && formData.id) {
|
||||||
|
await modifyUser(formData)
|
||||||
|
Message.success('修改成功')
|
||||||
|
} else {
|
||||||
|
await createUser(formData)
|
||||||
|
Message.success('创建成功')
|
||||||
|
}
|
||||||
|
modalVisible.value = false
|
||||||
|
await fetchData()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save user:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 弹窗取消
|
||||||
|
const handleModalCancel = () => {
|
||||||
|
modalVisible.value = false
|
||||||
|
formRef.value?.resetFields()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置表单数据
|
||||||
|
const resetFormData = () => {
|
||||||
|
Object.assign(formData, {
|
||||||
|
id: undefined,
|
||||||
|
account: '',
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
phone: '',
|
||||||
|
status: 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
onMounted(() => {
|
||||||
|
fetchData()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default {
|
||||||
|
name: 'AccountManagement',
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.container {
|
||||||
|
padding: 0 20px 20px 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -43,19 +43,34 @@
|
|||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
|
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="component" label="组件路径">
|
||||||
|
<a-input v-model="formData.component" placeholder="请输入组件路径" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
|
||||||
<!-- 根菜单才显示图标选择 -->
|
<!-- 根菜单才显示图标选择 -->
|
||||||
<a-col v-if="!parentId" :span="12">
|
<a-col v-if="!parentId" :span="12">
|
||||||
<a-form-item field="menu_icon" label="菜单图标" :rules="[{ required: true, message: '请选择菜单图标' }]">
|
<a-form-item field="menu_icon" label="菜单图标" style="width: 100%">
|
||||||
|
<a-input-group style="width: 100%;">
|
||||||
<a-input
|
<a-input
|
||||||
v-model="formData.menu_icon"
|
v-model="formData.menu_icon"
|
||||||
placeholder="点击选择图标"
|
placeholder="点击选择图标"
|
||||||
readonly
|
readonly
|
||||||
|
style="cursor: pointer"
|
||||||
@click="iconPickerVisible = true"
|
@click="iconPickerVisible = true"
|
||||||
>
|
>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<component v-if="formData.menu_icon" :is="getIconComponent(formData.menu_icon)" :size="18" />
|
<component v-if="formData.menu_icon" :is="getIconComponent(formData.menu_icon)" :size="18" />
|
||||||
</template>
|
</template>
|
||||||
</a-input>
|
</a-input>
|
||||||
|
<a-button
|
||||||
|
v-if="formData.menu_icon"
|
||||||
|
@click="clearIcon"
|
||||||
|
>
|
||||||
|
<template #icon><icon-close /></template>
|
||||||
|
</a-button>
|
||||||
|
</a-input-group>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
|
|
||||||
@@ -66,16 +81,29 @@
|
|||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
|
|
||||||
|
<!-- 路由配置 -->
|
||||||
|
<a-divider orientation="left">路由配置</a-divider>
|
||||||
|
|
||||||
|
<a-row :gutter="16">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="is_new_tab">
|
||||||
|
<a-switch v-model="formData.is_new_tab" />
|
||||||
|
<span class="switch-label">在新窗口打开</span>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="is_web_page">
|
||||||
|
<a-switch v-model="formData.is_web_page" />
|
||||||
|
<span class="switch-label">嵌入网页</span>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
<!-- 网页嵌入配置 -->
|
<!-- 网页嵌入配置 -->
|
||||||
<a-divider orientation="left">网页嵌入配置</a-divider>
|
<a-divider orientation="left">网页嵌入配置</a-divider>
|
||||||
|
|
||||||
<a-row :gutter="16">
|
<a-row :gutter="16">
|
||||||
<a-col :span="24">
|
|
||||||
<a-form-item field="is_web_page">
|
|
||||||
<a-switch v-model="formData.is_web_page" />
|
|
||||||
<span class="switch-label">是否为嵌入网页</span>
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
|
|
||||||
<a-col v-if="formData.is_web_page" :span="24">
|
<a-col v-if="formData.is_web_page" :span="24">
|
||||||
<a-form-item field="web_url" label="网页地址">
|
<a-form-item field="web_url" label="网页地址">
|
||||||
@@ -154,12 +182,14 @@ const formData = ref<MenuRouteRequest>({
|
|||||||
description: '',
|
description: '',
|
||||||
menu_icon: '',
|
menu_icon: '',
|
||||||
menu_path: '',
|
menu_path: '',
|
||||||
|
component: '',
|
||||||
parent_id: undefined,
|
parent_id: undefined,
|
||||||
title: '',
|
title: '',
|
||||||
title_en: '',
|
title_en: '',
|
||||||
type: 1,
|
type: 1,
|
||||||
is_web_page: false,
|
is_web_page: false,
|
||||||
web_url: '',
|
web_url: '',
|
||||||
|
is_new_tab: false,
|
||||||
// 编辑时需要的额外字段
|
// 编辑时需要的额外字段
|
||||||
id: undefined,
|
id: undefined,
|
||||||
identity: undefined,
|
identity: undefined,
|
||||||
@@ -194,12 +224,14 @@ const resetForm = () => {
|
|||||||
description: '',
|
description: '',
|
||||||
menu_icon: '',
|
menu_icon: '',
|
||||||
menu_path: '',
|
menu_path: '',
|
||||||
|
component: '',
|
||||||
parent_id: props.parentId || undefined,
|
parent_id: props.parentId || undefined,
|
||||||
title: '',
|
title: '',
|
||||||
title_en: '',
|
title_en: '',
|
||||||
type: 1,
|
type: 1,
|
||||||
is_web_page: false,
|
is_web_page: false,
|
||||||
web_url: '',
|
web_url: '',
|
||||||
|
is_new_tab: false,
|
||||||
id: undefined,
|
id: undefined,
|
||||||
identity: undefined,
|
identity: undefined,
|
||||||
app_id: undefined,
|
app_id: undefined,
|
||||||
@@ -227,12 +259,14 @@ watch(
|
|||||||
description: initialValues.description || '',
|
description: initialValues.description || '',
|
||||||
menu_icon: initialValues.menu_icon || '',
|
menu_icon: initialValues.menu_icon || '',
|
||||||
menu_path: initialValues.menu_path || '',
|
menu_path: initialValues.menu_path || '',
|
||||||
|
component: initialValues.component || '',
|
||||||
parent_id: initialValues.parent_id,
|
parent_id: initialValues.parent_id,
|
||||||
title: initialValues.title || '',
|
title: initialValues.title || '',
|
||||||
title_en: initialValues.title_en || '',
|
title_en: initialValues.title_en || '',
|
||||||
type: initialValues.type || 1,
|
type: initialValues.type || 1,
|
||||||
is_web_page: initialValues.is_web_page || false,
|
is_web_page: initialValues.is_web_page || false,
|
||||||
web_url: initialValues.web_url || '',
|
web_url: initialValues.web_url || '',
|
||||||
|
is_new_tab: initialValues.is_new_tab || false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -253,6 +287,11 @@ const selectIcon = (icon: string) => {
|
|||||||
iconSearch.value = ''
|
iconSearch.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 清空图标
|
||||||
|
const clearIcon = () => {
|
||||||
|
formData.value.menu_icon = ''
|
||||||
|
}
|
||||||
|
|
||||||
// 提交表单
|
// 提交表单
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -296,10 +335,11 @@ export default {
|
|||||||
|
|
||||||
.icon-grid {
|
.icon-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(6, 1fr);
|
grid-template-columns: repeat(5, 1fr);
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
max-height: 400px;
|
max-height: 400px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-item {
|
.icon-item {
|
||||||
|
|||||||
@@ -16,66 +16,29 @@
|
|||||||
@drag-end="$emit('drag-end')"
|
@drag-end="$emit('drag-end')"
|
||||||
@drop="$emit('drop', $event)"
|
@drop="$emit('drop', $event)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 根级别拖放区域 -->
|
|
||||||
<div
|
|
||||||
v-if="draggingNode"
|
|
||||||
class="root-drop-zone"
|
|
||||||
:class="{ 'drag-over': isDragOver }"
|
|
||||||
@dragover.prevent="handleDragOver"
|
|
||||||
@dragleave="handleDragLeave"
|
|
||||||
@drop.prevent="handleDropToRoot"
|
|
||||||
>
|
|
||||||
<icon-plus-circle /> 拖放到此处设为根级菜单
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue'
|
|
||||||
import MenuTreeItem from './MenuTreeItem.vue'
|
import MenuTreeItem from './MenuTreeItem.vue'
|
||||||
import type { MenuNode } from '../types'
|
import type { MenuNode } from '../types'
|
||||||
|
|
||||||
const props = defineProps<{
|
defineProps<{
|
||||||
nodes: MenuNode[]
|
nodes: MenuNode[]
|
||||||
expandedKeys: Set<number>
|
expandedKeys: Set<number>
|
||||||
selectedKey: number | null
|
selectedKey: number | null
|
||||||
draggingNode?: MenuNode | null
|
draggingNode?: MenuNode | null
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
defineEmits<{
|
||||||
(e: 'toggle', nodeId: number): void
|
(e: 'toggle', nodeId: number): void
|
||||||
(e: 'select', node: MenuNode): void
|
(e: 'select', node: MenuNode): void
|
||||||
(e: 'add-child', parentId: number): void
|
(e: 'add-child', parentId: number): void
|
||||||
(e: 'delete', node: MenuNode): void
|
(e: 'delete', node: MenuNode): void
|
||||||
(e: 'drag-start', node: MenuNode): void
|
(e: 'drag-start', node: MenuNode): void
|
||||||
(e: 'drag-end'): void
|
(e: 'drag-end'): void
|
||||||
(e: 'drop', data: { dragNode: MenuNode; targetNode: MenuNode | null; position: 'before' | 'after' | 'inside' | 'root' }): void
|
(e: 'drop', data: { dragNode: MenuNode; targetNode: MenuNode; position: 'before' | 'after' | 'inside' }): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const isDragOver = ref(false)
|
|
||||||
|
|
||||||
const handleDragOver = (e: DragEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
isDragOver.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDragLeave = () => {
|
|
||||||
isDragOver.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDropToRoot = () => {
|
|
||||||
isDragOver.value = false
|
|
||||||
|
|
||||||
if (props.draggingNode) {
|
|
||||||
// 拖放到根级别
|
|
||||||
emit('drop', {
|
|
||||||
dragNode: props.draggingNode,
|
|
||||||
targetNode: null,
|
|
||||||
position: 'root'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
@@ -88,26 +51,4 @@ export default {
|
|||||||
.menu-tree {
|
.menu-tree {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.root-drop-zone {
|
|
||||||
margin-top: 8px;
|
|
||||||
padding: 16px;
|
|
||||||
border: 2px dashed var(--color-border-2);
|
|
||||||
border-radius: 4px;
|
|
||||||
text-align: center;
|
|
||||||
color: var(--color-text-3);
|
|
||||||
font-size: 13px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 8px;
|
|
||||||
|
|
||||||
&:hover, &.drag-over {
|
|
||||||
border-color: rgb(var(--primary-6));
|
|
||||||
color: rgb(var(--primary-6));
|
|
||||||
background-color: rgb(var(--primary-1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<Breadcrumb :items="['menu.ops', 'menu.ops.systemSettings', 'menu.ops.systemSettings.menuManagement']" />
|
<Breadcrumb :items="['menu.ops.systemSettings', 'menu.ops.systemSettings.menuManagement']" />
|
||||||
<a-row :gutter="16" class="menu-container">
|
<a-row :gutter="16" class="menu-container">
|
||||||
<!-- 左侧: 菜单树 -->
|
<!-- 左侧: 菜单树 -->
|
||||||
<a-col :xs="24" :md="10" :lg="8">
|
<a-col :xs="24" :md="10" :lg="8">
|
||||||
@@ -286,7 +286,7 @@ const handleDragEnd = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 处理放置
|
// 处理放置
|
||||||
const handleDrop = async (data: { dragNode: MenuNode; targetNode: MenuNode | null; position: 'before' | 'after' | 'inside' | 'root' }) => {
|
const handleDrop = async (data: { dragNode: MenuNode; targetNode: MenuNode; position: 'before' | 'after' | 'inside' }) => {
|
||||||
const { dragNode, targetNode, position } = data
|
const { dragNode, targetNode, position } = data
|
||||||
|
|
||||||
if (!dragNode.id) return
|
if (!dragNode.id) return
|
||||||
@@ -294,14 +294,6 @@ const handleDrop = async (data: { dragNode: MenuNode; targetNode: MenuNode | nul
|
|||||||
try {
|
try {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
|
||||||
if (position === 'root' && !targetNode) {
|
|
||||||
// 拖放到根级别
|
|
||||||
const { children, ...dragData } = dragNode
|
|
||||||
await modifyMenu({
|
|
||||||
...dragData,
|
|
||||||
parent_id: null
|
|
||||||
} as MenuRouteRequest)
|
|
||||||
} else if (targetNode) {
|
|
||||||
if (position === 'inside') {
|
if (position === 'inside') {
|
||||||
// 拖放到某个节点内部,成为其子节点
|
// 拖放到某个节点内部,成为其子节点
|
||||||
const { children, ...dragData } = dragNode
|
const { children, ...dragData } = dragNode
|
||||||
@@ -319,7 +311,7 @@ const handleDrop = async (data: { dragNode: MenuNode; targetNode: MenuNode | nul
|
|||||||
.filter(item => item.parent_id === targetNode.parent_id)
|
.filter(item => item.parent_id === targetNode.parent_id)
|
||||||
.sort((a, b) => (a.order || 0) - (b.order || 0))
|
.sort((a, b) => (a.order || 0) - (b.order || 0))
|
||||||
|
|
||||||
siblings.forEach((item, index) => {
|
siblings.forEach((item) => {
|
||||||
if (item.id === targetNode.id) {
|
if (item.id === targetNode.id) {
|
||||||
if (position === 'before') {
|
if (position === 'before') {
|
||||||
sortList.push({ pmn_id: dragNode.id!, sort_key: sortKey++ })
|
sortList.push({ pmn_id: dragNode.id!, sort_key: sortKey++ })
|
||||||
@@ -345,7 +337,6 @@ const handleDrop = async (data: { dragNode: MenuNode; targetNode: MenuNode | nul
|
|||||||
await updateMenuOrder(sortList)
|
await updateMenuOrder(sortList)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Message.success('移动成功')
|
Message.success('移动成功')
|
||||||
await loadMenuItems()
|
await loadMenuItems()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<Breadcrumb :items="['menu.ops', 'menu.ops.systemSettings', 'menu.ops.systemSettings.systemLogs']" />
|
<Breadcrumb :items="['menu.ops.systemSettings', 'menu.ops.systemSettings.systemLogs']" />
|
||||||
|
|
||||||
<!-- 使用 SearchTable 公共组件 -->
|
<!-- 使用 SearchTable 公共组件 -->
|
||||||
<SearchTable
|
<SearchTable
|
||||||
|
|||||||
Reference in New Issue
Block a user