import type { MenuRecord } from '@/types/auth'; import type { MenuProps } from 'antd'; import type { RouteObject } from 'react-router'; import { lazy, Suspense } from 'react'; import { Spin } from 'antd'; import Icon from '@/components/Icon'; type AntMenuItem = NonNullable[number]; // ── Component loader using Vite glob import ───────────────────── const pages = import.meta.glob('@/pages/**/index.{tsx,jsx}'); function loadComponent(componentPath: string) { if (!componentPath) return null; const tsxKey = `/src/pages/${componentPath}.tsx`; const jsxKey = `/src/pages/${componentPath}.jsx`; const loader = (pages as Record)[tsxKey] ?? (pages as Record)[jsxKey]; if (!loader) return null; return lazy(loader as () => Promise<{ default: React.ComponentType }>); } // ── Icon helper ───────────────────────────────────────────────── function getIcon(iconName?: string): React.ReactNode { if (!iconName) return undefined; return ; } // ── Menu UI items (for sidebar) ───────────────────────────────── export function menusToMenuItems(menus: MenuRecord[]): AntMenuItem[] { return menus .filter((m) => m.type !== 3 && m.status === 0 && m.visible) .sort((a, b) => a.sort - b.sort) .map((menu): AntMenuItem => { const children = menu.children?.length ? menusToMenuItems(menu.children) : undefined; if (children?.length) { return { key: menu.path, icon: getIcon(menu.icon), label: menu.name, children, }; } return { key: menu.path, icon: getIcon(menu.icon), label: menu.name, }; }); } // ── Menu → Route conversion ───────────────────────────────────── /** * Flatten all type=2 menu items to a flat list. * This avoids the double-path nesting bug where directory routes * add their path prefix to children that already have full paths. * * Equivalent to Vue's flatMultiLevelRoutes(), but more aggressive: * we flatten ALL levels instead of just collapsing depth > 2. */ function flatMenus(menus: MenuRecord[]): MenuRecord[] { const result: MenuRecord[] = []; function walk(items: MenuRecord[]) { for (const m of items) { if (m.visible === false) { m.canTo = true; } if (m.type === 2 && m.component) { result.push(m); } if (m.children?.length) walk(m.children); } } walk(menus); return result; } function menuToRoute(menu: MenuRecord): RouteObject | null { // Skip external links if (menu.path.startsWith('http://') || menu.path.startsWith('https://')) { return null; } const routePath = menu.path.startsWith('/') ? menu.path.slice(1) : menu.path; const LazyComponent = loadComponent(menu.component); if (!LazyComponent) return null; return { id: `menu-${menu.id}`, path: routePath, element: ( } > ), }; } /** * 将菜单数据转换为路由配置 * 1. 仅转换 type=2 且 status=0 的菜单项 * 2. 路径前缀 '/' 会被移除(因为路由是相对于父路径的) * 3. 使用 Suspense 包裹组件,显示全屏加载状态 * 4. 外部链接(http:// 或 https://)会被忽略,不生成路由 * 5. 组件路径通过 Vite 的 glob 导入动态加载 */ export function menusToRoutes(menus: MenuRecord[]): RouteObject[] { return flatMenus(menus) .filter((m) => m.status === 0) .sort((a, b) => a.sort - b.sort) .map((menu) => menuToRoute(menu)) .filter((r): r is RouteObject => r !== null); } // ── Open keys & breadcrumbs ───────────────────────────────────── export function findOpenKeys( path: string, menus: MenuRecord[], parents: string[] = [], ): string[] { for (const menu of menus) { if (menu.path === path) return parents; if (menu.children?.length) { const found = findOpenKeys(path, menu.children, [ ...parents, menu.path, ]); if (found.length) return found; } } return []; } export function findBreadcrumb( path: string, menus: MenuRecord[], trail: { title: string }[] = [], ): { title: string }[] { for (const menu of menus) { if (menu.path === path) return [...trail, { title: menu.name }]; if (menu.children?.length) { const found = findBreadcrumb(path, menu.children, [ ...trail, { title: menu.name }, ]); if (found.length > trail.length + 1) return found; } } return trail; } export function findActiveMenuMeta( path: string, menus: MenuRecord[], ): string | null { for (const menu of menus) { if (menu.path === path) return menu.activeMenu ?? null; if (menu.children?.length) { const found = findActiveMenuMeta(path, menu.children); if (found) return found; } } return null; }