2026-07-17 08:51:48 +08:00
|
|
|
|
import { h, type Component, type VNode } from 'vue';
|
2026-07-14 10:31:17 +08:00
|
|
|
|
import type { RouteRecordRaw } from 'vue-router';
|
|
|
|
|
|
|
|
|
|
|
|
/** 路由 meta 类型 */
|
|
|
|
|
|
export type AppMeta = {
|
2026-07-17 08:51:48 +08:00
|
|
|
|
/** 页面 / 菜单标题,同时用于面包屑 */
|
2026-07-14 10:31:17 +08:00
|
|
|
|
title: string;
|
2026-07-17 08:51:48 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 菜单图标(仅一级菜单需要),支持两种写法:
|
|
|
|
|
|
* - iconfont 的 class 名,如 'icon-changguan1'
|
|
|
|
|
|
* - 组件,如 ant-design 图标 BarChartOutlined
|
|
|
|
|
|
*/
|
|
|
|
|
|
icon?: Component | string;
|
|
|
|
|
|
/** 是否在侧边栏菜单中隐藏(详情页等) */
|
2026-07-14 10:31:17 +08:00
|
|
|
|
hideInMenu?: boolean;
|
2026-07-17 08:51:48 +08:00
|
|
|
|
/** 高亮的菜单 key(路径不等于菜单 key 的场景,如详情页) */
|
2026-07-14 10:31:17 +08:00
|
|
|
|
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,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
2026-07-17 08:51:48 +08:00
|
|
|
|
|
|
|
|
|
|
/** 侧边栏菜单项(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 图标(<i class="iconfont icon-xxx" />);组件 -> 直接渲染
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|