fix: 菜单管理接口按swagger适配
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import api from '@/utils/http'
|
||||
import type { AppRouteRecord } from '@/types/router'
|
||||
|
||||
// 1. 获取用户列表
|
||||
export function fetchGetUserList(params: Api.SystemManage.UserSearchParams) {
|
||||
@@ -80,7 +79,14 @@ export function fetchBatchUserOperation(
|
||||
|
||||
// 10. 获取菜单列表
|
||||
export function fetchGetMenuList() {
|
||||
return api.get<AppRouteRecord[]>({
|
||||
url: '/api/v3/system/menus/simple'
|
||||
return api.get<Api.Menu.MenuDefinitionDto[]>({
|
||||
url: '/api/admin/v1/menus'
|
||||
})
|
||||
}
|
||||
|
||||
// 11. 获取菜单详情
|
||||
export function fetchGetMenuDetail(menuId: string) {
|
||||
return api.get<Api.Menu.MenuDefinitionDto>({
|
||||
url: `/api/admin/v1/menus/${menuId}`
|
||||
})
|
||||
}
|
||||
|
||||
70
src/types/api/menu.d.ts
vendored
Normal file
70
src/types/api/menu.d.ts
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
declare namespace Api {
|
||||
/**
|
||||
* 菜单管理
|
||||
* 对齐 Swagger: /api/admin/v1/menus
|
||||
*/
|
||||
namespace Menu {
|
||||
/** 菜单权限项 */
|
||||
interface MenuAuthItemDto {
|
||||
title: string | null
|
||||
authMark: string | null
|
||||
}
|
||||
|
||||
/** 菜单定义(平铺) */
|
||||
interface MenuDefinitionDto {
|
||||
/** 菜单 ID(Snowflake,前端以 string 接收) */
|
||||
id: string
|
||||
/** 父级菜单 ID(根节点通常为 0) */
|
||||
parentId: string
|
||||
/** 路由名称 */
|
||||
name: string | null
|
||||
/** 路由路径 */
|
||||
path: string | null
|
||||
/** 组件路径 */
|
||||
component: string | null
|
||||
/** 显示标题 */
|
||||
title: string | null
|
||||
/** 图标 */
|
||||
icon: string | null
|
||||
/** 是否为 iframe */
|
||||
isIframe: boolean
|
||||
/** 外部链接 */
|
||||
link: string | null
|
||||
/** 是否缓存 */
|
||||
keepAlive: boolean
|
||||
/** 排序 */
|
||||
sortOrder: number
|
||||
/** 访问所需权限(后端校验) */
|
||||
requiredPermissions: string[] | null
|
||||
/** 元数据权限(前端校验) */
|
||||
metaPermissions: string[] | null
|
||||
/** 元数据角色(前端校验) */
|
||||
metaRoles: string[] | null
|
||||
/** 权限按钮列表 */
|
||||
authList: MenuAuthItemDto[] | null
|
||||
}
|
||||
|
||||
/** 创建菜单命令 */
|
||||
interface CreateMenuCommand {
|
||||
parentId: string
|
||||
name: string | null
|
||||
path: string | null
|
||||
component: string | null
|
||||
title: string | null
|
||||
icon: string | null
|
||||
isIframe: boolean
|
||||
link: string | null
|
||||
keepAlive: boolean
|
||||
sortOrder: number
|
||||
requiredPermissions: string[] | null
|
||||
metaPermissions: string[] | null
|
||||
metaRoles: string[] | null
|
||||
authList: MenuAuthItemDto[] | null
|
||||
}
|
||||
|
||||
/** 更新菜单命令 */
|
||||
interface UpdateMenuCommand extends CreateMenuCommand {
|
||||
id: string
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,11 +55,17 @@
|
||||
import { useTableColumns } from '@/hooks/core/useTableColumns'
|
||||
import type { AppRouteRecord } from '@/types/router'
|
||||
import MenuDialog from './modules/menu-dialog.vue'
|
||||
import { fetchGetMenuList } from '@/api/system-manage'
|
||||
import { fetchGetMenuDetail, fetchGetMenuList } from '@/api/system-manage'
|
||||
import { ElTag, ElMessageBox } from 'element-plus'
|
||||
|
||||
defineOptions({ name: 'Menus' })
|
||||
|
||||
interface MenuRouteRecord extends AppRouteRecord {
|
||||
menuId: string
|
||||
parentId: string
|
||||
sortOrder: number
|
||||
}
|
||||
|
||||
// 状态管理
|
||||
const loading = ref(false)
|
||||
const isExpanded = ref(false)
|
||||
@@ -99,6 +105,112 @@
|
||||
getMenuList()
|
||||
})
|
||||
|
||||
/**
|
||||
* 将后端菜单定义转换为路由结构(用于表格展示)
|
||||
*/
|
||||
const transformMenuToRoute = (menu: Api.Menu.MenuDefinitionDto): MenuRouteRecord => {
|
||||
return {
|
||||
menuId: String(menu.id),
|
||||
parentId: String(menu.parentId),
|
||||
sortOrder: menu.sortOrder,
|
||||
path: menu.path ?? '',
|
||||
name: menu.name ?? undefined,
|
||||
component: menu.component ?? '',
|
||||
meta: {
|
||||
title: menu.title ?? '',
|
||||
icon: menu.icon ?? undefined,
|
||||
link: menu.link ?? undefined,
|
||||
isIframe: menu.isIframe,
|
||||
keepAlive: menu.keepAlive,
|
||||
roles: menu.metaRoles ?? undefined,
|
||||
authList: (menu.authList ?? []).map((auth) => ({
|
||||
title: auth.title ?? '',
|
||||
authMark: auth.authMark ?? ''
|
||||
})),
|
||||
sort: menu.sortOrder
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建完整路径(与菜单处理器逻辑保持一致)
|
||||
*/
|
||||
const buildFullPath = (path: string, parentPath: string): string => {
|
||||
if (!path) return ''
|
||||
|
||||
// 外部链接直接返回
|
||||
if (path.startsWith('http://') || path.startsWith('https://')) {
|
||||
return path
|
||||
}
|
||||
|
||||
// 已是绝对路径,直接返回
|
||||
if (path.startsWith('/')) {
|
||||
return path
|
||||
}
|
||||
|
||||
// 拼接父路径和当前路径
|
||||
if (parentPath) {
|
||||
const cleanParent = parentPath.replace(/\/$/, '')
|
||||
const cleanChild = path.replace(/^\//, '')
|
||||
return `${cleanParent}/${cleanChild}`
|
||||
}
|
||||
|
||||
// 没有父路径,添加前导斜杠
|
||||
return `/${path}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化菜单路径:将相对路径转换为完整路径
|
||||
*/
|
||||
const normalizeMenuPaths = (menuList: MenuRouteRecord[], parentPath = ''): MenuRouteRecord[] => {
|
||||
return menuList.map((item) => {
|
||||
const fullPath = buildFullPath(item.path || '', parentPath)
|
||||
const children = item.children?.length
|
||||
? normalizeMenuPaths(item.children as MenuRouteRecord[], fullPath)
|
||||
: item.children
|
||||
|
||||
return {
|
||||
...item,
|
||||
path: fullPath,
|
||||
children
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 将平铺菜单构建为树结构并按 sortOrder 排序
|
||||
*/
|
||||
const buildMenuTree = (menus: Api.Menu.MenuDefinitionDto[]): MenuRouteRecord[] => {
|
||||
// 1. 映射为路由结构(平铺)
|
||||
const nodes = menus.map(transformMenuToRoute)
|
||||
const nodeMap = new Map<string, MenuRouteRecord>(nodes.map((node) => [node.menuId, node]))
|
||||
const roots: MenuRouteRecord[] = []
|
||||
|
||||
// 2. 挂载父子关系(根节点通常 parentId 为 0)
|
||||
nodes.forEach((node) => {
|
||||
const parent = nodeMap.get(node.parentId)
|
||||
if (parent && node.parentId !== node.menuId) {
|
||||
parent.children = parent.children?.length ? [...parent.children, node] : [node]
|
||||
} else {
|
||||
roots.push(node)
|
||||
}
|
||||
})
|
||||
|
||||
// 3. 递归排序(按 sortOrder 升序)
|
||||
const sortTree = (list: MenuRouteRecord[]) => {
|
||||
list.sort((a, b) => a.sortOrder - b.sortOrder)
|
||||
list.forEach((item) => {
|
||||
if (item.children?.length) {
|
||||
sortTree(item.children as MenuRouteRecord[])
|
||||
}
|
||||
})
|
||||
}
|
||||
sortTree(roots)
|
||||
|
||||
// 4. 规范化路径(用于展示/搜索/展开)
|
||||
return normalizeMenuPaths(roots)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取菜单列表数据
|
||||
*/
|
||||
@@ -106,8 +218,11 @@
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
// 1. 拉取平铺菜单列表
|
||||
const list = await fetchGetMenuList()
|
||||
tableData.value = list
|
||||
|
||||
// 2. 转换为树结构供表格展示
|
||||
tableData.value = buildMenuTree(list)
|
||||
} catch (error) {
|
||||
throw error instanceof Error ? error : new Error('获取菜单失败')
|
||||
} finally {
|
||||
@@ -230,7 +345,7 @@
|
||||
])
|
||||
|
||||
// 数据相关
|
||||
const tableData = ref<AppRouteRecord[]>([])
|
||||
const tableData = ref<MenuRouteRecord[]>([])
|
||||
|
||||
/**
|
||||
* 重置搜索条件
|
||||
@@ -376,10 +491,20 @@
|
||||
* @param row 菜单行数据
|
||||
*/
|
||||
const handleEditMenu = (row: AppRouteRecord): void => {
|
||||
dialogType.value = 'menu'
|
||||
editData.value = row
|
||||
lockMenuType.value = true
|
||||
dialogVisible.value = true
|
||||
const currentRow = row as unknown as MenuRouteRecord
|
||||
|
||||
loading.value = true
|
||||
fetchGetMenuDetail(currentRow.menuId)
|
||||
.then((detail) => {
|
||||
// 1. 拉取详情后再打开弹窗,避免数据不完整
|
||||
dialogType.value = 'menu'
|
||||
editData.value = transformMenuToRoute(detail)
|
||||
lockMenuType.value = true
|
||||
dialogVisible.value = true
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user