import type { RouteRecordRaw } from 'vue-router'; import type { ComponentRecordType, GenerateMenuAndRoutesOptions, RouteRecordStringComponent, } from '@vben-core/typings'; import { mapTree } from '@vben-core/shared/utils'; /** * 动态生成路由 - 后端方式 */ async function generateRoutesByBackend( options: GenerateMenuAndRoutesOptions, ): Promise { const { fetchMenuListAsync, layoutMap = {}, pageMap = {} } = options; try { const menuRoutes = await fetchMenuListAsync?.(); if (!menuRoutes) { return []; } const normalizePageMap: ComponentRecordType = {}; for (const [key, value] of Object.entries(pageMap)) { normalizePageMap[normalizeViewPath(key)] = value; } const routes = convertRoutes( removePathlessRoutes(menuRoutes), layoutMap, normalizePageMap, ); return routes; } catch (error) { console.error(error); throw error; } } function removePathlessRoutes( routes: RouteRecordStringComponent[], ): RouteRecordStringComponent[] { return routes.flatMap((route) => { const children = removePathlessRoutes(route.children ?? []); if (!route.path?.trim()) { console.warn('Skipping backend menu route with an empty path', { name: route.name, }); return children; } return [{ ...route, children }]; }); } function convertRoutes( routes: RouteRecordStringComponent[], layoutMap: ComponentRecordType, pageMap: ComponentRecordType, ): RouteRecordRaw[] { // 前端无对应视图、需丢弃的路由 const invalidRoutes = new WeakSet(); const converted = mapTree(routes, (node) => { const route = node as unknown as RouteRecordRaw; const { component, name } = node; if (!name) { console.error('route name is required', route); } // layout转换 if (component && layoutMap[component]) { route.component = layoutMap[component]; // 页面组件转换 } else if (component) { const normalizePath = normalizeViewPath(component); const pageKey = normalizePath.endsWith('.vue') ? normalizePath : `${normalizePath}.vue`; if (pageMap[pageKey]) { route.component = pageMap[pageKey]; } else { // 前端没有对应视图(后端菜单指向了已不存在的页面)。 // 这里不再替换成 not-found 组件——那样菜单还在、点进去却是 404 页面。 // 直接标记丢弃:路由不注册、菜单里也不出现这个死链。 console.warn(`route component is invalid, route skipped: ${pageKey}`, { name, path: node.path, }); invalidRoutes.add(route); } } return route; }); return pruneInvalidRoutes(converted, layoutMap, invalidRoutes); } /** * 丢弃解析不到视图的路由;并去掉因此变空的目录。 */ function pruneInvalidRoutes( routes: RouteRecordRaw[], layoutMap: ComponentRecordType, invalidRoutes: WeakSet, ): RouteRecordRaw[] { return routes .filter((route) => !invalidRoutes.has(route)) .map((route) => { if (!route.children) { return route; } return { ...route, children: pruneInvalidRoutes(route.children, layoutMap, invalidRoutes), }; }) .filter((route) => { const children = route.children ?? []; if (children.length > 0 || route.redirect) { return true; } const meta = route.meta as Record | undefined; // 外链菜单(新窗口打开)没有组件是正常的,不能丢 if (meta?.openInNewWindow || meta?.link) { return true; } // 只剩布局组件 = 变成了空目录,丢掉 const isLayout = Object.values(layoutMap).includes( route.component as never, ); if (isLayout) { return false; } return Boolean(route.component); }); } function normalizeViewPath(path: string): string { // 去除相对路径前缀 const normalizedPath = path.replace(/^(\.\/|\.\.\/)+/, ''); // 确保路径以 '/' 开头 const viewPath = normalizedPath.startsWith('/') ? normalizedPath : `/${normalizedPath}`; // 这里耦合了vben-admin的目录结构 return viewPath.replace(/^\/views/, ''); } export { generateRoutesByBackend };