kra/web/src/utils/dynamicRouter.ts

253 lines
7.6 KiB
TypeScript

/**
* KRA - Dynamic Router Utility
* 动态路由处理工具,参考 GVA 的 asyncRouter.js 实现
*/
import React from 'react';
// 页面组件映射 - 对应 GVA 的 viewModules
// UMI 使用 React.lazy 动态导入
const pageModules: Record<string, () => Promise<{ default: React.ComponentType<any> }>> = {
// 仪表盘
'view/dashboard/index.vue': () => import('@/pages/dashboard'),
// 超级管理员
'view/superAdmin/authority/authority.vue': () => import('@/pages/system/authority'),
'view/superAdmin/menu/menu.vue': () => import('@/pages/system/menu'),
'view/superAdmin/api/api.vue': () => import('@/pages/system/api'),
'view/superAdmin/user/user.vue': () => import('@/pages/system/user'),
'view/superAdmin/dictionary/sysDictionary.vue': () => import('@/pages/system/dictionary'),
'view/superAdmin/operation/sysOperationRecord.vue': () => import('@/pages/system/operation'),
'view/superAdmin/params/sysParams.vue': () => import('@/pages/system/params'),
// 系统工具
'view/systemTools/autoCode/index.vue': () => import('@/pages/systemTools/autoCode'),
'view/systemTools/formCreate/index.vue': () => import('@/pages/systemTools/formCreate'),
'view/systemTools/system/system.vue': () => import('@/pages/systemTools/system'),
'view/systemTools/autoCodeAdmin/index.vue': () => import('@/pages/systemTools/autoCodeAdmin'),
'view/systemTools/autoPkg/autoPkg.vue': () => import('@/pages/systemTools/autoPkg'),
'view/systemTools/exportTemplate/exportTemplate.vue': () => import('@/pages/systemTools/exportTemplate'),
'view/systemTools/installPlugin/index.vue': () => import('@/pages/systemTools/installPlugin'),
'view/systemTools/pubPlug/pubPlug.vue': () => import('@/pages/systemTools/pubPlug'),
'view/systemTools/version/version.vue': () => import('@/pages/systemTools/version'),
'view/systemTools/sysError/sysError.vue': () => import('@/pages/systemTools/sysError'),
// 示例
'view/example/upload/upload.vue': () => import('@/pages/example/upload'),
'view/example/breakpoint/breakpoint.vue': () => import('@/pages/example/breakpoint'),
'view/example/customer/customer.vue': () => import('@/pages/example/customer'),
// 插件
'plugin/announcement/view/info.vue': () => import('@/pages/plugin/announcement'),
'plugin/email/view/index.vue': () => import('@/pages/plugin/email'),
// 个人中心
'view/person/person.vue': () => import('@/pages/person'),
// 关于
'view/about/index.vue': () => import('@/pages/about'),
// 服务器状态
'view/system/state.vue': () => import('@/pages/system/state'),
// 错误页面
'view/error/reload.vue': () => import('@/pages/error/reload'),
};
// 插件组件映射
const pluginModules: Record<string, () => Promise<{ default: React.ComponentType<any> }>> = {
'plugin/announcement/view/info.vue': () => import('@/pages/plugin/announcement'),
'plugin/email/view/index.vue': () => import('@/pages/plugin/email'),
};
export interface BackendMenuItem {
ID?: number;
parentId?: number;
path: string;
name: string;
hidden?: boolean;
component?: string;
sort?: number;
meta?: {
title?: string;
icon?: string;
keepAlive?: boolean;
defaultMenu?: boolean;
closeTab?: boolean;
};
children?: BackendMenuItem[];
btns?: Record<string, number>;
parameters?: Array<{ key: string; value: string }>;
}
export interface RouteItem {
path: string;
name?: string;
icon?: string;
component?: React.ComponentType<any> | React.LazyExoticComponent<any>;
routes?: RouteItem[];
hideInMenu?: boolean;
// UMI Pro Layout 需要的字段
access?: string;
// 自定义 meta 信息
meta?: {
title?: string;
icon?: string;
keepAlive?: boolean;
defaultMenu?: boolean;
closeTab?: boolean;
btns?: Record<string, number>;
};
}
/**
* 动态导入组件
* 对应 GVA 的 dynamicImport 函数
*/
function dynamicImport(component: string): React.LazyExoticComponent<any> | null {
// 先检查页面模块
if (pageModules[component]) {
return React.lazy(pageModules[component]);
}
// 再检查插件模块
if (component.startsWith('plugin/') && pluginModules[component]) {
return React.lazy(pluginModules[component]);
}
console.warn(`Component not found: ${component}`);
return null;
}
/**
* 检查是否为外部链接
*/
function isExternalUrl(val: string | undefined): boolean {
return typeof val === 'string' && /^(https?:)?\/\//.test(val);
}
/**
* 处理后端返回的菜单数据,转换为 UMI 路由格式
* 对应 GVA 的 asyncRouterHandle 函数
*/
export function asyncRouterHandle(asyncRouter: BackendMenuItem[]): RouteItem[] {
const routes: RouteItem[] = [];
asyncRouter.forEach((item) => {
// 跳过外部链接
if (isExternalUrl(item.path) || isExternalUrl(item.name) || isExternalUrl(item.component)) {
// 外部链接作为菜单项但不作为路由
routes.push({
path: item.path,
name: item.meta?.title || item.name,
icon: item.meta?.icon,
hideInMenu: item.hidden,
meta: {
title: item.meta?.title,
icon: item.meta?.icon,
},
});
return;
}
const route: RouteItem = {
path: item.path.startsWith('/') ? item.path : `/${item.path}`,
name: item.meta?.title || item.name,
icon: item.meta?.icon,
hideInMenu: item.hidden,
meta: {
title: item.meta?.title,
icon: item.meta?.icon,
keepAlive: item.meta?.keepAlive,
defaultMenu: item.meta?.defaultMenu,
closeTab: item.meta?.closeTab,
btns: item.btns,
},
};
// 处理组件
if (item.component && typeof item.component === 'string') {
const component = dynamicImport(item.component);
if (component) {
route.component = component;
}
}
// 递归处理子路由
if (item.children && item.children.length > 0) {
route.routes = asyncRouterHandle(item.children);
}
routes.push(route);
});
return routes;
}
/**
* 扁平化路由(用于路由注册)
*/
export function flattenRoutes(routes: RouteItem[], parentPath: string = ''): RouteItem[] {
const result: RouteItem[] = [];
routes.forEach((route) => {
const fullPath = route.path.startsWith('/')
? route.path
: `${parentPath}/${route.path}`.replace(/\/+/g, '/');
result.push({
...route,
path: fullPath,
routes: undefined,
});
if (route.routes && route.routes.length > 0) {
result.push(...flattenRoutes(route.routes, fullPath));
}
});
return result;
}
/**
* 构建菜单数据(用于侧边栏显示)
*/
export function buildMenuData(routes: RouteItem[]): RouteItem[] {
return routes.filter((route) => !route.hideInMenu).map((route) => ({
...route,
routes: route.routes ? buildMenuData(route.routes) : undefined,
}));
}
/**
* 将后端菜单数据转换为 ProLayout 菜单格式
* 与 app.tsx 中的函数保持一致
*/
export function convertToMenuData(menus: BackendMenuItem[], parentPath: string = ''): any[] {
return menus.map((menu) => {
const path = menu.path.startsWith('/') || menu.path.startsWith('http')
? menu.path
: `${parentPath}/${menu.path}`.replace(/\/+/g, '/');
const menuItem: any = {
path,
name: menu.meta?.title || menu.name,
icon: menu.meta?.icon,
hideInMenu: menu.hidden,
};
if (menu.children && menu.children.length > 0) {
menuItem.children = convertToMenuData(menu.children, path);
}
return menuItem;
});
}
export default {
asyncRouterHandle,
flattenRoutes,
buildMenuData,
isExternalUrl,
convertToMenuData,
};