diff --git a/WonderQ-Admin-UI-Vue/src/components/layout/LayoutIcon.vue b/WonderQ-Admin-UI-Vue/src/components/layout/LayoutIcon.vue
new file mode 100644
index 0000000..b322b8e
--- /dev/null
+++ b/WonderQ-Admin-UI-Vue/src/components/layout/LayoutIcon.vue
@@ -0,0 +1,46 @@
+
+
+
+
+
diff --git a/WonderQ-Admin-UI-Vue/src/lib/menu-display.test.ts b/WonderQ-Admin-UI-Vue/src/lib/menu-display.test.ts
new file mode 100644
index 0000000..1b55f44
--- /dev/null
+++ b/WonderQ-Admin-UI-Vue/src/lib/menu-display.test.ts
@@ -0,0 +1,87 @@
+import { describe, expect, it } from "vitest";
+import { readFileSync } from "node:fs";
+import type { AdminMenu, SystemMenuItem } from "@/types";
+import { findMenuTitle, getMenuIconName, visibleNavigableMenus } from "@/lib/menu-display";
+
+const menu = (value: Partial & Pick): AdminMenu => value;
+
+describe("menu display primitives", () => {
+ it("recursively removes hidden and button menus while preserving hierarchy and order", () => {
+ const menus: AdminMenu[] = [
+ menu({ id: "home", name: "首页", type: "page", path: "/home" }),
+ menu({
+ id: "system",
+ name: "系统管理",
+ type: "directory",
+ path: "/system",
+ children: [
+ menu({ id: "users", name: "用户", type: "page", path: "/system/users" }),
+ menu({ id: "hidden", name: "隐藏", type: "page", path: "/system/hidden", isVisible: false } as AdminMenu & { isVisible?: boolean }),
+ menu({ id: "button", name: "新增", type: "button", path: null }),
+ ],
+ }),
+ menu({
+ id: "empty",
+ name: "空目录",
+ type: "directory",
+ children: [menu({ id: "hidden-child", name: "隐藏子项", type: "page", path: "/hidden-child", isVisible: false } as AdminMenu & { isVisible?: boolean })],
+ }),
+ ];
+
+ expect(visibleNavigableMenus(menus)).toEqual([
+ {
+ ...menus[0],
+ },
+ {
+ ...menus[1],
+ children: [{ ...menus[1].children?.[0] }],
+ },
+ ]);
+ });
+
+ it("returns parent directory and page titles for normalized paths", () => {
+ const menus: SystemMenuItem[] = [
+ {
+ id: "system",
+ name: "系统管理",
+ type: "directory",
+ path: "/system/",
+ sortOrder: 1,
+ isVisible: true,
+ isActive: true,
+ },
+ {
+ id: "users",
+ parentId: "system",
+ name: "用户管理",
+ type: "page",
+ path: "system/users/",
+ sortOrder: 1,
+ isVisible: true,
+ isActive: true,
+ },
+ ];
+
+ expect(findMenuTitle(menus, "//system/users//")).toEqual(["系统管理", "用户管理"]);
+ expect(findMenuTitle(menus, "/missing")).toEqual([]);
+ });
+
+ it("maps WonderQ paths and components, accepts supported custom icons, and falls back safely", () => {
+ expect(getMenuIconName(menu({ id: "dashboard", name: "仪表盘", type: "page", path: "/dashboard", icon: "Search" }))).toBe("Search");
+ expect(getMenuIconName(menu({ id: "dashboard-default", name: "仪表盘", type: "page", path: "/dashboard" }))).toBe("Grid");
+ expect(getMenuIconName(menu({ id: "play", name: "玩法", type: "page", component: "WanfaPage" }))).toBe("Route");
+ expect(getMenuIconName(menu({ id: "custom", name: "自定义", type: "page", icon: "Collection" }))).toBe("Collection");
+ expect(getMenuIconName(menu({ id: "unknown", name: "未知", type: "page", icon: "NotAnIcon" }))).toBe("Menu");
+ });
+
+ it("keeps the LayoutIcon SFC contract static and dependency-free", () => {
+ const source = readFileSync(new URL("../components/layout/LayoutIcon.vue", import.meta.url), "utf8");
+ expect(source).toContain('viewBox="0 0 24 24"');
+ for (const name of ["Menu", "Grid", "Route", "User", "Collection", "Setting", "Search", "FullScreen", "Moon", "Sunny", "Refresh", "Close", "More", "Back", "Right", "Chevron"]) {
+ expect(source).toContain(`name === '${name}'`);
+ }
+ expect(source).toContain('');
+ expect(source).toContain(':aria-hidden="title ? undefined : true"');
+ expect(source).not.toMatch(/emoji|external|lucide|element-plus/i);
+ });
+});
diff --git a/WonderQ-Admin-UI-Vue/src/lib/menu-display.ts b/WonderQ-Admin-UI-Vue/src/lib/menu-display.ts
new file mode 100644
index 0000000..dc62e3f
--- /dev/null
+++ b/WonderQ-Admin-UI-Vue/src/lib/menu-display.ts
@@ -0,0 +1,110 @@
+import type { AdminMenu, SystemMenuItem } from "@/types";
+
+export type MenuDisplayItem = (AdminMenu | SystemMenuItem) & {
+ isVisible?: boolean;
+ children?: MenuDisplayItem[];
+};
+
+export type LayoutIconName =
+ | "Menu"
+ | "Grid"
+ | "Route"
+ | "User"
+ | "Collection"
+ | "Setting"
+ | "Search"
+ | "FullScreen"
+ | "Moon"
+ | "Sunny"
+ | "Refresh"
+ | "Close"
+ | "More"
+ | "Back"
+ | "Right"
+ | "Chevron";
+
+const supportedIconNames = new Set([
+ "Menu", "Grid", "Route", "User", "Collection", "Setting", "Search", "FullScreen",
+ "Moon", "Sunny", "Refresh", "Close", "More", "Back", "Right", "Chevron",
+]);
+
+const pathIconMap: Array<[string, LayoutIconName]> = [
+ ["/dashboard", "Grid"],
+ ["/home", "Collection"],
+ ["/play", "Route"],
+ ["/concierge", "User"],
+ ["/leads", "Collection"],
+ ["/media", "Collection"],
+ ["/tools", "Setting"],
+ ["/system", "Setting"],
+];
+
+const componentIconMap: Record = {
+ DashboardPage: "Grid",
+ HomePage: "Collection",
+ WanfaPage: "Route",
+ ConciergePage: "User",
+ LeadsPage: "Collection",
+ MediaPage: "Collection",
+ OperationsToolsPage: "Setting",
+ SystemResourcePage: "Setting",
+};
+
+function normalizePath(path: string | null | undefined) {
+ if (!path) return "/";
+ const normalized = `/${path}`.replace(/\/+/g, "/");
+ const withLeadingSlash = normalized.startsWith("/") ? normalized : `/${normalized}`;
+ const trimmed = withLeadingSlash.replace(/\/+$/, "");
+ return trimmed || "/";
+}
+
+export function visibleNavigableMenus(menus: readonly T[]): T[] {
+ return menus.reduce((visible, menu) => {
+ if (menu.type === "button" || menu.isVisible === false) return visible;
+
+ const children = menu.children ? visibleNavigableMenus(menu.children) : undefined;
+ if (menu.type === "directory" && !menu.path && !children?.length) return visible;
+ visible.push({ ...menu, ...(children ? { children } : {}) });
+ return visible;
+ }, []);
+}
+
+export function findMenuTitle(menus: readonly MenuDisplayItem[], path: string): string[] {
+ const targetPath = normalizePath(path);
+ const entries: Array<{ menu: MenuDisplayItem; parentId?: string | null }> = [];
+
+ function collect(items: readonly MenuDisplayItem[], parentId?: string | null) {
+ for (const menu of items) {
+ entries.push({ menu, parentId: menu.parentId ?? parentId });
+ if (menu.children) collect(menu.children, menu.id);
+ }
+ }
+
+ collect(menus);
+ const byId = new Map(entries.map((entry) => [entry.menu.id, entry]));
+ const match = entries.find(({ menu }) => normalizePath(menu.path) === targetPath);
+ if (!match) return [];
+
+ const titles: string[] = [];
+ let current: typeof match | undefined = match;
+ const seen = new Set();
+ while (current && !seen.has(current.menu.id)) {
+ seen.add(current.menu.id);
+ titles.unshift(current.menu.name);
+ current = current.parentId ? byId.get(current.parentId) : undefined;
+ }
+ return titles;
+}
+
+export function getMenuIconName(menu: Pick): LayoutIconName {
+ const customIcon = menu.icon as LayoutIconName | null | undefined;
+ if (customIcon && supportedIconNames.has(customIcon)) return customIcon;
+
+ const normalizedPath = normalizePath(menu.path);
+ const pathMatch = pathIconMap.find(([path]) => normalizedPath === path || normalizedPath.startsWith(`${path}/`));
+ if (pathMatch) return pathMatch[1];
+
+ const componentMatch = menu.component ? componentIconMap[menu.component] : undefined;
+ if (componentMatch) return componentMatch;
+ return "Menu";
+}