283 lines
8.3 KiB
TypeScript
283 lines
8.3 KiB
TypeScript
/**
|
||
* API 菜单批量导入脚本
|
||
* 从路由配置中读取 API 相关的路由,并批量导入到后端
|
||
*
|
||
* 使用方法:
|
||
* 1. 在浏览器控制台中运行(需要先登录)
|
||
* 2. 或者通过 Node.js 运行(需要配置环境变量)
|
||
*/
|
||
|
||
import { MenuService } from '@/api/rbac';
|
||
import type { MenuCreateDto } from '@/api/rbac';
|
||
import SYSTEM_ROUTES from '@/router/routes/newModules/system';
|
||
|
||
// 菜单标题映射(从 locale key 到中文)
|
||
const menuTitleMap: Record<string, string> = {
|
||
'menu.api': 'API',
|
||
'menu.api.upstreamInterface': '上游接口',
|
||
'menu.api.downstreamInterface': '下游接口',
|
||
'menu.api.appList': 'App列表',
|
||
'menu.api.requestLog': '请求日志',
|
||
'menu.api.callStatistics': '调用统计',
|
||
'menu.api.apiDebugger': 'API调试',
|
||
'menu.api.createInterface': '创建接口',
|
||
'menu.api.editInterface': '修改接口',
|
||
'menu.api.createApplication': '创建应用',
|
||
};
|
||
|
||
/**
|
||
* 从动态导入函数中提取组件路径
|
||
*/
|
||
function extractComponentPath(component: any): string | undefined {
|
||
if (typeof component === 'string') {
|
||
return component;
|
||
}
|
||
|
||
if (typeof component === 'function') {
|
||
try {
|
||
const componentStr = component.toString();
|
||
// 匹配 import('@/views/...') 或 import("@/views/...")
|
||
const match = componentStr.match(/import\(['"]@\/views\/([^'"]+)\.vue['"]\)/);
|
||
if (match && match[1]) {
|
||
return `views/${match[1]}`;
|
||
}
|
||
} catch (error) {
|
||
console.error('提取组件路径失败:', error);
|
||
}
|
||
}
|
||
|
||
return undefined;
|
||
}
|
||
|
||
/**
|
||
* 获取菜单标题
|
||
*/
|
||
function getMenuTitle(route: any): string {
|
||
const localeKey = route.meta?.locale as string;
|
||
if (localeKey && menuTitleMap[localeKey]) {
|
||
return menuTitleMap[localeKey];
|
||
}
|
||
return route.name as string || route.path;
|
||
}
|
||
|
||
/**
|
||
* 构建路由路径
|
||
*/
|
||
function buildRoutePath(route: any, parentPath?: string): string {
|
||
let routePath = route.path;
|
||
if (!routePath.startsWith('/')) {
|
||
if (parentPath) {
|
||
routePath = `${parentPath}/${routePath}`;
|
||
} else {
|
||
routePath = `/${routePath}`;
|
||
}
|
||
}
|
||
return routePath;
|
||
}
|
||
|
||
/**
|
||
* 将路由配置转换为菜单数据
|
||
*/
|
||
function convertRouteToMenuData(
|
||
route: any,
|
||
parentId?: string | null,
|
||
parentPath?: string
|
||
): MenuCreateDto[] {
|
||
const menus: MenuCreateDto[] = [];
|
||
|
||
// 跳过没有 meta 的路由
|
||
if (!route.meta) {
|
||
return menus;
|
||
}
|
||
|
||
// 跳过隐藏的菜单(hideInMenu: true),这些不在导航中显示
|
||
if (route.meta.hideInMenu) {
|
||
return menus;
|
||
}
|
||
|
||
const routePath = buildRoutePath(route, parentPath);
|
||
const componentPath = extractComponentPath(route.component);
|
||
const menuTitle = getMenuTitle(route);
|
||
|
||
const menuData: MenuCreateDto = {
|
||
pId: parentId || null,
|
||
name: menuTitle,
|
||
type: 1, // 菜单类型:1=菜单
|
||
permissionCode: route.meta.permission as string || null,
|
||
icon: route.meta.icon as string || null,
|
||
route: routePath,
|
||
component: componentPath || null,
|
||
order: route.meta.order as number || 0,
|
||
isHidden: false,
|
||
isCached: false,
|
||
description: `从路由配置导入: ${routePath}`,
|
||
};
|
||
|
||
menus.push(menuData);
|
||
|
||
// 处理子路由(只处理一级子路由)
|
||
if (route.children && route.children.length > 0) {
|
||
route.children.forEach((child: any) => {
|
||
const childMenus = convertRouteToMenuData(child, null, routePath);
|
||
menus.push(...childMenus);
|
||
});
|
||
}
|
||
|
||
return menus;
|
||
}
|
||
|
||
/**
|
||
* 批量导入 API 菜单
|
||
*/
|
||
export async function importApiMenus(): Promise<void> {
|
||
try {
|
||
console.log('开始导入 API 菜单...');
|
||
|
||
// 1. 先获取已有菜单,用于去重
|
||
let existingMenus: any[] = [];
|
||
try {
|
||
existingMenus = await MenuService.getRbacMenuGetMenuTree();
|
||
console.log('获取到已有菜单数量:', existingMenus.length);
|
||
} catch (error) {
|
||
console.warn('获取已有菜单失败,将继续导入:', error);
|
||
}
|
||
|
||
// 收集已有菜单的 route
|
||
const existingRoutes = new Set<string>();
|
||
function collectRoutes(menus: any[]) {
|
||
menus.forEach(menu => {
|
||
if (menu.route) {
|
||
existingRoutes.add(menu.route);
|
||
}
|
||
if (menu.children && menu.children.length > 0) {
|
||
collectRoutes(menu.children);
|
||
}
|
||
});
|
||
}
|
||
collectRoutes(existingMenus);
|
||
|
||
// 2. 转换路由配置为菜单数据
|
||
const allMenus: MenuCreateDto[] = [];
|
||
|
||
// 处理 API 主菜单(目录)
|
||
const apiMainMenu: MenuCreateDto = {
|
||
pId: null,
|
||
name: 'API',
|
||
type: 1,
|
||
permissionCode: null,
|
||
icon: 'icon-computer',
|
||
route: '/api',
|
||
component: null,
|
||
order: 1,
|
||
isHidden: false,
|
||
isCached: false,
|
||
description: 'API 管理模块',
|
||
};
|
||
allMenus.push(apiMainMenu);
|
||
|
||
// 处理 API 子菜单
|
||
if (SYSTEM_ROUTES.children && SYSTEM_ROUTES.children.length > 0) {
|
||
SYSTEM_ROUTES.children.forEach((child: any) => {
|
||
const childMenus = convertRouteToMenuData(child, null, '/api');
|
||
allMenus.push(...childMenus);
|
||
});
|
||
}
|
||
|
||
// 3. 去重:过滤掉已存在的菜单
|
||
const menusToImport = allMenus.filter(menu => {
|
||
if (menu.route && existingRoutes.has(menu.route)) {
|
||
console.log(`菜单已存在,跳过: ${menu.name} (${menu.route})`);
|
||
return false;
|
||
}
|
||
return true;
|
||
});
|
||
|
||
if (menusToImport.length === 0) {
|
||
console.log('所有菜单已存在,无需导入');
|
||
return;
|
||
}
|
||
|
||
console.log(`准备导入 ${menusToImport.length} 个菜单`);
|
||
|
||
// 4. 先创建父菜单(API 主菜单)
|
||
let apiParentId: string | null = null;
|
||
const parentMenu = menusToImport.find(m => m.route === '/api' && !m.pId);
|
||
|
||
if (parentMenu) {
|
||
try {
|
||
// 检查父菜单是否已存在
|
||
const existingParent = existingMenus.find((m: any) => m.route === '/api');
|
||
if (existingParent) {
|
||
apiParentId = existingParent.id;
|
||
console.log(`API 主菜单已存在,ID: ${apiParentId}`);
|
||
} else {
|
||
const result = await MenuService.postRbacMenuCreate(parentMenu);
|
||
if (result.id) {
|
||
apiParentId = result.id;
|
||
console.log(`✅ 成功创建 API 主菜单,ID: ${apiParentId}`);
|
||
}
|
||
}
|
||
} catch (error: any) {
|
||
if (error.status === 400 || error.status === 409) {
|
||
// 菜单已存在,尝试查找
|
||
const existingParent = existingMenus.find((m: any) => m.route === '/api');
|
||
if (existingParent) {
|
||
apiParentId = existingParent.id;
|
||
console.log(`API 主菜单已存在,ID: ${apiParentId}`);
|
||
}
|
||
} else {
|
||
console.error('创建 API 主菜单失败:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 5. 创建子菜单
|
||
const childMenus = menusToImport.filter(m => m.route !== '/api');
|
||
let successCount = 0;
|
||
let skipCount = 0;
|
||
let failCount = 0;
|
||
|
||
// 按 order 排序
|
||
const sortedChildMenus = [...childMenus].sort((a, b) => (a.order || 0) - (b.order || 0));
|
||
|
||
for (const menu of sortedChildMenus) {
|
||
try {
|
||
// 设置父菜单ID
|
||
if (menu.route?.startsWith('/api/') && apiParentId) {
|
||
menu.pId = apiParentId;
|
||
}
|
||
|
||
const result = await MenuService.postRbacMenuCreate(menu);
|
||
successCount++;
|
||
console.log(`✅ 成功导入菜单: ${menu.name} (${menu.route})`);
|
||
} catch (error: any) {
|
||
if (error.status === 400 || error.status === 409) {
|
||
skipCount++;
|
||
console.log(`⚠️ 菜单已存在,跳过: ${menu.name} (${menu.route})`);
|
||
} else {
|
||
failCount++;
|
||
console.error(`❌ 导入菜单失败: ${menu.name} (${menu.route})`, error);
|
||
}
|
||
}
|
||
}
|
||
|
||
console.log('\n=== 导入完成 ===');
|
||
console.log(`成功: ${successCount} 个`);
|
||
console.log(`跳过: ${skipCount} 个`);
|
||
console.log(`失败: ${failCount} 个`);
|
||
console.log(`总计: ${menusToImport.length} 个`);
|
||
|
||
} catch (error) {
|
||
console.error('批量导入菜单失败:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// 如果在浏览器环境中运行
|
||
if (typeof window !== 'undefined') {
|
||
(window as any).importApiMenus = importApiMenus;
|
||
console.log('API 菜单导入函数已挂载到 window.importApiMenus');
|
||
console.log('使用方法: await window.importApiMenus()');
|
||
}
|