From 083d5a472017bf407d01e1d9be1dde029ea3297a Mon Sep 17 00:00:00 2001 From: sunlei Date: Mon, 29 Jun 2026 18:06:42 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=90=8E=E5=8F=B0?= =?UTF-8?q?=E8=8F=9C=E5=8D=95=E6=8E=92=E5=BA=8F=E7=94=9F=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- apps/web-antdv-next/src/api/core/menu.spec.ts | 48 +++++++++++++++++++ apps/web-antdv-next/src/api/core/menu.ts | 40 ++++++++++++++-- .../@core/base/typings/src/vue-router.d.ts | 1 + 4 files changed, 87 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d1bd5e4..c070d6a 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ pnpm run build:antdv-next ## 业务页面 -- 系统管理 / 菜单管理维护后端 `admin_menu.sort` 排序字段,菜单树展示仍以后端返回顺序为准;默认首页入口收敛到环境总览 `/analytics`,不再保留假工作台 `/workspace` 页面。 +- 系统管理 / 菜单管理维护后端 `admin_menu.sort` 排序字段;`/menu/all` caller 会把后端 `sort` 映射到 Vben 菜单生成器读取的 `meta.order`,保证侧边栏菜单展示以后端返回顺序为准。默认首页入口收敛到环境总览 `/analytics`,不再保留假工作台 `/workspace` 页面。 - 系统管理 / 站内信是日志级通知列表,只展示 API 错误、QQBot 下线、NapCat 离线等后端自动捕获事件;页面提供筛选、处理/重新打开、置顶和删除,不提供人工新增或编辑。 - QQBot / 账号连接页拆分 OneBot 连接、QQ 登录、NapCat 运行和运行说明列;更新登录通过 SSE 展示 quick / password / captcha / new-device / qrcode 每步中文进度,密码登录触发 QQ 安全验证时在弹窗内完成腾讯验证码并回交 API,新设备验证二维码和腾讯验证码分开展示;行操作“运行态”打开只读抽屉,展示 NapCat runtime/protocol/session behavior profile、风险模式和登录事件证据。 - QQBot / 插件平台页保留在线命令能力表,并提供 manifest 校验、本地插件安装、安装记录、运行事件和账号绑定抽屉,接口走 `/qqbot/plugin-platform/*`。 diff --git a/apps/web-antdv-next/src/api/core/menu.spec.ts b/apps/web-antdv-next/src/api/core/menu.spec.ts index 62f195f..d4170df 100644 --- a/apps/web-antdv-next/src/api/core/menu.spec.ts +++ b/apps/web-antdv-next/src/api/core/menu.spec.ts @@ -109,4 +109,52 @@ describe('core menu api', () => { }, ]); }); + + it('uses backend sort as the authoritative menu order before menu generation', async () => { + requestClientGet.mockResolvedValue([ + { + name: 'Dashboard', + path: '/dashboard', + sort: 200, + meta: { + order: -1, + title: 'Dashboard', + }, + }, + { + name: 'QqBot', + path: '/qqbot', + sort: 1, + meta: { + title: 'QQBot', + }, + children: [ + { + name: 'QqBotAccount', + path: '/qqbot/account', + component: '/qqbot/account/list', + sort: 1, + meta: { + title: '账号连接', + }, + }, + ], + }, + { + name: 'Blog', + path: '/blog', + sort: 0, + meta: { + order: 100, + title: '博客管理', + }, + }, + ]); + + const { getAllMenusApi } = await import('./menu'); + const menus = await getAllMenusApi(); + + expect(menus.map((menu) => menu.meta?.order)).toEqual([200, 1, 0]); + expect(menus[1]?.children?.[0]?.meta?.order).toBe(1); + }); }); diff --git a/apps/web-antdv-next/src/api/core/menu.ts b/apps/web-antdv-next/src/api/core/menu.ts index b3ecd07..ee00b13 100644 --- a/apps/web-antdv-next/src/api/core/menu.ts +++ b/apps/web-antdv-next/src/api/core/menu.ts @@ -97,15 +97,49 @@ export function isSupportedAdminMenuName(name?: null | string | symbol) { return typeof name === 'string' && SUPPORTED_ADMIN_MENU_NAMES.has(name); } +/** + * 将后端数据库排序字段对齐到 Vben 菜单生成器实际读取的 `meta.order`。 + * @param menu - `/menu/all` 返回的后端菜单节点;节点可能同时带有历史路由 `meta.order` 和 DB `sort`。 + * @returns 克隆后的菜单节点;当后端提供有限数字 `sort` 时以 DB 排序为准,否则保留原节点。 + */ +function normalizeBackendMenuOrder( + menu: RouteRecordStringComponent, +): RouteRecordStringComponent { + const sortOrder = + typeof menu.sort === 'number' && Number.isFinite(menu.sort) + ? menu.sort + : undefined; + + if (sortOrder === undefined) { + return menu; + } + + const meta = menu.meta ?? { title: String(menu.name ?? '') }; + + return { + ...menu, + meta: { + ...meta, + order: sortOrder, + }, + }; +} + +/** + * 过滤当前 Admin 已实现的后端菜单,并保留后端排序语义给路由菜单生成器使用。 + * @param menus - `/menu/all` 返回的后端菜单树;包含页面节点、隐藏路由节点和按钮权限节点。 + * @returns 仅含当前前端支持节点的菜单树,且每个节点的 DB `sort` 已映射为权威 `meta.order`。 + */ function filterSupportedAdminMenus( menus: RouteRecordStringComponent[], ): RouteRecordStringComponent[] { return menus .map((menu) => { - const children = menu.children - ? filterSupportedAdminMenus(menu.children) + const normalizedMenu = normalizeBackendMenuOrder(menu); + const children = normalizedMenu.children + ? filterSupportedAdminMenus(normalizedMenu.children) : undefined; - const menuWithoutChildren = { ...menu }; + const menuWithoutChildren = { ...normalizedMenu }; delete menuWithoutChildren.children; return { diff --git a/packages/@core/base/typings/src/vue-router.d.ts b/packages/@core/base/typings/src/vue-router.d.ts index 1577520..9f1a38a 100644 --- a/packages/@core/base/typings/src/vue-router.d.ts +++ b/packages/@core/base/typings/src/vue-router.d.ts @@ -130,6 +130,7 @@ type RouteRecordStringComponent = Omit< > & { children?: RouteRecordStringComponent[]; component: T; + sort?: number; }; type ComponentRecordType = Record Promise>;