fix: 菜单管理接口按swagger适配

This commit is contained in:
msumshk
2026-01-30 06:22:11 +00:00
parent f5c8f67830
commit 87a0efbdea
3 changed files with 211 additions and 10 deletions

View File

@@ -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
View 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 {
/** 菜单 IDSnowflake前端以 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
}
}
}

View File

@@ -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
})
}
/**