feat
This commit is contained in:
@@ -18,6 +18,8 @@ export interface MenuItem {
|
||||
children?: MenuItem[];
|
||||
is_web_page?: boolean; // 是否为网页
|
||||
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 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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)}
|
||||
</a-sub-menu>
|
||||
) : (
|
||||
<a-menu-item key={element?.name} v-slots={{ icon }} onClick={() => goto(element)}>
|
||||
{t(element?.meta?.locale || '')}
|
||||
{element?.meta?.locale}
|
||||
</a-menu-item>
|
||||
)
|
||||
nodes.push(node as never)
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</li> -->
|
||||
<li>
|
||||
<!-- <li>
|
||||
<a-tooltip :content="$t('settings.language')">
|
||||
<a-button class="nav-btn" type="outline" :shape="'circle'" @click="setDropDownVisible">
|
||||
<template #icon>
|
||||
@@ -38,7 +38,7 @@
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</li>
|
||||
</li> -->
|
||||
<li>
|
||||
<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">
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<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 />
|
||||
</div>
|
||||
<a-layout>
|
||||
<a-layout>
|
||||
<a-layout-sider
|
||||
v-if="renderMenu"
|
||||
v-show="!hideMenu"
|
||||
v-show="!hideMenu && !route?.meta?.isNewTab"
|
||||
class="layout-sider"
|
||||
:breakpoint="'xl'"
|
||||
:collapsible="true"
|
||||
@@ -41,7 +41,7 @@
|
||||
<Menu />
|
||||
</a-drawer>
|
||||
<a-layout class="layout-content" :style="paddingStyle">
|
||||
<TabBar v-if="appStore.tabBar" />
|
||||
<!-- <TabBar v-if="appStore.tabBar" /> -->
|
||||
<a-layout-content>
|
||||
<PageLayout />
|
||||
</a-layout-content>
|
||||
@@ -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 } : {}
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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: ['*'],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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 = []
|
||||
},
|
||||
},
|
||||
|
||||
@@ -16,5 +16,6 @@ export interface AppState {
|
||||
tabBar: boolean
|
||||
menuFromServer: boolean
|
||||
serverMenu: RouteRecordNormalized[]
|
||||
workspace?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
<script lang="ts" setup>
|
||||
import type { LoginData } from '@/api/types'
|
||||
import useLoading from '@/hooks/loading'
|
||||
import { useUserStore } from '@/store'
|
||||
import { useUserStore, useAppStore } from '@/store'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { ValidatedError } from '@arco-design/web-vue/es/form/interface'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
@@ -59,6 +59,7 @@ const { t } = useI18n()
|
||||
const errorMessage = ref('')
|
||||
const { loading, setLoading } = useLoading()
|
||||
const userStore = useUserStore()
|
||||
const appStore = useAppStore()
|
||||
|
||||
const loginConfig = useStorage('login-config', {
|
||||
rememberPassword: true,
|
||||
@@ -76,12 +77,10 @@ const handleSubmit = async ({ errors, values }: { errors: Record<string, Validat
|
||||
setLoading(true)
|
||||
try {
|
||||
await userStore.login(values as LoginData)
|
||||
await appStore.fetchServerMenuConfig()
|
||||
const { redirect, ...othersQuery } = router.currentRoute.value.query
|
||||
router.push({
|
||||
name: (redirect as string) || 'Workplace',
|
||||
query: {
|
||||
...othersQuery,
|
||||
},
|
||||
path: '/overview'
|
||||
})
|
||||
Message.success(t('login.form.login.success'))
|
||||
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-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-form-item field="menu_icon" label="菜单图标" :rules="[{ required: true, message: '请选择菜单图标' }]">
|
||||
<a-input
|
||||
v-model="formData.menu_icon"
|
||||
placeholder="点击选择图标"
|
||||
readonly
|
||||
@click="iconPickerVisible = true"
|
||||
>
|
||||
<template #prefix>
|
||||
<component v-if="formData.menu_icon" :is="getIconComponent(formData.menu_icon)" :size="18" />
|
||||
</template>
|
||||
</a-input>
|
||||
<a-form-item field="menu_icon" label="菜单图标" style="width: 100%">
|
||||
<a-input-group style="width: 100%;">
|
||||
<a-input
|
||||
v-model="formData.menu_icon"
|
||||
placeholder="点击选择图标"
|
||||
readonly
|
||||
style="cursor: pointer"
|
||||
@click="iconPickerVisible = true"
|
||||
>
|
||||
<template #prefix>
|
||||
<component v-if="formData.menu_icon" :is="getIconComponent(formData.menu_icon)" :size="18" />
|
||||
</template>
|
||||
</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-col>
|
||||
|
||||
@@ -66,16 +81,29 @@
|
||||
</a-col>
|
||||
</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-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-form-item field="web_url" label="网页地址">
|
||||
@@ -154,12 +182,14 @@ const formData = ref<MenuRouteRequest>({
|
||||
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 {
|
||||
|
||||
@@ -16,66 +16,29 @@
|
||||
@drag-end="$emit('drag-end')"
|
||||
@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>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import MenuTreeItem from './MenuTreeItem.vue'
|
||||
import type { MenuNode } from '../types'
|
||||
|
||||
const props = defineProps<{
|
||||
defineProps<{
|
||||
nodes: MenuNode[]
|
||||
expandedKeys: Set<number>
|
||||
selectedKey: number | null
|
||||
draggingNode?: MenuNode | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
defineEmits<{
|
||||
(e: 'toggle', nodeId: number): void
|
||||
(e: 'select', node: MenuNode): void
|
||||
(e: 'add-child', parentId: number): void
|
||||
(e: 'delete', node: MenuNode): void
|
||||
(e: 'drag-start', node: MenuNode): 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 lang="ts">
|
||||
@@ -88,26 +51,4 @@ export default {
|
||||
.menu-tree {
|
||||
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>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<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-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
|
||||
|
||||
if (!dragNode.id) return
|
||||
@@ -294,56 +294,47 @@ const handleDrop = async (data: { dragNode: MenuNode; targetNode: MenuNode | nul
|
||||
try {
|
||||
loading.value = true
|
||||
|
||||
if (position === 'root' && !targetNode) {
|
||||
// 拖放到根级别
|
||||
if (position === 'inside') {
|
||||
// 拖放到某个节点内部,成为其子节点
|
||||
const { children, ...dragData } = dragNode
|
||||
await modifyMenu({
|
||||
...dragData,
|
||||
parent_id: null
|
||||
parent_id: targetNode.id
|
||||
} as MenuRouteRequest)
|
||||
} else if (targetNode) {
|
||||
if (position === 'inside') {
|
||||
// 拖放到某个节点内部,成为其子节点
|
||||
const { children, ...dragData } = dragNode
|
||||
await modifyMenu({
|
||||
...dragData,
|
||||
parent_id: targetNode.id
|
||||
} as MenuRouteRequest)
|
||||
} else {
|
||||
// 拖放到某个节点前后,需要更新排序
|
||||
const sortList: { pmn_id: number; sort_key: number }[] = []
|
||||
let sortKey = 1
|
||||
|
||||
// 获取同级节点
|
||||
const siblings = menuItems.value
|
||||
.filter(item => item.parent_id === targetNode.parent_id)
|
||||
.sort((a, b) => (a.order || 0) - (b.order || 0))
|
||||
|
||||
siblings.forEach((item, index) => {
|
||||
if (item.id === targetNode.id) {
|
||||
if (position === 'before') {
|
||||
sortList.push({ pmn_id: dragNode.id!, sort_key: sortKey++ })
|
||||
sortList.push({ pmn_id: item.id!, sort_key: sortKey++ })
|
||||
} else {
|
||||
sortList.push({ pmn_id: item.id!, sort_key: sortKey++ })
|
||||
sortList.push({ pmn_id: dragNode.id!, sort_key: sortKey++ })
|
||||
}
|
||||
} else if (item.id !== dragNode.id) {
|
||||
} else {
|
||||
// 拖放到某个节点前后,需要更新排序
|
||||
const sortList: { pmn_id: number; sort_key: number }[] = []
|
||||
let sortKey = 1
|
||||
|
||||
// 获取同级节点
|
||||
const siblings = menuItems.value
|
||||
.filter(item => item.parent_id === targetNode.parent_id)
|
||||
.sort((a, b) => (a.order || 0) - (b.order || 0))
|
||||
|
||||
siblings.forEach((item) => {
|
||||
if (item.id === targetNode.id) {
|
||||
if (position === 'before') {
|
||||
sortList.push({ pmn_id: dragNode.id!, sort_key: sortKey++ })
|
||||
sortList.push({ pmn_id: item.id!, sort_key: sortKey++ })
|
||||
} else {
|
||||
sortList.push({ pmn_id: item.id!, sort_key: sortKey++ })
|
||||
sortList.push({ pmn_id: dragNode.id!, sort_key: sortKey++ })
|
||||
}
|
||||
})
|
||||
|
||||
// 更新拖拽节点的 parent_id
|
||||
const { children, ...dragData } = dragNode
|
||||
await modifyMenu({
|
||||
...dragData,
|
||||
parent_id: targetNode.parent_id || null
|
||||
} as MenuRouteRequest)
|
||||
|
||||
// 更新排序
|
||||
if (sortList.length > 0) {
|
||||
await updateMenuOrder(sortList)
|
||||
} else if (item.id !== dragNode.id) {
|
||||
sortList.push({ pmn_id: item.id!, sort_key: sortKey++ })
|
||||
}
|
||||
})
|
||||
|
||||
// 更新拖拽节点的 parent_id
|
||||
const { children, ...dragData } = dragNode
|
||||
await modifyMenu({
|
||||
...dragData,
|
||||
parent_id: targetNode.parent_id || null
|
||||
} as MenuRouteRequest)
|
||||
|
||||
// 更新排序
|
||||
if (sortList.length > 0) {
|
||||
await updateMenuOrder(sortList)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<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
|
||||
|
||||
Reference in New Issue
Block a user