From 30f7472d26d75d914085324ca8795f34a4e6cc7d Mon Sep 17 00:00:00 2001 From: vben Date: Sun, 2 Jun 2024 21:33:31 +0800 Subject: [PATCH] feat: support the dynamic introduction and sorting of routes --- apps/antd-view/src/router/guard.ts | 42 +++--- apps/antd-view/src/router/index.ts | 28 ++-- .../routes/{modules => dynamic}/nested.ts | 4 +- .../routes/{modules => dynamic}/outside.ts | 4 +- .../routes/{modules => dynamic}/root.ts | 5 +- .../routes/{modules => dynamic}/vben.ts | 4 +- .../src/router/routes/external/.gitkeep | 0 apps/antd-view/src/router/routes/index.ts | 36 ++--- .../src/router/routes/static/.gitkeep | 0 .../helpers/src/generator-menus.test.ts | 125 +++++++++++++----- .../forward/helpers/src/generator-menus.ts | 4 +- .../@vben-core/forward/helpers/src/index.ts | 1 + .../helpers/src/merge-route-modules.test.ts | 68 ++++++++++ .../helpers/src/merge-route-modules.ts | 28 ++++ .../shared/typings/src/vue-router.d.ts | 1 - 15 files changed, 257 insertions(+), 93 deletions(-) rename apps/antd-view/src/router/routes/{modules => dynamic}/nested.ts (96%) rename apps/antd-view/src/router/routes/{modules => dynamic}/outside.ts (92%) rename apps/antd-view/src/router/routes/{modules => dynamic}/root.ts (86%) rename apps/antd-view/src/router/routes/{modules => dynamic}/vben.ts (95%) create mode 100644 apps/antd-view/src/router/routes/external/.gitkeep create mode 100644 apps/antd-view/src/router/routes/static/.gitkeep create mode 100644 packages/@vben-core/forward/helpers/src/merge-route-modules.test.ts create mode 100644 packages/@vben-core/forward/helpers/src/merge-route-modules.ts diff --git a/apps/antd-view/src/router/guard.ts b/apps/antd-view/src/router/guard.ts index aa80320e..0bb08c24 100644 --- a/apps/antd-view/src/router/guard.ts +++ b/apps/antd-view/src/router/guard.ts @@ -10,11 +10,14 @@ import { useTitle } from '@vueuse/core'; import { dynamicRoutes } from '@/router/routes'; +// 不需要权限的页面白名单 +const WHITE_ROUTE_NAMES = new Set([]); + /** * 通用守卫配置 * @param router */ -function configCommonGuard(router: Router) { +function setupCommonGuard(router: Router) { // 记录已经加载的页面 const loadedPaths = new Set(); @@ -44,28 +47,11 @@ function configCommonGuard(router: Router) { }); } -// 不需要权限的页面白名单 -const WHITE_ROUTE_NAMES = new Set([]); - -/** - * 跳转登录页面 - * @param to - */ -function loginPageMeta(to: RouteLocationNormalized) { - return { - path: LOGIN_PATH, - // 如不需要,直接删除 query - query: { redirect: encodeURIComponent(to.fullPath) }, - // 携带当前跳转的页面,登录后重新跳转该页面 - replace: true, - }; -} - /** * 权限访问守卫配置 * @param router */ -function configAccessGuard(router: Router) { +function setupAccessGuard(router: Router) { router.beforeEach(async (to, from) => { const accessStore = useAccessStore(); const accessToken = accessStore.getAccessToken; @@ -123,7 +109,19 @@ function configAccessGuard(router: Router) { }); } -export { configAccessGuard }; +/** + * 登录页面信息 + * @param to + */ +function loginPageMeta(to: RouteLocationNormalized) { + return { + path: LOGIN_PATH, + // 如不需要,直接删除 query + query: { redirect: encodeURIComponent(to.fullPath) }, + // 携带当前跳转的页面,登录后重新跳转该页面 + replace: true, + }; +} /** * 项目守卫配置 @@ -131,9 +129,9 @@ export { configAccessGuard }; */ function createRouterGuard(router: Router) { /** 通用 */ - configCommonGuard(router); + setupCommonGuard(router); /** 权限访问 */ - configAccessGuard(router); + setupAccessGuard(router); } export { createRouterGuard }; diff --git a/apps/antd-view/src/router/index.ts b/apps/antd-view/src/router/index.ts index eed2a80a..dd9fd63f 100644 --- a/apps/antd-view/src/router/index.ts +++ b/apps/antd-view/src/router/index.ts @@ -4,7 +4,7 @@ import { traverseTreeValues } from '@vben/utils'; import { createRouter, createWebHashHistory } from 'vue-router'; import { createRouterGuard } from './guard'; -import { staticRoutes } from './routes'; +import { routes } from './routes'; /** * @zh_CN 创建vue-router实例 @@ -12,16 +12,14 @@ import { staticRoutes } from './routes'; const router = createRouter({ history: createWebHashHistory(import.meta.env.VITE_PUBLIC_PATH), // 应该添加到路由的初始路由列表。 - routes: staticRoutes, - scrollBehavior: (to, from, savedPosition) => { - if (to.path !== from.path) { - setTimeout(() => { - const app = document.querySelector('#app'); - if (app) { - app.scrollTop = 0; - } - }); - } + routes, + scrollBehavior: (_to, _from, savedPosition) => { + // if (to.path !== from.path) { + // const app = document.querySelector('#app'); + // if (app) { + // app.scrollTop = 0; + // } + // } return savedPosition || { left: 0, top: 0 }; }, }); @@ -34,9 +32,9 @@ function resetRoutes() { const staticRouteNames = traverseTreeValues< RouteRecordRaw, RouteRecordName | undefined - >(staticRoutes, (route) => { + >(routes, (route) => { // 这些路由需要指定 name,防止在路由重置时,不能删除没有指定 name 的路由 - if (!route.name) { + if (import.meta.env.DEV && !route.name) { console.warn( `The route with the path ${route.path} needs to specify the field name.`, ); @@ -45,8 +43,8 @@ function resetRoutes() { }); const { getRoutes, hasRoute, removeRoute } = router; - const routes = getRoutes(); - routes.forEach(({ name }) => { + const allRoutes = getRoutes(); + allRoutes.forEach(({ name }) => { // 存在于路由表且非白名单才需要删除 if (name && !staticRouteNames.includes(name) && hasRoute(name)) { removeRoute(name); diff --git a/apps/antd-view/src/router/routes/modules/nested.ts b/apps/antd-view/src/router/routes/dynamic/nested.ts similarity index 96% rename from apps/antd-view/src/router/routes/modules/nested.ts rename to apps/antd-view/src/router/routes/dynamic/nested.ts index a8ec4b9a..99b96b85 100644 --- a/apps/antd-view/src/router/routes/modules/nested.ts +++ b/apps/antd-view/src/router/routes/dynamic/nested.ts @@ -2,7 +2,7 @@ import type { RouteRecordRaw } from 'vue-router'; import { BasicLayout } from '@/layouts'; -export const nestedRoutes: RouteRecordRaw[] = [ +const routes: RouteRecordRaw[] = [ { component: BasicLayout, meta: { @@ -69,3 +69,5 @@ export const nestedRoutes: RouteRecordRaw[] = [ ], }, ]; + +export default routes; diff --git a/apps/antd-view/src/router/routes/modules/outside.ts b/apps/antd-view/src/router/routes/dynamic/outside.ts similarity index 92% rename from apps/antd-view/src/router/routes/modules/outside.ts rename to apps/antd-view/src/router/routes/dynamic/outside.ts index ad26b466..9727048b 100644 --- a/apps/antd-view/src/router/routes/modules/outside.ts +++ b/apps/antd-view/src/router/routes/dynamic/outside.ts @@ -2,7 +2,7 @@ import type { RouteRecordRaw } from 'vue-router'; import { BasicLayout, IFrameView } from '@/layouts'; -export const outsideRoutes: RouteRecordRaw[] = [ +const routes: RouteRecordRaw[] = [ { component: BasicLayout, meta: { @@ -35,3 +35,5 @@ export const outsideRoutes: RouteRecordRaw[] = [ ], }, ]; + +export default routes; diff --git a/apps/antd-view/src/router/routes/modules/root.ts b/apps/antd-view/src/router/routes/dynamic/root.ts similarity index 86% rename from apps/antd-view/src/router/routes/modules/root.ts rename to apps/antd-view/src/router/routes/dynamic/root.ts index 1b91603b..f7693b06 100644 --- a/apps/antd-view/src/router/routes/modules/root.ts +++ b/apps/antd-view/src/router/routes/dynamic/root.ts @@ -2,11 +2,12 @@ import type { RouteRecordRaw } from 'vue-router'; import { BasicLayout } from '@/layouts'; -const rootRoutes: RouteRecordRaw[] = [ +const routes: RouteRecordRaw[] = [ { component: BasicLayout, meta: { hideChildrenInMenu: true, + orderNo: -1, title: '首页', }, name: 'Home', @@ -26,4 +27,4 @@ const rootRoutes: RouteRecordRaw[] = [ }, ]; -export { rootRoutes }; +export default routes; diff --git a/apps/antd-view/src/router/routes/modules/vben.ts b/apps/antd-view/src/router/routes/dynamic/vben.ts similarity index 95% rename from apps/antd-view/src/router/routes/modules/vben.ts rename to apps/antd-view/src/router/routes/dynamic/vben.ts index bb180a92..ce9690cd 100644 --- a/apps/antd-view/src/router/routes/modules/vben.ts +++ b/apps/antd-view/src/router/routes/dynamic/vben.ts @@ -5,7 +5,7 @@ import { BasicLayout, IFrameView } from '@/layouts'; import { VBEN_GITHUB_URL } from '@vben/constants'; import { $t } from '@vben/locales/helper'; -export const vbenRoutes: RouteRecordRaw[] = [ +const routes: RouteRecordRaw[] = [ { component: BasicLayout, meta: { @@ -49,3 +49,5 @@ export const vbenRoutes: RouteRecordRaw[] = [ ], }, ]; + +export default routes; diff --git a/apps/antd-view/src/router/routes/external/.gitkeep b/apps/antd-view/src/router/routes/external/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/apps/antd-view/src/router/routes/index.ts b/apps/antd-view/src/router/routes/index.ts index 520cd86b..9c0645e7 100644 --- a/apps/antd-view/src/router/routes/index.ts +++ b/apps/antd-view/src/router/routes/index.ts @@ -1,24 +1,28 @@ +import { mergeRouteModules } from '@vben-core/helpers'; import type { RouteRecordRaw } from 'vue-router'; import { essentialRoutes } from './_essential'; -import { nestedRoutes } from './modules/nested'; -import { outsideRoutes } from './modules/outside'; -import { rootRoutes } from './modules/root'; -import { vbenRoutes } from './modules/vben'; + +const dynamicRouteFiles = import.meta.glob('./dynamic/**/*.ts', { + eager: true, +}); + +const staticRouteFiles = import.meta.glob('./static/**/*.ts', { eager: true }); + +const externalRouteFiles = import.meta.glob('./external/**/*.ts', { + eager: true, +}); /** 动态路由 */ -const dynamicRoutes: RouteRecordRaw[] = [ - // 根路由 - ...rootRoutes, - ...nestedRoutes, - ...outsideRoutes, - ...vbenRoutes, -]; - -/** 排除在主框架外的路由,这些路由没有菜单和顶部及其他框架内容 */ -const externalRoutes: RouteRecordRaw[] = []; +const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles); /** 静态路由列表,访问这些页面可以不需要权限 */ -const staticRoutes: RouteRecordRaw[] = [...essentialRoutes]; +const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles); -export { dynamicRoutes, externalRoutes, staticRoutes }; +/** 排除在主框架外的路由,这些路由没有菜单和顶部及其他框架内容 */ +const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles); + +/** 路由列表,由基本路由+静态路由组成 */ +const routes: RouteRecordRaw[] = [...essentialRoutes, ...staticRoutes]; + +export { dynamicRoutes, externalRoutes, routes }; diff --git a/apps/antd-view/src/router/routes/static/.gitkeep b/apps/antd-view/src/router/routes/static/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/packages/@vben-core/forward/helpers/src/generator-menus.test.ts b/packages/@vben-core/forward/helpers/src/generator-menus.test.ts index 0357631e..af21cad5 100644 --- a/packages/@vben-core/forward/helpers/src/generator-menus.test.ts +++ b/packages/@vben-core/forward/helpers/src/generator-menus.test.ts @@ -1,41 +1,46 @@ import { describe, expect, it, vi } from 'vitest'; import { generatorMenus } from './generator-menus'; // 替换为您的实际路径 -import type { RouteRecordRaw } from 'vue-router'; - -// 模拟路由数据 -const mockRoutes = [ - { - meta: { icon: 'home-icon', title: '首页' }, - name: 'home', - path: '/home', - }, - { - meta: { hideChildrenInMenu: true, icon: 'about-icon', title: '关于' }, - name: 'about', - path: '/about', - children: [ - { - path: 'team', - name: 'team', - meta: { icon: 'team-icon', title: '团队' }, - }, - ], - }, -] as RouteRecordRaw[]; - -// 模拟 Vue 路由器实例 -const mockRouter = { - getRoutes: vi.fn(() => [ - { name: 'home', path: '/home' }, - { name: 'about', path: '/about' }, - { name: 'team', path: '/about/team' }, - ]), -}; +import { + type RouteRecordRaw, + type Router, + createRouter, + createWebHistory, +} from 'vue-router'; // Nested route setup to test child inclusion and hideChildrenInMenu functionality describe('generatorMenus', () => { + // 模拟路由数据 + const mockRoutes = [ + { + meta: { icon: 'home-icon', title: '首页' }, + name: 'home', + path: '/home', + }, + { + meta: { hideChildrenInMenu: true, icon: 'about-icon', title: '关于' }, + name: 'about', + path: '/about', + children: [ + { + path: 'team', + name: 'team', + meta: { icon: 'team-icon', title: '团队' }, + }, + ], + }, + ] as RouteRecordRaw[]; + + // 模拟 Vue 路由器实例 + const mockRouter = { + getRoutes: vi.fn(() => [ + { name: 'home', path: '/home' }, + { name: 'about', path: '/about' }, + { name: 'team', path: '/about/team' }, + ]), + }; + it('the correct menu list should be generated according to the route', async () => { const expectedMenus = [ { @@ -138,8 +143,6 @@ describe('generatorMenus', () => { mockRoutesWithRedirect, mockRouter as any, ); - console.log(111, menus); - expect(menus).toEqual([ // Assuming your generatorMenus function excludes redirect routes from the menu { @@ -168,4 +171,60 @@ describe('generatorMenus', () => { }, ]); }); + + const routes: any = [ + { + meta: { orderNo: 2, title: 'Home' }, + name: 'home', + path: '/', + }, + { + meta: { orderNo: 1, title: 'About' }, + name: 'about', + path: '/about', + }, + ]; + + const router: Router = createRouter({ + history: createWebHistory(), + routes, + }); + + it('should generate menu list with correct order', async () => { + const menus = await generatorMenus(routes, router); + const expectedMenus = [ + { + badge: undefined, + badgeType: undefined, + badgeVariants: undefined, + icon: undefined, + name: 'About', + orderNo: 1, + parent: undefined, + parents: undefined, + path: '/about', + children: [], + }, + { + badge: undefined, + badgeType: undefined, + badgeVariants: undefined, + icon: undefined, + name: 'Home', + orderNo: 2, + parent: undefined, + parents: undefined, + path: '/', + children: [], + }, + ]; + + expect(menus).toEqual(expectedMenus); + }); + + it('should handle empty routes', async () => { + const emptyRoutes: any[] = []; + const menus = await generatorMenus(emptyRoutes, router); + expect(menus).toEqual([]); + }); }); diff --git a/packages/@vben-core/forward/helpers/src/generator-menus.ts b/packages/@vben-core/forward/helpers/src/generator-menus.ts index c5127a36..f04fe462 100644 --- a/packages/@vben-core/forward/helpers/src/generator-menus.ts +++ b/packages/@vben-core/forward/helpers/src/generator-menus.ts @@ -17,7 +17,7 @@ async function generatorMenus( router.getRoutes().map(({ name, path }) => [name, path]), ); - const menus = mapTree(routes, (route) => { + let menus = mapTree(routes, (route) => { // 路由表的路径写法有多种,这里从router获取到最终的path并赋值 const path = finalRoutesMap[route.name as string] ?? route.path; @@ -65,6 +65,8 @@ async function generatorMenus( }; }); + // 对菜单进行排序 + menus = menus.sort((a, b) => (a.orderNo || 999) - (b.orderNo || 999)); return menus; } diff --git a/packages/@vben-core/forward/helpers/src/index.ts b/packages/@vben-core/forward/helpers/src/index.ts index 06b99225..7f47ba14 100644 --- a/packages/@vben-core/forward/helpers/src/index.ts +++ b/packages/@vben-core/forward/helpers/src/index.ts @@ -1,4 +1,5 @@ export * from './flatten-object'; export * from './generator-menus'; export * from './generator-routes'; +export * from './merge-route-modules'; export * from './nested-object'; diff --git a/packages/@vben-core/forward/helpers/src/merge-route-modules.test.ts b/packages/@vben-core/forward/helpers/src/merge-route-modules.test.ts new file mode 100644 index 00000000..591ffd26 --- /dev/null +++ b/packages/@vben-core/forward/helpers/src/merge-route-modules.test.ts @@ -0,0 +1,68 @@ +import type { RouteRecordRaw } from 'vue-router'; + +import { describe, expect, it } from 'vitest'; + +import { mergeRouteModules } from './merge-route-modules'; + +import type { RouteModuleType } from './merge-route-modules'; + +describe('mergeRouteModules', () => { + it('should merge route modules correctly', () => { + const routeModules: Record = { + './dynamic-routes/about.ts': { + default: [ + { + component: () => Promise.resolve({ template: '
About
' }), + name: 'About', + path: '/about', + }, + ], + }, + './dynamic-routes/home.ts': { + default: [ + { + component: () => Promise.resolve({ template: '
Home
' }), + name: 'Home', + path: '/', + }, + ], + }, + }; + + const expectedRoutes: RouteRecordRaw[] = [ + { + component: expect.any(Function), + name: 'About', + path: '/about', + }, + { + component: expect.any(Function), + name: 'Home', + path: '/', + }, + ]; + + const mergedRoutes = mergeRouteModules(routeModules); + expect(mergedRoutes).toEqual(expectedRoutes); + }); + + it('should handle empty modules', () => { + const routeModules: Record = {}; + const expectedRoutes: RouteRecordRaw[] = []; + + const mergedRoutes = mergeRouteModules(routeModules); + expect(mergedRoutes).toEqual(expectedRoutes); + }); + + it('should handle modules with no default export', () => { + const routeModules: Record = { + './dynamic-routes/empty.ts': { + default: [], + }, + }; + const expectedRoutes: RouteRecordRaw[] = []; + + const mergedRoutes = mergeRouteModules(routeModules); + expect(mergedRoutes).toEqual(expectedRoutes); + }); +}); diff --git a/packages/@vben-core/forward/helpers/src/merge-route-modules.ts b/packages/@vben-core/forward/helpers/src/merge-route-modules.ts new file mode 100644 index 00000000..53e21f37 --- /dev/null +++ b/packages/@vben-core/forward/helpers/src/merge-route-modules.ts @@ -0,0 +1,28 @@ +import type { RouteRecordRaw } from 'vue-router'; + +// 定义模块类型 +interface RouteModuleType { + default: RouteRecordRaw[]; +} + +/** + * 合并动态路由模块的默认导出 + * @param routeModules 动态导入的路由模块对象 + * @returns 合并后的路由配置数组 + */ +function mergeRouteModules( + routeModules: Record, +): RouteRecordRaw[] { + const mergedRoutes: RouteRecordRaw[] = []; + + for (const routeModule of Object.values(routeModules)) { + const moduleRoutes = (routeModule as RouteModuleType)?.default ?? []; + mergedRoutes.push(...moduleRoutes); + } + + return mergedRoutes; +} + +export { mergeRouteModules }; + +export type { RouteModuleType }; diff --git a/packages/@vben-core/shared/typings/src/vue-router.d.ts b/packages/@vben-core/shared/typings/src/vue-router.d.ts index c0e12590..0e3b8426 100644 --- a/packages/@vben-core/shared/typings/src/vue-router.d.ts +++ b/packages/@vben-core/shared/typings/src/vue-router.d.ts @@ -42,7 +42,6 @@ interface RouteMeta { * @default false */ hideInMenu?: boolean; - /** * 当前路由在标签页不展现 * @default false