sync: 同步管理前端 v1.0.4 并脱敏
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user