Commit e12c588c0abb8d639babd7f4b62abec111187302

Authored by Vben
1 parent 37320160

refactor(route): refactoring the routing multi-layer model close #215

.eslintrc.js
... ... @@ -38,15 +38,15 @@ module.exports = {
38 38 '@typescript-eslint/no-unused-vars': [
39 39 'error',
40 40 {
41   - argsIgnorePattern: '^h$',
42   - varsIgnorePattern: '^h$',
  41 + argsIgnorePattern: '^_',
  42 + varsIgnorePattern: '^_',
43 43 },
44 44 ],
45 45 'no-unused-vars': [
46 46 'error',
47 47 {
48   - argsIgnorePattern: '^h$',
49   - varsIgnorePattern: '^h$',
  48 + argsIgnorePattern: '^_',
  49 + varsIgnorePattern: '^_',
50 50 },
51 51 ],
52 52 'space-before-function-paren': 'off',
... ...
.vscode/settings.json
... ... @@ -8,7 +8,6 @@
8 8 "explorer.openEditors.visible": 0,
9 9 "editor.tabSize": 2,
10 10 "editor.renderControlCharacters": true,
11   - "window.zoomLevel": -1,
12 11 "editor.minimap.renderCharacters": false,
13 12 "editor.minimap.maxColumn": 300,
14 13 "editor.minimap.showSlider": "always",
... ...
CHANGELOG.zh_CN.md
  1 +## Wip
  2 +
  3 +### ✨ Refactor
  4 +
  5 +- 重构路由多层模式,解决嵌套 keepalive 执行多次问题
  6 +
1 7 ## 2.1.0 (2021-03-15)
2 8  
3 9 ### ✨ Features
... ...
build/vite/plugin/hmr.ts 0 → 100644
  1 +import type { Plugin } from 'vite';
  2 +
  3 +/**
  4 + * TODO
  5 + * Temporarily solve the Vite circular dependency problem, and wait for a better solution to fix it later. I don't know what problems this writing will bring.
  6 + * @returns
  7 + */
  8 +
  9 +export function configHmrPlugin(): Plugin {
  10 + return {
  11 + name: 'singleHMR',
  12 + handleHotUpdate({ modules, file }) {
  13 + if (file.match(/xml$/)) return [];
  14 + modules.forEach((m) => {
  15 + m.importedModules = new Set();
  16 + m.importers = new Set();
  17 + });
  18 + return modules;
  19 + },
  20 + };
  21 +}
... ...
build/vite/plugin/index.ts
... ... @@ -17,6 +17,7 @@ import { configThemePlugin } from './theme';
17 17 import { configImageminPlugin } from './imagemin';
18 18 import { configWindiCssPlugin } from './windicss';
19 19 import { configSvgIconsPlugin } from './svgSprite';
  20 +import { configHmrPlugin } from './hmr';
20 21  
21 22 export function createVitePlugins(viteEnv: ViteEnv, isBuild: boolean) {
22 23 const { VITE_USE_IMAGEMIN, VITE_USE_MOCK, VITE_LEGACY, VITE_BUILD_COMPRESS } = viteEnv;
... ... @@ -28,6 +29,9 @@ export function createVitePlugins(viteEnv: ViteEnv, isBuild: boolean) {
28 29 vueJsx(),
29 30 ];
30 31  
  32 + // TODO
  33 + !isBuild && vitePlugins.push(configHmrPlugin());
  34 +
31 35 // @vitejs/plugin-legacy
32 36 VITE_LEGACY && isBuild && vitePlugins.push(legacy());
33 37  
... ...
src/layouts/default/header/components/Breadcrumb.vue
... ... @@ -33,6 +33,7 @@
33 33 import { useGo } from '/@/hooks/web/usePage';
34 34 import { isString } from '/@/utils/is';
35 35 import { useI18n } from '/@/hooks/web/useI18n';
  36 + import { getMenus } from '/@/router/menus';
36 37  
37 38 export default defineComponent({
38 39 name: 'LayoutBreadcrumb',
... ... @@ -47,7 +48,7 @@
47 48 const { getShowBreadCrumbIcon } = useRootSetting();
48 49  
49 50 const { t } = useI18n();
50   - watchEffect(() => {
  51 + watchEffect(async () => {
51 52 if (currentRoute.value.name === REDIRECT_NAME) return;
52 53  
53 54 const matched = currentRoute.value?.matched;
... ...
src/layouts/page/ParentView.vue deleted 100644 → 0
1   -<!--
2   - * @Description: The reason is that tsx will report warnings under multi-level nesting.
3   --->
4   -<template>
5   - <div>
6   - <RouterView>
7   - <template #default="{ Component, route }">
8   - <transition
9   - :name="
10   - getTransitionName({
11   - route,
12   - openCache: openCache,
13   - enableTransition: getEnableTransition,
14   - cacheTabs: getCaches,
15   - def: getBasicTransition,
16   - })
17   - "
18   - mode="out-in"
19   - appear
20   - >
21   - <keep-alive v-if="openCache" :include="getCaches">
22   - <component :is="Component" v-bind="getKey(Component, route)" />
23   - </keep-alive>
24   - <component v-else :is="Component" v-bind="getKey(Component, route)" />
25   - </transition>
26   - </template>
27   - </RouterView>
28   - </div>
29   -</template>
30   -<script lang="ts">
31   - import { computed, defineComponent, unref } from 'vue';
32   -
33   - import { useRootSetting } from '/@/hooks/setting/useRootSetting';
34   - import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting';
35   -
36   - import { useTransitionSetting } from '/@/hooks/setting/useTransitionSetting';
37   - import { useCache, getKey } from './useCache';
38   - import { getTransitionName } from './transition';
39   -
40   - export default defineComponent({
41   - parentView: true,
42   - setup() {
43   - const { getCaches } = useCache(false);
44   -
45   - const { getShowMultipleTab } = useMultipleTabSetting();
46   -
47   - const { getOpenKeepAlive } = useRootSetting();
48   -
49   - const { getBasicTransition, getEnableTransition } = useTransitionSetting();
50   -
51   - const openCache = computed(() => unref(getOpenKeepAlive) && unref(getShowMultipleTab));
52   -
53   - return {
54   - getCaches,
55   - getBasicTransition,
56   - openCache,
57   - getEnableTransition,
58   - getTransitionName,
59   - getKey,
60   - };
61   - },
62   - });
63   -</script>
src/layouts/page/index.vue
... ... @@ -16,9 +16,9 @@
16 16 appear
17 17 >
18 18 <keep-alive v-if="openCache" :include="getCaches">
19   - <component :is="Component" v-bind="getKey(Component, route)" />
  19 + <component :is="Component" :key="route.fullPath" />
20 20 </keep-alive>
21   - <component v-else :is="Component" v-bind="getKey(Component, route)" />
  21 + <component v-else :is="Component" :key="route.fullPath" />
22 22 </transition>
23 23 </template>
24 24 </RouterView>
... ... @@ -34,15 +34,15 @@
34 34 import { useRootSetting } from '/@/hooks/setting/useRootSetting';
35 35  
36 36 import { useTransitionSetting } from '/@/hooks/setting/useTransitionSetting';
37   - import { useCache, getKey } from './useCache';
38 37 import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting';
39 38 import { getTransitionName } from './transition';
40 39  
  40 + import { useStore } from 'vuex';
  41 +
41 42 export default defineComponent({
42 43 name: 'PageLayout',
43 44 components: { FrameLayout },
44 45 setup() {
45   - const { getCaches } = useCache(true);
46 46 const { getShowMultipleTab } = useMultipleTabSetting();
47 47  
48 48 const { getOpenKeepAlive, getCanEmbedIFramePage } = useRootSetting();
... ... @@ -51,6 +51,17 @@
51 51  
52 52 const openCache = computed(() => unref(getOpenKeepAlive) && unref(getShowMultipleTab));
53 53  
  54 + const { getters } = useStore();
  55 +
  56 + const getCaches = computed((): string[] => {
  57 + if (!unref(getOpenKeepAlive)) {
  58 + return [];
  59 + }
  60 + // TODO The useStore is used here mainly to solve the problem of circular dependency hot update
  61 + const cacheTabs = getters['app-tab/getCachedTabsState'];
  62 + return cacheTabs;
  63 + });
  64 +
54 65 return {
55 66 getTransitionName,
56 67 openCache,
... ... @@ -58,7 +69,6 @@
58 69 getBasicTransition,
59 70 getCaches,
60 71 getCanEmbedIFramePage,
61   - getKey,
62 72 };
63 73 },
64 74 });
... ...
src/layouts/page/useCache.ts deleted 100644 → 0
1   -import type { FunctionalComponent } from 'vue';
2   -import type { RouteLocation } from 'vue-router';
3   -import { computed, ref, unref, getCurrentInstance } from 'vue';
4   -import { useRootSetting } from '/@/hooks/setting/useRootSetting';
5   -
6   -import { useRouter } from 'vue-router';
7   -import { useStore } from 'vuex';
8   -
9   -const ParentLayoutName = 'ParentLayout';
10   -
11   -const PAGE_LAYOUT_KEY = '__PAGE_LAYOUT__';
12   -
13   -export function getKey(component: FunctionalComponent & { type: Indexable }, route: RouteLocation) {
14   - return !!component?.type.parentView ? {} : { key: route.fullPath };
15   -}
16   -
17   -export function useCache(isPage: boolean) {
18   - const { getters } = useStore();
19   -
20   - const name = ref('');
21   - const { currentRoute } = useRouter();
22   - const instance = getCurrentInstance();
23   - const routeName = instance?.type.name;
24   - if (routeName && ![ParentLayoutName].includes(routeName)) {
25   - name.value = routeName;
26   - } else {
27   - const matched = currentRoute.value?.matched;
28   - if (!matched) {
29   - return;
30   - }
31   - const len = matched.length;
32   - if (len < 2) return;
33   - name.value = matched[len - 2].name as string;
34   - }
35   -
36   - const { getOpenKeepAlive } = useRootSetting();
37   -
38   - const getCaches = computed((): string[] => {
39   - if (!unref(getOpenKeepAlive)) {
40   - return [];
41   - }
42   - const cached = getters['app-tab/getCachedMapState'];
43   -
44   - if (isPage) {
45   - // page Layout
46   - return cached.get(PAGE_LAYOUT_KEY) || [];
47   - }
48   - const cacheSet = new Set<string>();
49   - cacheSet.add(unref(name));
50   -
51   - const list = cached.get(unref(name));
52   -
53   - if (!list) {
54   - return Array.from(cacheSet);
55   - }
56   - list.forEach((item) => {
57   - cacheSet.add(item);
58   - });
59   -
60   - return Array.from(cacheSet);
61   - });
62   - return { getCaches };
63   -}
src/logics/mitt/tabChange.ts
... ... @@ -4,7 +4,7 @@
4 4  
5 5 import Mitt from '/@/utils/mitt';
6 6 import type { RouteLocationNormalized } from 'vue-router';
7   -import { getRoute } from '/@/router/helper/routeHelper';
  7 +import { getRawRoute } from '/@/utils';
8 8  
9 9 const mitt = new Mitt();
10 10  
... ... @@ -13,7 +13,7 @@ const key = Symbol();
13 13 let lastChangeTab: RouteLocationNormalized;
14 14  
15 15 export function setLastChangeTab(lastChangeRoute: RouteLocationNormalized) {
16   - const r = getRoute(lastChangeRoute);
  16 + const r = getRawRoute(lastChangeRoute);
17 17 mitt.emit(key, r);
18 18 lastChangeTab = r;
19 19 }
... ...
src/router/constant.ts
1   -import type { AppRouteRecordRaw } from '/@/router/types';
2   -import ParentLayout from '/@/layouts/page/ParentView.vue';
3   -import { t } from '/@/hooks/web/useI18n';
4   -
5 1 export const REDIRECT_NAME = 'Redirect';
6 2  
  3 +export const PARENT_LAYOUT_NAME = 'ParentLayout';
  4 +
7 5 export const EXCEPTION_COMPONENT = () => import('../views/sys/exception/Exception.vue');
8 6  
9 7 /**
... ... @@ -12,78 +10,23 @@ export const EXCEPTION_COMPONENT = () =&gt; import(&#39;../views/sys/exception/Exceptio
12 10 export const LAYOUT = () => import('/@/layouts/default/index.vue');
13 11  
14 12 /**
15   - * @description: page-layout
  13 + * @description: parent-layout
16 14 */
17   -export const getParentLayout = (name: string) => {
  15 +export const getParentLayout = (_name?: string) => {
18 16 return () =>
19 17 new Promise((resolve) => {
20 18 resolve({
21   - ...ParentLayout,
22   - name,
  19 + name: PARENT_LAYOUT_NAME,
23 20 });
24 21 });
25 22 };
26 23  
27   -// 404 on a page
28   -export const PAGE_NOT_FOUND_ROUTE: AppRouteRecordRaw = {
29   - path: '/:path(.*)*',
30   - name: 'ErrorPage',
31   - component: LAYOUT,
32   - meta: {
33   - title: 'ErrorPage',
34   - hideBreadcrumb: true,
35   - },
36   - children: [
37   - {
38   - path: '/:path(.*)*',
39   - name: 'ErrorPage',
40   - component: EXCEPTION_COMPONENT,
41   - meta: {
42   - title: 'ErrorPage',
43   - hideBreadcrumb: true,
44   - },
45   - },
46   - ],
47   -};
48   -
49   -export const REDIRECT_ROUTE: AppRouteRecordRaw = {
50   - path: '/redirect',
51   - name: REDIRECT_NAME,
52   - component: LAYOUT,
53   - meta: {
54   - title: REDIRECT_NAME,
55   - hideBreadcrumb: true,
56   - },
57   - children: [
58   - {
59   - path: '/redirect/:path(.*)',
60   - name: REDIRECT_NAME,
61   - component: () => import('/@/views/sys/redirect/index.vue'),
62   - meta: {
63   - title: REDIRECT_NAME,
64   - hideBreadcrumb: true,
65   - },
66   - },
67   - ],
68   -};
69   -
70   -export const ERROR_LOG_ROUTE: AppRouteRecordRaw = {
71   - path: '/error-log',
72   - name: 'errorLog',
73   - component: LAYOUT,
74   - meta: {
75   - title: 'ErrorLog',
76   - hideBreadcrumb: true,
77   - },
78   - children: [
79   - {
80   - path: 'list',
81   - name: 'errorLogList',
82   - component: () => import('/@/views/sys/error-log/index.vue'),
83   - meta: {
84   - title: t('routes.basic.errorLogList'),
85   - hideBreadcrumb: true,
86   - },
87   - },
88   - ],
89   -};
  24 +// export const getParentLayout = (name: string) => {
  25 +// return () =>
  26 +// new Promise((resolve) => {
  27 +// resolve({
  28 +// ...ParentLayout,
  29 +// name,
  30 +// });
  31 +// });
  32 +// };
... ...
src/router/guard/permissionGuard.ts
... ... @@ -5,7 +5,7 @@ import { permissionStore } from &#39;/@/store/modules/permission&#39;;
5 5 import { PageEnum } from '/@/enums/pageEnum';
6 6 import { userStore } from '/@/store/modules/user';
7 7  
8   -import { PAGE_NOT_FOUND_ROUTE } from '/@/router/constant';
  8 +import { PAGE_NOT_FOUND_ROUTE } from '/@/router/routes/basic';
9 9  
10 10 const LOGIN_PATH = PageEnum.BASE_LOGIN;
11 11  
... ...
src/router/helper/menuHelper.ts
1 1 import { AppRouteModule } from '/@/router/types';
2 2 import type { MenuModule, Menu, AppRouteRecordRaw } from '/@/router/types';
3 3  
4   -import { findPath, forEach, treeMap } from '/@/utils/helper/treeHelper';
  4 +import { findPath, treeMap } from '/@/utils/helper/treeHelper';
5 5 import { cloneDeep } from 'lodash-es';
6 6 import { isUrl } from '/@/utils/is';
7 7  
8   -export function getAllParentPath(treeData: any[], path: string) {
  8 +export function getAllParentPath<T = Recordable>(treeData: T[], path: string) {
9 9 const menuList = findPath(treeData, (n) => n.path === path) as Menu[];
10 10 return (menuList || []).map((item) => item.path);
11 11 }
12 12  
13   -// 拼接父级路径
14   -function joinParentPath(list: any, node: any) {
15   - let allPaths = getAllParentPath(list, node.path);
16   -
17   - allPaths = allPaths.slice(0, allPaths.length - 1);
18   - let parentPath = '';
19   - if (Array.isArray(allPaths) && allPaths.length >= 2) {
20   - parentPath = allPaths[allPaths.length - 1];
21   - } else {
22   - allPaths.forEach((p) => {
23   - parentPath += /^\//.test(p) ? p : `/${p}`;
24   - });
  13 +function joinParentPath(menus: Menu[], parentPath = '') {
  14 + for (let index = 0; index < menus.length; index++) {
  15 + const menu = menus[index];
  16 + const p = menu.path.startsWith('/') ? menu.path : `/${menu.path}`;
  17 + const parent = isUrl(menu.path) ? menu.path : `${parentPath}${p}`;
  18 + menus[index].path = parent;
  19 + if (menu?.children?.length) {
  20 + joinParentPath(menu.children, parent);
  21 + }
25 22 }
26   - node.path = `${/^\//.test(node.path) ? node.path : `${parentPath}/${node.path}`}`.replace(
27   - /\/\//g,
28   - '/'
29   - );
30   - return node;
31 23 }
32 24  
33   -// 解析菜单模块
  25 +// Parsing the menu module
34 26 export function transformMenuModule(menuModule: MenuModule): Menu {
35 27 const { menu } = menuModule;
36 28  
37 29 const menuList = [menu];
38   - forEach(menuList, (m) => {
39   - !isUrl(m.path) && joinParentPath(menuList, m);
40   - });
41 30  
  31 + joinParentPath(menuList);
42 32 return menuList[0];
43 33 }
44 34  
... ... @@ -54,17 +44,16 @@ export function transformRouteToMenu(routeModList: AppRouteModule[]) {
54 44 routeList.push(item);
55 45 }
56 46 });
57   - return treeMap(routeList, {
  47 + const list = treeMap(routeList, {
58 48 conversion: (node: AppRouteRecordRaw) => {
59   - const { meta: { title, icon, hideMenu = false } = {} } = node;
60   -
61   - !isUrl(node.path) && joinParentPath(routeList, node);
  49 + const { meta: { title, hideMenu = false } = {} } = node;
62 50 return {
  51 + ...(node.meta || {}),
63 52 name: title,
64   - icon,
65   - path: node.path,
66 53 hideMenu,
67 54 };
68 55 },
69 56 });
  57 + joinParentPath(list);
  58 + return list;
70 59 }
... ...
src/router/helper/routeHelper.ts
1 1 import type { AppRouteModule, AppRouteRecordRaw } from '/@/router/types';
2   -import type { RouteLocationNormalized, RouteRecordNormalized } from 'vue-router';
  2 +import type { Router, RouteRecordNormalized } from 'vue-router';
3 3  
4 4 import { getParentLayout, LAYOUT } from '/@/router/constant';
5 5 import { cloneDeep } from 'lodash-es';
6 6 import { warn } from '/@/utils/log';
  7 +import { createRouter, createWebHashHistory } from 'vue-router';
7 8  
8 9 export type LayoutMapKey = 'LAYOUT';
9 10  
10 11 const LayoutMap = new Map<LayoutMapKey, () => Promise<typeof import('*.vue')>>();
11 12  
12   -let dynamicViewsModules: Record<
13   - string,
14   - () => Promise<{
15   - [key: string]: any;
16   - }>
17   ->;
  13 +let dynamicViewsModules: Record<string, () => Promise<Recordable>>;
18 14  
19   -// 动态引入
  15 +// Dynamic introduction
20 16 function asyncImportRoute(routes: AppRouteRecordRaw[] | undefined) {
21 17 dynamicViewsModules = dynamicViewsModules || import.meta.glob('../../views/**/*.{vue,tsx}');
22 18 if (!routes) return;
... ... @@ -26,19 +22,14 @@ function asyncImportRoute(routes: AppRouteRecordRaw[] | undefined) {
26 22 if (component) {
27 23 item.component = dynamicImport(dynamicViewsModules, component as string);
28 24 } else if (name) {
29   - item.component = getParentLayout(name);
  25 + item.component = getParentLayout();
30 26 }
31 27 children && asyncImportRoute(children);
32 28 });
33 29 }
34 30  
35 31 function dynamicImport(
36   - dynamicViewsModules: Record<
37   - string,
38   - () => Promise<{
39   - [key: string]: any;
40   - }>
41   - >,
  32 + dynamicViewsModules: Record<string, () => Promise<Recordable>>,
42 33 component: string
43 34 ) {
44 35 const keys = Object.keys(dynamicViewsModules);
... ... @@ -84,18 +75,69 @@ export function transformObjToRoute&lt;T = AppRouteModule&gt;(routeList: AppRouteModul
84 75 return (routeList as unknown) as T[];
85 76 }
86 77  
87   -// Return to the new routing structure, not affected by the original example
88   -export function getRoute(route: RouteLocationNormalized): RouteLocationNormalized {
89   - if (!route) return route;
90   - const { matched, ...opt } = route;
91   - return {
92   - ...opt,
93   - matched: (matched
94   - ? matched.map((item) => ({
95   - meta: item.meta,
96   - name: item.name,
97   - path: item.path,
98   - }))
99   - : undefined) as RouteRecordNormalized[],
100   - };
  78 +/**
  79 + * Convert multi-level routing to level 2 routing
  80 + */
  81 +export function flatRoutes(routeModules: AppRouteModule[]) {
  82 + for (let index = 0; index < routeModules.length; index++) {
  83 + const routeModule = routeModules[index];
  84 + if (!isMultipleRoute(routeModule)) {
  85 + continue;
  86 + }
  87 + promoteRouteLevel(routeModule);
  88 + }
  89 +}
  90 +
  91 +// Routing level upgrade
  92 +function promoteRouteLevel(routeModule: AppRouteModule) {
  93 + // Use vue-router to splice menus
  94 + let router: Router | null = createRouter({
  95 + routes: [routeModule as any],
  96 + history: createWebHashHistory(),
  97 + });
  98 +
  99 + const routes = router.getRoutes();
  100 + const children = cloneDeep(routeModule.children);
  101 + addToChildren(routes, children || [], routeModule);
  102 + router = null;
  103 +
  104 + routeModule.children = routeModule.children?.filter((item) => !item.children?.length);
  105 +}
  106 +
  107 +// Add all sub-routes to the secondary route
  108 +function addToChildren(
  109 + routes: RouteRecordNormalized[],
  110 + children: AppRouteRecordRaw[],
  111 + routeModule: AppRouteModule
  112 +) {
  113 + for (let index = 0; index < children.length; index++) {
  114 + const child = children[index];
  115 + const route = routes.find((item) => item.name === child.name);
  116 + if (route) {
  117 + routeModule.children = routeModule.children || [];
  118 + routeModule.children?.push(route as any);
  119 + if (child.children?.length) {
  120 + addToChildren(routes, child.children, routeModule);
  121 + }
  122 + }
  123 + }
  124 +}
  125 +
  126 +// Determine whether the level exceeds 2 levels
  127 +function isMultipleRoute(routeModule: AppRouteModule) {
  128 + if (!routeModule || !Reflect.has(routeModule, 'children') || !routeModule.children?.length) {
  129 + return false;
  130 + }
  131 +
  132 + const children = routeModule.children;
  133 +
  134 + let flag = false;
  135 + for (let index = 0; index < children.length; index++) {
  136 + const child = children[index];
  137 + if (child.children?.length) {
  138 + flag = true;
  139 + break;
  140 + }
  141 + }
  142 + return flag;
101 143 }
... ...
src/router/menus/index.ts
... ... @@ -5,6 +5,7 @@ import { appStore } from &#39;/@/store/modules/app&#39;;
5 5 import { permissionStore } from '/@/store/modules/permission';
6 6 import { transformMenuModule, getAllParentPath } from '/@/router/helper/menuHelper';
7 7 import { filter } from '/@/utils/helper/treeHelper';
  8 +import { isUrl } from '/@/utils/is';
8 9 import router from '/@/router';
9 10 import { PermissionModeEnum } from '/@/enums/appEnum';
10 11 import { pathToRegexp } from 'path-to-regexp';
... ... @@ -19,8 +20,6 @@ Object.keys(modules).forEach((key) =&gt; {
19 20 menuModules.push(...modList);
20 21 });
21 22  
22   -const reg = /(((https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/;
23   -
24 23 // ===========================
25 24 // ==========Helper===========
26 25 // ===========================
... ... @@ -40,18 +39,15 @@ const staticMenus: Menu[] = [];
40 39 })();
41 40  
42 41 async function getAsyncMenus() {
43   - // 前端角色控制菜单 直接取菜单文件
44 42 return !isBackMode() ? staticMenus : permissionStore.getBackMenuListState;
45 43 }
46 44  
47   -// 获取菜单 树级
48 45 export const getMenus = async (): Promise<Menu[]> => {
49 46 const menus = await getAsyncMenus();
50 47 const routes = router.getRoutes();
51 48 return !isBackMode() ? filter(menus, basicFilter(routes)) : menus;
52 49 };
53 50  
54   -// 获取当前路径的顶级路径
55 51 export async function getCurrentParentPath(currentPath: string) {
56 52 const menus = await getAsyncMenus();
57 53  
... ... @@ -60,7 +56,7 @@ export async function getCurrentParentPath(currentPath: string) {
60 56 return allParentPath?.[0];
61 57 }
62 58  
63   -// 获取1级菜单,删除children
  59 +// Get the level 1 menu, delete children
64 60 export async function getShallowMenus(): Promise<Menu[]> {
65 61 const menus = await getAsyncMenus();
66 62 const routes = router.getRoutes();
... ... @@ -68,7 +64,7 @@ export async function getShallowMenus(): Promise&lt;Menu[]&gt; {
68 64 return !isBackMode() ? shallowMenuList.filter(basicFilter(routes)) : shallowMenuList;
69 65 }
70 66  
71   -// 获取菜单的children
  67 +// Get the children of the menu
72 68 export async function getChildrenMenus(parentPath: string) {
73 69 const menus = await getAsyncMenus();
74 70 const parent = menus.find((item) => item.path === parentPath);
... ... @@ -78,14 +74,10 @@ export async function getChildrenMenus(parentPath: string) {
78 74 return !isBackMode() ? filter(parent.children, basicFilter(routes)) : parent.children;
79 75 }
80 76  
81   -// 通用过滤方法
82 77 function basicFilter(routes: RouteRecordNormalized[]) {
83 78 return (menu: Menu) => {
84 79 const matchRoute = routes.find((route) => {
85   - const match = route.path.match(reg)?.[0];
86   - if (match && match === menu.path) {
87   - return true;
88   - }
  80 + if (isUrl(menu.path)) return true;
89 81  
90 82 if (route.meta?.carryParam) {
91 83 return pathToRegexp(route.path).test(menu.path);
... ...
src/router/routes/basic.ts 0 → 100644
  1 +import type { AppRouteRecordRaw } from '/@/router/types';
  2 +import { t } from '/@/hooks/web/useI18n';
  3 +import { REDIRECT_NAME, LAYOUT, EXCEPTION_COMPONENT } from '/@/router/constant';
  4 +
  5 +// 404 on a page
  6 +export const PAGE_NOT_FOUND_ROUTE: AppRouteRecordRaw = {
  7 + path: '/:path(.*)*',
  8 + name: 'ErrorPage',
  9 + component: LAYOUT,
  10 + meta: {
  11 + title: 'ErrorPage',
  12 + hideBreadcrumb: true,
  13 + },
  14 + children: [
  15 + {
  16 + path: '/:path(.*)*',
  17 + name: 'ErrorPage',
  18 + component: EXCEPTION_COMPONENT,
  19 + meta: {
  20 + title: 'ErrorPage',
  21 + hideBreadcrumb: true,
  22 + },
  23 + },
  24 + ],
  25 +};
  26 +
  27 +export const REDIRECT_ROUTE: AppRouteRecordRaw = {
  28 + path: '/redirect',
  29 + name: REDIRECT_NAME,
  30 + component: LAYOUT,
  31 + meta: {
  32 + title: REDIRECT_NAME,
  33 + hideBreadcrumb: true,
  34 + },
  35 + children: [
  36 + {
  37 + path: '/redirect/:path(.*)',
  38 + name: REDIRECT_NAME,
  39 + component: () => import('/@/views/sys/redirect/index.vue'),
  40 + meta: {
  41 + title: REDIRECT_NAME,
  42 + hideBreadcrumb: true,
  43 + },
  44 + },
  45 + ],
  46 +};
  47 +
  48 +export const ERROR_LOG_ROUTE: AppRouteRecordRaw = {
  49 + path: '/error-log',
  50 + name: 'errorLog',
  51 + component: LAYOUT,
  52 + meta: {
  53 + title: 'ErrorLog',
  54 + hideBreadcrumb: true,
  55 + },
  56 + children: [
  57 + {
  58 + path: 'list',
  59 + name: 'errorLogList',
  60 + component: () => import('/@/views/sys/error-log/index.vue'),
  61 + meta: {
  62 + title: t('routes.basic.errorLogList'),
  63 + hideBreadcrumb: true,
  64 + },
  65 + },
  66 + ],
  67 +};
... ...
src/router/routes/index.ts
1 1 import type { AppRouteRecordRaw, AppRouteModule } from '/@/router/types';
2 2  
3   -import { PAGE_NOT_FOUND_ROUTE, REDIRECT_ROUTE } from '../constant';
  3 +import { PAGE_NOT_FOUND_ROUTE, REDIRECT_ROUTE } from '/@/router/routes/basic';
4 4  
5 5 import { mainOutRoutes } from './mainOut';
6 6 import { PageEnum } from '/@/enums/pageEnum';
7 7 import { t } from '/@/hooks/web/useI18n';
  8 +import { flatRoutes } from '/@/router/helper/routeHelper';
8 9  
9 10 const modules = import.meta.globEager('./modules/**/*.ts');
10 11  
... ... @@ -16,6 +17,9 @@ Object.keys(modules).forEach((key) =&gt; {
16 17 routeModuleList.push(...modList);
17 18 });
18 19  
  20 +// Multi-level routing conversion
  21 +flatRoutes(routeModuleList);
  22 +
19 23 export const asyncRoutes = [PAGE_NOT_FOUND_ROUTE, ...routeModuleList];
20 24  
21 25 export const RootRoute: AppRouteRecordRaw = {
... ...
src/store/modules/permission.ts
... ... @@ -14,12 +14,12 @@ import { filter } from &#39;/@/utils/helper/treeHelper&#39;;
14 14 import { toRaw } from 'vue';
15 15 import { getMenuListById } from '/@/api/sys/menu';
16 16  
17   -import { transformObjToRoute } from '/@/router/helper/routeHelper';
  17 +import { transformObjToRoute, flatRoutes } from '/@/router/helper/routeHelper';
18 18 import { transformRouteToMenu } from '/@/router/helper/menuHelper';
19 19  
20 20 import { useMessage } from '/@/hooks/web/useMessage';
21 21 import { useI18n } from '/@/hooks/web/useI18n';
22   -import { ERROR_LOG_ROUTE, PAGE_NOT_FOUND_ROUTE } from '/@/router/constant';
  22 +import { ERROR_LOG_ROUTE, PAGE_NOT_FOUND_ROUTE } from '/@/router/routes/basic';
23 23  
24 24 const { createMessage } = useMessage();
25 25 const NAME = 'app-permission';
... ... @@ -113,11 +113,12 @@ class Permission extends VuexModule {
113 113  
114 114 // Dynamically introduce components
115 115 routeList = transformObjToRoute(routeList);
  116 +
116 117 // Background routing to menu structure
117 118 const backMenuList = transformRouteToMenu(routeList);
118   -
119 119 this.commitBackMenuListState(backMenuList);
120 120  
  121 + flatRoutes(routeList);
121 122 routes = [PAGE_NOT_FOUND_ROUTE, ...routeList];
122 123 }
123 124 routes.push(ERROR_LOG_ROUTE);
... ...
src/store/modules/tab.ts
... ... @@ -8,8 +8,8 @@ import { PageEnum } from &#39;/@/enums/pageEnum&#39;;
8 8  
9 9 import store from '/@/store';
10 10 import router from '/@/router';
11   -import { PAGE_NOT_FOUND_ROUTE, REDIRECT_ROUTE } from '/@/router/constant';
12   -import { getRoute } from '/@/router/helper/routeHelper';
  11 +import { PAGE_NOT_FOUND_ROUTE, REDIRECT_ROUTE } from '/@/router/routes/basic';
  12 +import { getRawRoute } from '/@/utils';
13 13  
14 14 import { useGo, useRedo } from '/@/hooks/web/usePage';
15 15 import { cloneDeep } from 'lodash-es';
... ... @@ -18,8 +18,6 @@ const NAME = &#39;app-tab&#39;;
18 18  
19 19 hotModuleUnregisterModule(NAME);
20 20  
21   -export const PAGE_LAYOUT_KEY = '__PAGE_LAYOUT__';
22   -
23 21 function isGotoPage() {
24 22 const go = useGo();
25 23 go(unref(router.currentRoute).path, true);
... ... @@ -27,7 +25,7 @@ function isGotoPage() {
27 25  
28 26 @Module({ namespaced: true, name: NAME, dynamic: true, store })
29 27 class Tab extends VuexModule {
30   - cachedMapState = new Map<string, string[]>();
  28 + cachedTabsState: Set<string> = new Set();
31 29  
32 30 // tab list
33 31 tabsState: RouteLocationNormalized[] = [];
... ... @@ -43,8 +41,8 @@ class Tab extends VuexModule {
43 41 return this.tabsState.find((item) => item.path === route.path)!;
44 42 }
45 43  
46   - get getCachedMapState(): Map<string, string[]> {
47   - return this.cachedMapState;
  44 + get getCachedTabsState(): string[] {
  45 + return Array.from(this.cachedTabsState);
48 46 }
49 47  
50 48 get getLastDragEndIndexState(): number {
... ... @@ -53,7 +51,7 @@ class Tab extends VuexModule {
53 51  
54 52 @Mutation
55 53 commitClearCache(): void {
56   - this.cachedMapState = new Map();
  54 + this.cachedTabsState = new Set();
57 55 }
58 56  
59 57 @Mutation
... ... @@ -77,46 +75,16 @@ class Tab extends VuexModule {
77 75  
78 76 @Mutation
79 77 commitCachedMapState(): void {
80   - const cacheMap = new Map<string, string[]>();
  78 + const cacheMap: Set<string> = new Set();
81 79  
82   - const pageCacheSet = new Set<string>();
83 80 this.tabsState.forEach((tab) => {
84   - const item = getRoute(tab);
  81 + const item = getRawRoute(tab);
85 82 const needCache = !item.meta?.ignoreKeepAlive;
86 83 if (!needCache) return;
87   -
88   - if (item.meta?.affix) {
89   - const name = item.name as string;
90   - pageCacheSet.add(name);
91   - } else if (item?.matched && needCache) {
92   - const matched = item?.matched;
93   - if (!matched) return;
94   - const len = matched.length;
95   -
96   - if (len < 2) return;
97   -
98   - for (let i = 0; i < matched.length; i++) {
99   - const key = matched[i].name as string;
100   -
101   - if (i < 2) {
102   - pageCacheSet.add(key);
103   - }
104   - if (i < len - 1) {
105   - const { meta, name } = matched[i + 1];
106   - if (meta && (meta.affix || needCache)) {
107   - const mapList = cacheMap.get(key) || [];
108   - if (!mapList.includes(name as string)) {
109   - mapList.push(name as string);
110   - }
111   - cacheMap.set(key, mapList);
112   - }
113   - }
114   - }
115   - }
  84 + const name = item.name as string;
  85 + cacheMap.add(name);
116 86 });
117   -
118   - cacheMap.set(PAGE_LAYOUT_KEY, Array.from(pageCacheSet));
119   - this.cachedMapState = cacheMap;
  87 + this.cachedTabsState = cacheMap;
120 88 }
121 89  
122 90 @Mutation
... ... @@ -162,7 +130,7 @@ class Tab extends VuexModule {
162 130 @Mutation
163 131 commitResetState(): void {
164 132 this.tabsState = [];
165   - this.cachedMapState = new Map();
  133 + this.cachedTabsState = new Set();
166 134 }
167 135  
168 136 @Mutation
... ... @@ -190,7 +158,7 @@ class Tab extends VuexModule {
190 158 ) {
191 159 return;
192 160 }
193   - this.commitTabRoutesState(getRoute(route));
  161 + this.commitTabRoutesState(getRawRoute(route));
194 162  
195 163 this.commitCachedMapState();
196 164 }
... ... @@ -198,17 +166,12 @@ class Tab extends VuexModule {
198 166 @Mutation
199 167 async commitRedoPage() {
200 168 const route = router.currentRoute.value;
201   - for (const [key, value] of this.cachedMapState) {
202   - const index = value.findIndex((item) => item === (route.name as string));
203   - if (index === -1) {
204   - continue;
205   - }
206   - if (value.length === 1) {
207   - this.cachedMapState.delete(key);
208   - continue;
209   - }
210   - value.splice(index, 1);
211   - this.cachedMapState.set(key, value);
  169 + const name = route.name;
  170 +
  171 + const findVal = Array.from(this.cachedTabsState).find((item) => item === name);
  172 + if (findVal) {
  173 + this.cachedTabsState.delete(findVal);
  174 + // this.cachedTabsState.splice(index, 1);
212 175 }
213 176 const redo = useRedo();
214 177 await redo();
... ...
src/utils/index.ts
1   -export const timestamp = () => +Date.now();
  1 +import type { RouteLocationNormalized, RouteRecordNormalized } from 'vue-router';
2 2 import { unref } from 'vue';
3 3 import { isObject } from '/@/utils/is';
4   -export const clamp = (n: number, min: number, max: number) => Math.min(max, Math.max(min, n));
  4 +
5 5 export const noop = () => {};
6   -export const now = () => Date.now();
7 6  
8 7 /**
9 8 * @description: Set ui mount node
... ... @@ -91,3 +90,18 @@ export function setTitle(title: string, appTitle?: string) {
91 90 setDocumentTitle(_title);
92 91 }
93 92 }
  93 +
  94 +export function getRawRoute(route: RouteLocationNormalized): RouteLocationNormalized {
  95 + if (!route) return route;
  96 + const { matched, ...opt } = route;
  97 + return {
  98 + ...opt,
  99 + matched: (matched
  100 + ? matched.map((item) => ({
  101 + meta: item.meta,
  102 + name: item.name,
  103 + path: item.path,
  104 + }))
  105 + : undefined) as RouteRecordNormalized[],
  106 + };
  107 +}
... ...
vite.config.ts
... ... @@ -43,7 +43,7 @@ export default ({ command, mode }: ConfigEnv): UserConfig =&gt; {
43 43 },
44 44  
45 45 build: {
46   - minify: 'esbuild',
  46 + // minify: 'esbuild',
47 47 outDir: OUTPUT_DIR,
48 48 polyfillDynamicImport: VITE_LEGACY,
49 49 terserOptions: {
... ...