feat(admin): 实现动态路由菜单并修复rbac菜单树构建问题
- 修复rbac.py中菜单树构建逻辑,将`elif not menu.parentId`改为`else`,避免父菜单不存在时子菜单丢失 - 新增后端`/api/admin/system/routers`接口,作为动态路由菜单的API - 前端新增`getRouters` API并整合到用户信息加载流程 - 添加菜单扁平化工具函数,更新路由注册逻辑以支持嵌套路由 - 新增相关测试用例并更新API和集成文档
This commit is contained in:
1 parent
f087663b5b
commit
2c8c327de7
12 files changed
+108
-5
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
createSystemDept,
|
||||
getRouters,
|
||||
getSystemDepts,
|
||||
getSystemMenus,
|
||||
getSystemRoles,
|
||||
@@ -49,6 +50,16 @@ describe("system RBAC client", () => {
|
||||
["/api/admin/system/depts", "POST"],
|
||||
]);
|
||||
});
|
||||
|
||||
it("loads the RuoYi-compatible dynamic router tree from the admin API", async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValueOnce(jsonResponse(200, [{ id: "dashboard", name: "仪表盘", type: "page" }]));
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const routers = await getRouters();
|
||||
|
||||
expect(routers).toEqual([{ id: "dashboard", name: "仪表盘", type: "page" }]);
|
||||
expect(fetchMock).toHaveBeenCalledWith("/api/admin/system/routers", expect.anything());
|
||||
});
|
||||
});
|
||||
|
||||
function jsonResponse(code: number, data: unknown) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import type {
|
||||
ApiErrorResponse,
|
||||
ApiResponse,
|
||||
AuthPayload,
|
||||
AdminMenu,
|
||||
AdminProfile,
|
||||
ConciergeAdvisorCreate,
|
||||
ConciergeAdvisorPatch,
|
||||
@@ -140,6 +141,8 @@ export async function request<T>(path: string, options: RequestInit = {}, retry
|
||||
|
||||
export const login = (email: string, password: string) => request<AuthPayload>("/api/admin/auth/login", { method: "POST", body: JSON.stringify({ email, password }) }, false);
|
||||
export const getProfile = () => request<AdminProfile>("/api/admin/system/profile");
|
||||
/** Load the authenticated user's visible menu tree for dynamic route registration. */
|
||||
export const getRouters = () => request<AdminMenu[]>("/api/admin/system/routers");
|
||||
export const logout = () => request<{ ok: boolean }>("/api/admin/auth/logout", { method: "POST" }, false);
|
||||
export const getDashboard = () => request<Dashboard>("/api/admin/dashboard");
|
||||
export const getSiteConfig = () => request<SiteConfig>("/api/admin/site-config");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { isStaticRouteComponent } from "./menu-routes";
|
||||
import { flattenMenuTree, isStaticRouteComponent } from "./menu-routes";
|
||||
|
||||
describe("admin menu route registration", () => {
|
||||
it("keeps RBAC resource pages on the static parameter route", () => {
|
||||
@@ -10,3 +10,18 @@ describe("admin menu route registration", () => {
|
||||
expect(isStaticRouteComponent("Dashboard")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("dynamic menu routes", () => {
|
||||
it("flattens nested router menus so child pages can be registered", () => {
|
||||
const menus = [
|
||||
{
|
||||
id: "system",
|
||||
name: "系统管理",
|
||||
type: "directory" as const,
|
||||
children: [{ id: "users", name: "用户管理", type: "page" as const, path: "/system/users" }],
|
||||
},
|
||||
];
|
||||
|
||||
expect(flattenMenuTree(menus).map((item) => item.id)).toEqual(["system", "users"]);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,11 @@
|
||||
import type { AdminMenu } from "@/types";
|
||||
|
||||
const staticRouteComponents = new Set(["SystemUsers", "SystemRoles", "SystemMenus", "SystemDepts"]);
|
||||
|
||||
export function isStaticRouteComponent(component: string | null | undefined) {
|
||||
return typeof component === "string" && staticRouteComponents.has(component);
|
||||
}
|
||||
|
||||
export function flattenMenuTree(menus: readonly AdminMenu[]): AdminMenu[] {
|
||||
return menus.flatMap((menu) => [menu, ...flattenMenuTree(menu.children ?? [])]);
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import LeadsPage from "@/pages/LeadsPage.vue";
|
||||
import MediaPage from "@/pages/MediaPage.vue";
|
||||
import SystemResourcePage from "@/pages/SystemResourcePage.vue";
|
||||
import NotFoundPage from "@/pages/NotFoundPage.vue";
|
||||
import { isStaticRouteComponent } from "@/lib/menu-routes";
|
||||
import { flattenMenuTree, isStaticRouteComponent } from "@/lib/menu-routes";
|
||||
import { adminIndexRoute } from "./route-config";
|
||||
|
||||
const componentRegistry = {
|
||||
@@ -54,7 +54,7 @@ let dynamicRoutesRegistered = false;
|
||||
function registerDynamicRoutes() {
|
||||
if (dynamicRoutesRegistered) return;
|
||||
const auth = useAuthStore();
|
||||
for (const menu of auth.menus) {
|
||||
for (const menu of flattenMenuTree(auth.menus)) {
|
||||
if (menu.type !== "page" || !menu.path || !menu.component || isStaticRouteComponent(menu.component) || !(menu.component in componentRegistry)) continue;
|
||||
const path = menu.path.replace(/^\/+/, "");
|
||||
if (router.getRoutes().some((route) => route.path === `/${path}`)) continue;
|
||||
|
||||
@@ -18,7 +18,9 @@ export const useAuthStore = defineStore("auth", () => {
|
||||
}
|
||||
|
||||
async function loadProfile() {
|
||||
applyProfile(await api.getProfile());
|
||||
const profile = await api.getProfile();
|
||||
applyProfile(profile);
|
||||
menus.value = await api.getRouters();
|
||||
}
|
||||
|
||||
async function bootstrap() {
|
||||
|
||||
Reference in new issue
Block a user