From 87a0efbdeaf315a199f519e7207648e77a3a2a82 Mon Sep 17 00:00:00 2001 From: msumshk Date: Fri, 30 Jan 2026 06:22:11 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=E8=8F=9C=E5=8D=95=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E6=8C=89swagger=E9=80=82=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/system-manage.ts | 12 ++- src/types/api/menu.d.ts | 70 ++++++++++++++++ src/views/system/menu/index.vue | 139 ++++++++++++++++++++++++++++++-- 3 files changed, 211 insertions(+), 10 deletions(-) create mode 100644 src/types/api/menu.d.ts diff --git a/src/api/system-manage.ts b/src/api/system-manage.ts index 3bd83c5..7cff929 100644 --- a/src/api/system-manage.ts +++ b/src/api/system-manage.ts @@ -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({ - url: '/api/v3/system/menus/simple' + return api.get({ + url: '/api/admin/v1/menus' + }) +} + +// 11. 获取菜单详情 +export function fetchGetMenuDetail(menuId: string) { + return api.get({ + url: `/api/admin/v1/menus/${menuId}` }) } diff --git a/src/types/api/menu.d.ts b/src/types/api/menu.d.ts new file mode 100644 index 0000000..bca2d91 --- /dev/null +++ b/src/types/api/menu.d.ts @@ -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 + } + } +} diff --git a/src/views/system/menu/index.vue b/src/views/system/menu/index.vue index 973b1e7..40536cf 100644 --- a/src/views/system/menu/index.vue +++ b/src/views/system/menu/index.vue @@ -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(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([]) + const tableData = ref([]) /** * 重置搜索条件 @@ -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 + }) } /**