Files
iotlcm/admin-ui/vue/packages/utils/src/helpers/generate-routes-backend.ts
T

158 lines
4.3 KiB
TypeScript

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<RouteRecordRaw[]> {
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<object>();
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<object>,
): 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<string, unknown> | 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 };