Browse Source

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

Vben 4 years ago
parent
commit
e12c588c0a

+ 4 - 4
.eslintrc.js

@@ -38,15 +38,15 @@ module.exports = {
     '@typescript-eslint/no-unused-vars': [
       'error',
       {
-        argsIgnorePattern: '^h$',
-        varsIgnorePattern: '^h$',
+        argsIgnorePattern: '^_',
+        varsIgnorePattern: '^_',
       },
     ],
     'no-unused-vars': [
       'error',
       {
-        argsIgnorePattern: '^h$',
-        varsIgnorePattern: '^h$',
+        argsIgnorePattern: '^_',
+        varsIgnorePattern: '^_',
       },
     ],
     'space-before-function-paren': 'off',

+ 0 - 1
.vscode/settings.json

@@ -8,7 +8,6 @@
   "explorer.openEditors.visible": 0,
   "editor.tabSize": 2,
   "editor.renderControlCharacters": true,
-  "window.zoomLevel": -1,
   "editor.minimap.renderCharacters": false,
   "editor.minimap.maxColumn": 300,
   "editor.minimap.showSlider": "always",

+ 6 - 0
CHANGELOG.zh_CN.md

@@ -1,3 +1,9 @@
+## Wip
+
+### ✨ Refactor
+
+- 重构路由多层模式,解决嵌套 keepalive 执行多次问题
+
 ## 2.1.0 (2021-03-15)
 
 ### ✨ Features

+ 21 - 0
build/vite/plugin/hmr.ts

@@ -0,0 +1,21 @@
+import type { Plugin } from 'vite';
+
+/**
+ * TODO
+ * 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.
+ * @returns
+ */
+
+export function configHmrPlugin(): Plugin {
+  return {
+    name: 'singleHMR',
+    handleHotUpdate({ modules, file }) {
+      if (file.match(/xml$/)) return [];
+      modules.forEach((m) => {
+        m.importedModules = new Set();
+        m.importers = new Set();
+      });
+      return modules;
+    },
+  };
+}

+ 4 - 0
build/vite/plugin/index.ts

@@ -17,6 +17,7 @@ import { configThemePlugin } from './theme';
 import { configImageminPlugin } from './imagemin';
 import { configWindiCssPlugin } from './windicss';
 import { configSvgIconsPlugin } from './svgSprite';
+import { configHmrPlugin } from './hmr';
 
 export function createVitePlugins(viteEnv: ViteEnv, isBuild: boolean) {
   const { VITE_USE_IMAGEMIN, VITE_USE_MOCK, VITE_LEGACY, VITE_BUILD_COMPRESS } = viteEnv;
@@ -28,6 +29,9 @@ export function createVitePlugins(viteEnv: ViteEnv, isBuild: boolean) {
     vueJsx(),
   ];
 
+  // TODO
+  !isBuild && vitePlugins.push(configHmrPlugin());
+
   // @vitejs/plugin-legacy
   VITE_LEGACY && isBuild && vitePlugins.push(legacy());
 

+ 2 - 1
src/layouts/default/header/components/Breadcrumb.vue

@@ -33,6 +33,7 @@
   import { useGo } from '/@/hooks/web/usePage';
   import { isString } from '/@/utils/is';
   import { useI18n } from '/@/hooks/web/useI18n';
+  import { getMenus } from '/@/router/menus';
 
   export default defineComponent({
     name: 'LayoutBreadcrumb',
@@ -47,7 +48,7 @@
       const { getShowBreadCrumbIcon } = useRootSetting();
 
       const { t } = useI18n();
-      watchEffect(() => {
+      watchEffect(async () => {
         if (currentRoute.value.name === REDIRECT_NAME) return;
 
         const matched = currentRoute.value?.matched;

+ 0 - 63
src/layouts/page/ParentView.vue

@@ -1,63 +0,0 @@
-<!--
- * @Description: The reason is that tsx will report warnings under multi-level nesting.
--->
-<template>
-  <div>
-    <RouterView>
-      <template #default="{ Component, route }">
-        <transition
-          :name="
-            getTransitionName({
-              route,
-              openCache: openCache,
-              enableTransition: getEnableTransition,
-              cacheTabs: getCaches,
-              def: getBasicTransition,
-            })
-          "
-          mode="out-in"
-          appear
-        >
-          <keep-alive v-if="openCache" :include="getCaches">
-            <component :is="Component" v-bind="getKey(Component, route)" />
-          </keep-alive>
-          <component v-else :is="Component" v-bind="getKey(Component, route)" />
-        </transition>
-      </template>
-    </RouterView>
-  </div>
-</template>
-<script lang="ts">
-  import { computed, defineComponent, unref } from 'vue';
-
-  import { useRootSetting } from '/@/hooks/setting/useRootSetting';
-  import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting';
-
-  import { useTransitionSetting } from '/@/hooks/setting/useTransitionSetting';
-  import { useCache, getKey } from './useCache';
-  import { getTransitionName } from './transition';
-
-  export default defineComponent({
-    parentView: true,
-    setup() {
-      const { getCaches } = useCache(false);
-
-      const { getShowMultipleTab } = useMultipleTabSetting();
-
-      const { getOpenKeepAlive } = useRootSetting();
-
-      const { getBasicTransition, getEnableTransition } = useTransitionSetting();
-
-      const openCache = computed(() => unref(getOpenKeepAlive) && unref(getShowMultipleTab));
-
-      return {
-        getCaches,
-        getBasicTransition,
-        openCache,
-        getEnableTransition,
-        getTransitionName,
-        getKey,
-      };
-    },
-  });
-</script>

+ 15 - 5
src/layouts/page/index.vue

@@ -16,9 +16,9 @@
           appear
         >
           <keep-alive v-if="openCache" :include="getCaches">
-            <component :is="Component" v-bind="getKey(Component, route)" />
+            <component :is="Component" :key="route.fullPath" />
           </keep-alive>
-          <component v-else :is="Component" v-bind="getKey(Component, route)" />
+          <component v-else :is="Component" :key="route.fullPath" />
         </transition>
       </template>
     </RouterView>
@@ -34,15 +34,15 @@
   import { useRootSetting } from '/@/hooks/setting/useRootSetting';
 
   import { useTransitionSetting } from '/@/hooks/setting/useTransitionSetting';
-  import { useCache, getKey } from './useCache';
   import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting';
   import { getTransitionName } from './transition';
 
+  import { useStore } from 'vuex';
+
   export default defineComponent({
     name: 'PageLayout',
     components: { FrameLayout },
     setup() {
-      const { getCaches } = useCache(true);
       const { getShowMultipleTab } = useMultipleTabSetting();
 
       const { getOpenKeepAlive, getCanEmbedIFramePage } = useRootSetting();
@@ -51,6 +51,17 @@
 
       const openCache = computed(() => unref(getOpenKeepAlive) && unref(getShowMultipleTab));
 
+      const { getters } = useStore();
+
+      const getCaches = computed((): string[] => {
+        if (!unref(getOpenKeepAlive)) {
+          return [];
+        }
+        // TODO The useStore is used here mainly to solve the problem of circular dependency hot update
+        const cacheTabs = getters['app-tab/getCachedTabsState'];
+        return cacheTabs;
+      });
+
       return {
         getTransitionName,
         openCache,
@@ -58,7 +69,6 @@
         getBasicTransition,
         getCaches,
         getCanEmbedIFramePage,
-        getKey,
       };
     },
   });

+ 0 - 63
src/layouts/page/useCache.ts

@@ -1,63 +0,0 @@
-import type { FunctionalComponent } from 'vue';
-import type { RouteLocation } from 'vue-router';
-import { computed, ref, unref, getCurrentInstance } from 'vue';
-import { useRootSetting } from '/@/hooks/setting/useRootSetting';
-
-import { useRouter } from 'vue-router';
-import { useStore } from 'vuex';
-
-const ParentLayoutName = 'ParentLayout';
-
-const PAGE_LAYOUT_KEY = '__PAGE_LAYOUT__';
-
-export function getKey(component: FunctionalComponent & { type: Indexable }, route: RouteLocation) {
-  return !!component?.type.parentView ? {} : { key: route.fullPath };
-}
-
-export function useCache(isPage: boolean) {
-  const { getters } = useStore();
-
-  const name = ref('');
-  const { currentRoute } = useRouter();
-  const instance = getCurrentInstance();
-  const routeName = instance?.type.name;
-  if (routeName && ![ParentLayoutName].includes(routeName)) {
-    name.value = routeName;
-  } else {
-    const matched = currentRoute.value?.matched;
-    if (!matched) {
-      return;
-    }
-    const len = matched.length;
-    if (len < 2) return;
-    name.value = matched[len - 2].name as string;
-  }
-
-  const { getOpenKeepAlive } = useRootSetting();
-
-  const getCaches = computed((): string[] => {
-    if (!unref(getOpenKeepAlive)) {
-      return [];
-    }
-    const cached = getters['app-tab/getCachedMapState'];
-
-    if (isPage) {
-      //  page Layout
-      return cached.get(PAGE_LAYOUT_KEY) || [];
-    }
-    const cacheSet = new Set<string>();
-    cacheSet.add(unref(name));
-
-    const list = cached.get(unref(name));
-
-    if (!list) {
-      return Array.from(cacheSet);
-    }
-    list.forEach((item) => {
-      cacheSet.add(item);
-    });
-
-    return Array.from(cacheSet);
-  });
-  return { getCaches };
-}

+ 2 - 2
src/logics/mitt/tabChange.ts

@@ -4,7 +4,7 @@
 
 import Mitt from '/@/utils/mitt';
 import type { RouteLocationNormalized } from 'vue-router';
-import { getRoute } from '/@/router/helper/routeHelper';
+import { getRawRoute } from '/@/utils';
 
 const mitt = new Mitt();
 
@@ -13,7 +13,7 @@ const key = Symbol();
 let lastChangeTab: RouteLocationNormalized;
 
 export function setLastChangeTab(lastChangeRoute: RouteLocationNormalized) {
-  const r = getRoute(lastChangeRoute);
+  const r = getRawRoute(lastChangeRoute);
   mitt.emit(key, r);
   lastChangeTab = r;
 }

+ 14 - 71
src/router/constant.ts

@@ -1,9 +1,7 @@
-import type { AppRouteRecordRaw } from '/@/router/types';
-import ParentLayout from '/@/layouts/page/ParentView.vue';
-import { t } from '/@/hooks/web/useI18n';
-
 export const REDIRECT_NAME = 'Redirect';
 
+export const PARENT_LAYOUT_NAME = 'ParentLayout';
+
 export const EXCEPTION_COMPONENT = () => import('../views/sys/exception/Exception.vue');
 
 /**
@@ -12,78 +10,23 @@ export const EXCEPTION_COMPONENT = () => import('../views/sys/exception/Exceptio
 export const LAYOUT = () => import('/@/layouts/default/index.vue');
 
 /**
- * @description: page-layout
+ * @description: parent-layout
  */
-export const getParentLayout = (name: string) => {
+export const getParentLayout = (_name?: string) => {
   return () =>
     new Promise((resolve) => {
       resolve({
-        ...ParentLayout,
-        name,
+        name: PARENT_LAYOUT_NAME,
       });
     });
 };
 
-// 404 on a page
-export const PAGE_NOT_FOUND_ROUTE: AppRouteRecordRaw = {
-  path: '/:path(.*)*',
-  name: 'ErrorPage',
-  component: LAYOUT,
-  meta: {
-    title: 'ErrorPage',
-    hideBreadcrumb: true,
-  },
-  children: [
-    {
-      path: '/:path(.*)*',
-      name: 'ErrorPage',
-      component: EXCEPTION_COMPONENT,
-      meta: {
-        title: 'ErrorPage',
-        hideBreadcrumb: true,
-      },
-    },
-  ],
-};
-
-export const REDIRECT_ROUTE: AppRouteRecordRaw = {
-  path: '/redirect',
-  name: REDIRECT_NAME,
-  component: LAYOUT,
-  meta: {
-    title: REDIRECT_NAME,
-    hideBreadcrumb: true,
-  },
-  children: [
-    {
-      path: '/redirect/:path(.*)',
-      name: REDIRECT_NAME,
-      component: () => import('/@/views/sys/redirect/index.vue'),
-      meta: {
-        title: REDIRECT_NAME,
-        hideBreadcrumb: true,
-      },
-    },
-  ],
-};
-
-export const ERROR_LOG_ROUTE: AppRouteRecordRaw = {
-  path: '/error-log',
-  name: 'errorLog',
-  component: LAYOUT,
-  meta: {
-    title: 'ErrorLog',
-    hideBreadcrumb: true,
-  },
-  children: [
-    {
-      path: 'list',
-      name: 'errorLogList',
-      component: () => import('/@/views/sys/error-log/index.vue'),
-      meta: {
-        title: t('routes.basic.errorLogList'),
-        hideBreadcrumb: true,
-      },
-    },
-  ],
-};
+// export const getParentLayout = (name: string) => {
+//   return () =>
+//     new Promise((resolve) => {
+//       resolve({
+//         ...ParentLayout,
+//         name,
+//       });
+//     });
+// };

+ 1 - 1
src/router/guard/permissionGuard.ts

@@ -5,7 +5,7 @@ import { permissionStore } from '/@/store/modules/permission';
 import { PageEnum } from '/@/enums/pageEnum';
 import { userStore } from '/@/store/modules/user';
 
-import { PAGE_NOT_FOUND_ROUTE } from '/@/router/constant';
+import { PAGE_NOT_FOUND_ROUTE } from '/@/router/routes/basic';
 
 const LOGIN_PATH = PageEnum.BASE_LOGIN;
 

+ 18 - 29
src/router/helper/menuHelper.ts

@@ -1,44 +1,34 @@
 import { AppRouteModule } from '/@/router/types';
 import type { MenuModule, Menu, AppRouteRecordRaw } from '/@/router/types';
 
-import { findPath, forEach, treeMap } from '/@/utils/helper/treeHelper';
+import { findPath, treeMap } from '/@/utils/helper/treeHelper';
 import { cloneDeep } from 'lodash-es';
 import { isUrl } from '/@/utils/is';
 
-export function getAllParentPath(treeData: any[], path: string) {
+export function getAllParentPath<T = Recordable>(treeData: T[], path: string) {
   const menuList = findPath(treeData, (n) => n.path === path) as Menu[];
   return (menuList || []).map((item) => item.path);
 }
 
-// 拼接父级路径
-function joinParentPath(list: any, node: any) {
-  let allPaths = getAllParentPath(list, node.path);
-
-  allPaths = allPaths.slice(0, allPaths.length - 1);
-  let parentPath = '';
-  if (Array.isArray(allPaths) && allPaths.length >= 2) {
-    parentPath = allPaths[allPaths.length - 1];
-  } else {
-    allPaths.forEach((p) => {
-      parentPath += /^\//.test(p) ? p : `/${p}`;
-    });
+function joinParentPath(menus: Menu[], parentPath = '') {
+  for (let index = 0; index < menus.length; index++) {
+    const menu = menus[index];
+    const p = menu.path.startsWith('/') ? menu.path : `/${menu.path}`;
+    const parent = isUrl(menu.path) ? menu.path : `${parentPath}${p}`;
+    menus[index].path = parent;
+    if (menu?.children?.length) {
+      joinParentPath(menu.children, parent);
+    }
   }
-  node.path = `${/^\//.test(node.path) ? node.path : `${parentPath}/${node.path}`}`.replace(
-    /\/\//g,
-    '/'
-  );
-  return node;
 }
 
-// 解析菜单模块
+// Parsing the menu module
 export function transformMenuModule(menuModule: MenuModule): Menu {
   const { menu } = menuModule;
 
   const menuList = [menu];
-  forEach(menuList, (m) => {
-    !isUrl(m.path) && joinParentPath(menuList, m);
-  });
 
+  joinParentPath(menuList);
   return menuList[0];
 }
 
@@ -54,17 +44,16 @@ export function transformRouteToMenu(routeModList: AppRouteModule[]) {
       routeList.push(item);
     }
   });
-  return treeMap(routeList, {
+  const list = treeMap(routeList, {
     conversion: (node: AppRouteRecordRaw) => {
-      const { meta: { title, icon, hideMenu = false } = {} } = node;
-
-      !isUrl(node.path) && joinParentPath(routeList, node);
+      const { meta: { title, hideMenu = false } = {} } = node;
       return {
+        ...(node.meta || {}),
         name: title,
-        icon,
-        path: node.path,
         hideMenu,
       };
     },
   });
+  joinParentPath(list);
+  return list;
 }

+ 71 - 29
src/router/helper/routeHelper.ts

@@ -1,22 +1,18 @@
 import type { AppRouteModule, AppRouteRecordRaw } from '/@/router/types';
-import type { RouteLocationNormalized, RouteRecordNormalized } from 'vue-router';
+import type { Router, RouteRecordNormalized } from 'vue-router';
 
 import { getParentLayout, LAYOUT } from '/@/router/constant';
 import { cloneDeep } from 'lodash-es';
 import { warn } from '/@/utils/log';
+import { createRouter, createWebHashHistory } from 'vue-router';
 
 export type LayoutMapKey = 'LAYOUT';
 
 const LayoutMap = new Map<LayoutMapKey, () => Promise<typeof import('*.vue')>>();
 
-let dynamicViewsModules: Record<
-  string,
-  () => Promise<{
-    [key: string]: any;
-  }>
->;
+let dynamicViewsModules: Record<string, () => Promise<Recordable>>;
 
-// 动态引入
+// Dynamic introduction
 function asyncImportRoute(routes: AppRouteRecordRaw[] | undefined) {
   dynamicViewsModules = dynamicViewsModules || import.meta.glob('../../views/**/*.{vue,tsx}');
   if (!routes) return;
@@ -26,19 +22,14 @@ function asyncImportRoute(routes: AppRouteRecordRaw[] | undefined) {
     if (component) {
       item.component = dynamicImport(dynamicViewsModules, component as string);
     } else if (name) {
-      item.component = getParentLayout(name);
+      item.component = getParentLayout();
     }
     children && asyncImportRoute(children);
   });
 }
 
 function dynamicImport(
-  dynamicViewsModules: Record<
-    string,
-    () => Promise<{
-      [key: string]: any;
-    }>
-  >,
+  dynamicViewsModules: Record<string, () => Promise<Recordable>>,
   component: string
 ) {
   const keys = Object.keys(dynamicViewsModules);
@@ -84,18 +75,69 @@ export function transformObjToRoute<T = AppRouteModule>(routeList: AppRouteModul
   return (routeList as unknown) as T[];
 }
 
-// Return to the new routing structure, not affected by the original example
-export function getRoute(route: RouteLocationNormalized): RouteLocationNormalized {
-  if (!route) return route;
-  const { matched, ...opt } = route;
-  return {
-    ...opt,
-    matched: (matched
-      ? matched.map((item) => ({
-          meta: item.meta,
-          name: item.name,
-          path: item.path,
-        }))
-      : undefined) as RouteRecordNormalized[],
-  };
+/**
+ * Convert multi-level routing to level 2 routing
+ */
+export function flatRoutes(routeModules: AppRouteModule[]) {
+  for (let index = 0; index < routeModules.length; index++) {
+    const routeModule = routeModules[index];
+    if (!isMultipleRoute(routeModule)) {
+      continue;
+    }
+    promoteRouteLevel(routeModule);
+  }
+}
+
+// Routing level upgrade
+function promoteRouteLevel(routeModule: AppRouteModule) {
+  // Use vue-router to splice menus
+  let router: Router | null = createRouter({
+    routes: [routeModule as any],
+    history: createWebHashHistory(),
+  });
+
+  const routes = router.getRoutes();
+  const children = cloneDeep(routeModule.children);
+  addToChildren(routes, children || [], routeModule);
+  router = null;
+
+  routeModule.children = routeModule.children?.filter((item) => !item.children?.length);
+}
+
+// Add all sub-routes to the secondary route
+function addToChildren(
+  routes: RouteRecordNormalized[],
+  children: AppRouteRecordRaw[],
+  routeModule: AppRouteModule
+) {
+  for (let index = 0; index < children.length; index++) {
+    const child = children[index];
+    const route = routes.find((item) => item.name === child.name);
+    if (route) {
+      routeModule.children = routeModule.children || [];
+      routeModule.children?.push(route as any);
+      if (child.children?.length) {
+        addToChildren(routes, child.children, routeModule);
+      }
+    }
+  }
+}
+
+// Determine whether the level exceeds 2 levels
+function isMultipleRoute(routeModule: AppRouteModule) {
+  if (!routeModule || !Reflect.has(routeModule, 'children') || !routeModule.children?.length) {
+    return false;
+  }
+
+  const children = routeModule.children;
+
+  let flag = false;
+  for (let index = 0; index < children.length; index++) {
+    const child = children[index];
+    if (child.children?.length) {
+      flag = true;
+      break;
+    }
+  }
+  return flag;
 }

+ 4 - 12
src/router/menus/index.ts

@@ -5,6 +5,7 @@ import { appStore } from '/@/store/modules/app';
 import { permissionStore } from '/@/store/modules/permission';
 import { transformMenuModule, getAllParentPath } from '/@/router/helper/menuHelper';
 import { filter } from '/@/utils/helper/treeHelper';
+import { isUrl } from '/@/utils/is';
 import router from '/@/router';
 import { PermissionModeEnum } from '/@/enums/appEnum';
 import { pathToRegexp } from 'path-to-regexp';
@@ -19,8 +20,6 @@ Object.keys(modules).forEach((key) => {
   menuModules.push(...modList);
 });
 
-const reg = /(((https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/;
-
 // ===========================
 // ==========Helper===========
 // ===========================
@@ -40,18 +39,15 @@ const staticMenus: Menu[] = [];
 })();
 
 async function getAsyncMenus() {
-  // 前端角色控制菜单 直接取菜单文件
   return !isBackMode() ? staticMenus : permissionStore.getBackMenuListState;
 }
 
-// 获取菜单 树级
 export const getMenus = async (): Promise<Menu[]> => {
   const menus = await getAsyncMenus();
   const routes = router.getRoutes();
   return !isBackMode() ? filter(menus, basicFilter(routes)) : menus;
 };
 
-// 获取当前路径的顶级路径
 export async function getCurrentParentPath(currentPath: string) {
   const menus = await getAsyncMenus();
 
@@ -60,7 +56,7 @@ export async function getCurrentParentPath(currentPath: string) {
   return allParentPath?.[0];
 }
 
-// 获取1级菜单,删除children
+// Get the level 1 menu, delete children
 export async function getShallowMenus(): Promise<Menu[]> {
   const menus = await getAsyncMenus();
   const routes = router.getRoutes();
@@ -68,7 +64,7 @@ export async function getShallowMenus(): Promise<Menu[]> {
   return !isBackMode() ? shallowMenuList.filter(basicFilter(routes)) : shallowMenuList;
 }
 
-// 获取菜单的children
+// Get the children of the menu
 export async function getChildrenMenus(parentPath: string) {
   const menus = await getAsyncMenus();
   const parent = menus.find((item) => item.path === parentPath);
@@ -78,14 +74,10 @@ export async function getChildrenMenus(parentPath: string) {
   return !isBackMode() ? filter(parent.children, basicFilter(routes)) : parent.children;
 }
 
-// 通用过滤方法
 function basicFilter(routes: RouteRecordNormalized[]) {
   return (menu: Menu) => {
     const matchRoute = routes.find((route) => {
-      const match = route.path.match(reg)?.[0];
-      if (match && match === menu.path) {
-        return true;
-      }
+      if (isUrl(menu.path)) return true;
 
       if (route.meta?.carryParam) {
         return pathToRegexp(route.path).test(menu.path);

+ 67 - 0
src/router/routes/basic.ts

@@ -0,0 +1,67 @@
+import type { AppRouteRecordRaw } from '/@/router/types';
+import { t } from '/@/hooks/web/useI18n';
+import { REDIRECT_NAME, LAYOUT, EXCEPTION_COMPONENT } from '/@/router/constant';
+
+// 404 on a page
+export const PAGE_NOT_FOUND_ROUTE: AppRouteRecordRaw = {
+  path: '/:path(.*)*',
+  name: 'ErrorPage',
+  component: LAYOUT,
+  meta: {
+    title: 'ErrorPage',
+    hideBreadcrumb: true,
+  },
+  children: [
+    {
+      path: '/:path(.*)*',
+      name: 'ErrorPage',
+      component: EXCEPTION_COMPONENT,
+      meta: {
+        title: 'ErrorPage',
+        hideBreadcrumb: true,
+      },
+    },
+  ],
+};
+
+export const REDIRECT_ROUTE: AppRouteRecordRaw = {
+  path: '/redirect',
+  name: REDIRECT_NAME,
+  component: LAYOUT,
+  meta: {
+    title: REDIRECT_NAME,
+    hideBreadcrumb: true,
+  },
+  children: [
+    {
+      path: '/redirect/:path(.*)',
+      name: REDIRECT_NAME,
+      component: () => import('/@/views/sys/redirect/index.vue'),
+      meta: {
+        title: REDIRECT_NAME,
+        hideBreadcrumb: true,
+      },
+    },
+  ],
+};
+
+export const ERROR_LOG_ROUTE: AppRouteRecordRaw = {
+  path: '/error-log',
+  name: 'errorLog',
+  component: LAYOUT,
+  meta: {
+    title: 'ErrorLog',
+    hideBreadcrumb: true,
+  },
+  children: [
+    {
+      path: 'list',
+      name: 'errorLogList',
+      component: () => import('/@/views/sys/error-log/index.vue'),
+      meta: {
+        title: t('routes.basic.errorLogList'),
+        hideBreadcrumb: true,
+      },
+    },
+  ],
+};

+ 5 - 1
src/router/routes/index.ts

@@ -1,10 +1,11 @@
 import type { AppRouteRecordRaw, AppRouteModule } from '/@/router/types';
 
-import { PAGE_NOT_FOUND_ROUTE, REDIRECT_ROUTE } from '../constant';
+import { PAGE_NOT_FOUND_ROUTE, REDIRECT_ROUTE } from '/@/router/routes/basic';
 
 import { mainOutRoutes } from './mainOut';
 import { PageEnum } from '/@/enums/pageEnum';
 import { t } from '/@/hooks/web/useI18n';
+import { flatRoutes } from '/@/router/helper/routeHelper';
 
 const modules = import.meta.globEager('./modules/**/*.ts');
 
@@ -16,6 +17,9 @@ Object.keys(modules).forEach((key) => {
   routeModuleList.push(...modList);
 });
 
+// Multi-level routing conversion
+flatRoutes(routeModuleList);
+
 export const asyncRoutes = [PAGE_NOT_FOUND_ROUTE, ...routeModuleList];
 
 export const RootRoute: AppRouteRecordRaw = {

+ 4 - 3
src/store/modules/permission.ts

@@ -14,12 +14,12 @@ import { filter } from '/@/utils/helper/treeHelper';
 import { toRaw } from 'vue';
 import { getMenuListById } from '/@/api/sys/menu';
 
-import { transformObjToRoute } from '/@/router/helper/routeHelper';
+import { transformObjToRoute, flatRoutes } from '/@/router/helper/routeHelper';
 import { transformRouteToMenu } from '/@/router/helper/menuHelper';
 
 import { useMessage } from '/@/hooks/web/useMessage';
 import { useI18n } from '/@/hooks/web/useI18n';
-import { ERROR_LOG_ROUTE, PAGE_NOT_FOUND_ROUTE } from '/@/router/constant';
+import { ERROR_LOG_ROUTE, PAGE_NOT_FOUND_ROUTE } from '/@/router/routes/basic';
 
 const { createMessage } = useMessage();
 const NAME = 'app-permission';
@@ -113,11 +113,12 @@ class Permission extends VuexModule {
 
       // Dynamically introduce components
       routeList = transformObjToRoute(routeList);
+
       //  Background routing to menu structure
       const backMenuList = transformRouteToMenu(routeList);
-
       this.commitBackMenuListState(backMenuList);
 
+      flatRoutes(routeList);
       routes = [PAGE_NOT_FOUND_ROUTE, ...routeList];
     }
     routes.push(ERROR_LOG_ROUTE);

+ 19 - 56
src/store/modules/tab.ts

@@ -8,8 +8,8 @@ import { PageEnum } from '/@/enums/pageEnum';
 
 import store from '/@/store';
 import router from '/@/router';
-import { PAGE_NOT_FOUND_ROUTE, REDIRECT_ROUTE } from '/@/router/constant';
-import { getRoute } from '/@/router/helper/routeHelper';
+import { PAGE_NOT_FOUND_ROUTE, REDIRECT_ROUTE } from '/@/router/routes/basic';
+import { getRawRoute } from '/@/utils';
 
 import { useGo, useRedo } from '/@/hooks/web/usePage';
 import { cloneDeep } from 'lodash-es';
@@ -18,8 +18,6 @@ const NAME = 'app-tab';
 
 hotModuleUnregisterModule(NAME);
 
-export const PAGE_LAYOUT_KEY = '__PAGE_LAYOUT__';
-
 function isGotoPage() {
   const go = useGo();
   go(unref(router.currentRoute).path, true);
@@ -27,7 +25,7 @@ function isGotoPage() {
 
 @Module({ namespaced: true, name: NAME, dynamic: true, store })
 class Tab extends VuexModule {
-  cachedMapState = new Map<string, string[]>();
+  cachedTabsState: Set<string> = new Set();
 
   // tab list
   tabsState: RouteLocationNormalized[] = [];
@@ -43,8 +41,8 @@ class Tab extends VuexModule {
     return this.tabsState.find((item) => item.path === route.path)!;
   }
 
-  get getCachedMapState(): Map<string, string[]> {
-    return this.cachedMapState;
+  get getCachedTabsState(): string[] {
+    return Array.from(this.cachedTabsState);
   }
 
   get getLastDragEndIndexState(): number {
@@ -53,7 +51,7 @@ class Tab extends VuexModule {
 
   @Mutation
   commitClearCache(): void {
-    this.cachedMapState = new Map();
+    this.cachedTabsState = new Set();
   }
 
   @Mutation
@@ -77,46 +75,16 @@ class Tab extends VuexModule {
 
   @Mutation
   commitCachedMapState(): void {
-    const cacheMap = new Map<string, string[]>();
+    const cacheMap: Set<string> = new Set();
 
-    const pageCacheSet = new Set<string>();
     this.tabsState.forEach((tab) => {
-      const item = getRoute(tab);
+      const item = getRawRoute(tab);
       const needCache = !item.meta?.ignoreKeepAlive;
       if (!needCache) return;
-
-      if (item.meta?.affix) {
-        const name = item.name as string;
-        pageCacheSet.add(name);
-      } else if (item?.matched && needCache) {
-        const matched = item?.matched;
-        if (!matched) return;
-        const len = matched.length;
-
-        if (len < 2) return;
-
-        for (let i = 0; i < matched.length; i++) {
-          const key = matched[i].name as string;
-
-          if (i < 2) {
-            pageCacheSet.add(key);
-          }
-          if (i < len - 1) {
-            const { meta, name } = matched[i + 1];
-            if (meta && (meta.affix || needCache)) {
-              const mapList = cacheMap.get(key) || [];
-              if (!mapList.includes(name as string)) {
-                mapList.push(name as string);
-              }
-              cacheMap.set(key, mapList);
-            }
-          }
-        }
-      }
+      const name = item.name as string;
+      cacheMap.add(name);
     });
-
-    cacheMap.set(PAGE_LAYOUT_KEY, Array.from(pageCacheSet));
-    this.cachedMapState = cacheMap;
+    this.cachedTabsState = cacheMap;
   }
 
   @Mutation
@@ -162,7 +130,7 @@ class Tab extends VuexModule {
   @Mutation
   commitResetState(): void {
     this.tabsState = [];
-    this.cachedMapState = new Map();
+    this.cachedTabsState = new Set();
   }
 
   @Mutation
@@ -190,7 +158,7 @@ class Tab extends VuexModule {
     ) {
       return;
     }
-    this.commitTabRoutesState(getRoute(route));
+    this.commitTabRoutesState(getRawRoute(route));
 
     this.commitCachedMapState();
   }
@@ -198,17 +166,12 @@ class Tab extends VuexModule {
   @Mutation
   async commitRedoPage() {
     const route = router.currentRoute.value;
-    for (const [key, value] of this.cachedMapState) {
-      const index = value.findIndex((item) => item === (route.name as string));
-      if (index === -1) {
-        continue;
-      }
-      if (value.length === 1) {
-        this.cachedMapState.delete(key);
-        continue;
-      }
-      value.splice(index, 1);
-      this.cachedMapState.set(key, value);
+    const name = route.name;
+
+    const findVal = Array.from(this.cachedTabsState).find((item) => item === name);
+    if (findVal) {
+      this.cachedTabsState.delete(findVal);
+      // this.cachedTabsState.splice(index, 1);
     }
     const redo = useRedo();
     await redo();

+ 17 - 3
src/utils/index.ts

@@ -1,9 +1,8 @@
-export const timestamp = () => +Date.now();
+import type { RouteLocationNormalized, RouteRecordNormalized } from 'vue-router';
 import { unref } from 'vue';
 import { isObject } from '/@/utils/is';
-export const clamp = (n: number, min: number, max: number) => Math.min(max, Math.max(min, n));
+
 export const noop = () => {};
-export const now = () => Date.now();
 
 /**
  * @description:  Set ui mount node
@@ -91,3 +90,18 @@ export function setTitle(title: string, appTitle?: string) {
     setDocumentTitle(_title);
   }
 }
+
+export function getRawRoute(route: RouteLocationNormalized): RouteLocationNormalized {
+  if (!route) return route;
+  const { matched, ...opt } = route;
+  return {
+    ...opt,
+    matched: (matched
+      ? matched.map((item) => ({
+          meta: item.meta,
+          name: item.name,
+          path: item.path,
+        }))
+      : undefined) as RouteRecordNormalized[],
+  };
+}

+ 1 - 1
vite.config.ts

@@ -43,7 +43,7 @@ export default ({ command, mode }: ConfigEnv): UserConfig => {
     },
 
     build: {
-      minify: 'esbuild',
+      // minify: 'esbuild',
       outDir: OUTPUT_DIR,
       polyfillDynamicImport: VITE_LEGACY,
       terserOptions: {