import { h, type Component, type VNode } from 'vue'; import type { RouteRecordRaw } from 'vue-router'; /** 路由 meta 类型 */ export type AppMeta = { /** 页面 / 菜单标题,同时用于面包屑 */ title: string; /** * 菜单图标(仅一级菜单需要),支持两种写法: * - iconfont 的 class 名,如 'icon-changguan1' * - 组件,如 ant-design 图标 BarChartOutlined */ icon?: Component | string; /** 是否在侧边栏菜单中隐藏(详情页等) */ hideInMenu?: boolean; /** 高亮的菜单 key(路径不等于菜单 key 的场景,如详情页) */ activeMenu?: string; }; /** * 快速创建路由记录 * 根据路径自动生成 PascalCase 路由 name */ export function createRoute( path: string, component: RouteRecordRaw['component'], meta: AppMeta, children: RouteRecordRaw[] = [], ): RouteRecordRaw { const cleanPath = path.replace(/[:/]/g, '-'); const name = cleanPath .split('-') .filter(Boolean) .map((p) => p.charAt(0).toUpperCase() + p.slice(1)) .join(''); return { path, name: name || undefined, meta, component, children, }; } /** 侧边栏菜单项(ant-design-vue Menu 的 items 结构) */ export interface MenuItem { key: string; label: string; icon?: VNode; children?: MenuItem[]; } /** * 根据路由表生成侧边栏菜单 * * @description * 菜单完全由路由派生,路由是「菜单 / 面包屑」唯一的数据源: * - 仅收录带 meta.title 且未标记 hideInMenu 的路由; * - 有子路由的生成子菜单(父级本身不跳转,点击展开); * - 图标取自 meta.icon。 * * @param routes 需要遍历的路由数组(通常传入主布局的 children) */ export function generateMenu(routes: RouteRecordRaw[] = []): MenuItem[] { const items: MenuItem[] = []; for (const route of routes) { const meta = route.meta as AppMeta | undefined; if (!meta?.title || meta.hideInMenu) { continue; } const children = generateMenu(route.children); const item: MenuItem = { key: route.path, label: meta.title }; if (meta.icon) { // 字符串 -> iconfont 图标();组件 -> 直接渲染 item.icon = typeof meta.icon === 'string' ? h('i', { class: ['iconfont', meta.icon] }) : h(meta.icon); } if (children.length) { item.children = children; } items.push(item); } return items; }