sync: 同步管理前端 v1.0.4 并脱敏

This commit is contained in:
MiFaZhan
2026-10-29 14:43:14 +08:00
parent f754b27ac2
commit 59b307ca75
236 changed files with 5013 additions and 708 deletions
@@ -0,0 +1,125 @@
import { describe, expect, it, vi } from 'vitest';
import { generateRoutesByBackend } from '../generate-routes-backend';
// 布局组件与页面组件必须是不同对象:实现里用「引用是否相同」判断是不是布局
const NullComponent = { render: () => null };
const NullLayout = { render: () => null };
const NullPage = { render: () => null };
const layoutMap = {
BasicLayout: NullLayout,
IFrameView: NullLayout,
LAYOUT: NullLayout,
};
const pageMap = { '/iotlcm/tDashboard/index.vue': NullPage };
/**
* 后端菜单里可能存在指向已下线页面的菜单(前端无对应视图)。
* 这类路由不应注册、也不应出现在菜单里,否则点进去就是一整页 404。
*/
describe('generateRoutesByBackend 丢弃无对应视图的路由', () => {
it('丢弃解析不到视图的路由,并保留同级正常路由', async () => {
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
try {
const routes = await generateRoutesByBackend({
fetchMenuListAsync: async () => [
{
children: [
{
component: 'common/tDivisionCode/index',
name: 'menu_137',
path: 'index',
},
{
component: 'iotlcm/tDashboard/index',
name: 'menu_ok',
path: 'dashboard',
},
],
component: 'LAYOUT',
name: 'Root',
path: '/',
},
],
layoutMap,
pageMap,
} as never);
expect(routes).toHaveLength(1);
const children = routes[0]?.children ?? [];
expect(children.map((c) => c.name)).toEqual(['menu_ok']);
expect(warn).toHaveBeenCalledWith(
expect.stringContaining('route component is invalid, route skipped'),
expect.objectContaining({ name: 'menu_137' }),
);
} finally {
warn.mockRestore();
}
});
it('子路由被丢弃后,父目录若变空也一并去掉', async () => {
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
try {
const routes = await generateRoutesByBackend({
fetchMenuListAsync: async () => [
{
children: [
{
component: 'common/tDivisionCode/index',
name: 'menu_137',
path: 'index',
},
],
component: 'LAYOUT',
name: 'EmptyDir',
path: '/empty',
},
{
component: 'iotlcm/tDashboard/index',
name: 'menu_ok',
path: '/ok',
},
],
layoutMap,
pageMap,
} as never);
expect(routes.map((r) => r.name)).toEqual(['menu_ok']);
} finally {
warn.mockRestore();
}
});
it('外链菜单(无组件但有 link)不会被丢弃', async () => {
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
try {
const routes = await generateRoutesByBackend({
fetchMenuListAsync: async () => [
{
children: [
{
meta: {
link: 'https://example.com/docs',
openInNewWindow: true,
},
name: 'menu_link',
path: 'https://example.com/docs',
},
],
component: 'LAYOUT',
name: 'LinkDir',
path: '/links',
},
],
layoutMap,
pageMap,
} as never);
expect(routes).toHaveLength(1);
expect(routes[0]?.children?.[0]?.name).toBe('menu_link');
} finally {
warn.mockRestore();
}
});
});
@@ -28,7 +28,11 @@ async function generateRoutesByBackend(
normalizePageMap[normalizeViewPath(key)] = value;
}
const routes = convertRoutes(menuRoutes, layoutMap, normalizePageMap);
const routes = convertRoutes(
removePathlessRoutes(menuRoutes),
layoutMap,
normalizePageMap,
);
return routes;
} catch (error) {
@@ -37,12 +41,31 @@ async function generateRoutesByBackend(
}
}
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[] {
return mapTree(routes, (node) => {
// 前端无对应视图、需丢弃的路由
const invalidRoutes = new WeakSet<object>();
const converted = mapTree(routes, (node) => {
const route = node as unknown as RouteRecordRaw;
const { component, name } = node;
@@ -62,13 +85,61 @@ function convertRoutes(
if (pageMap[pageKey]) {
route.component = pageMap[pageKey];
} else {
console.error(`route component is invalid: ${pageKey}`, route);
route.component = pageMap['/_core/fallback/not-found.vue'];
// 前端没有对应视图(后端菜单指向了已不存在的页面)。
// 这里不再替换成 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 {