瀏覽代碼

refactor: refactor route

vben 4 年之前
父節點
當前提交
c303ec1a23
共有 84 個文件被更改,包括 1567 次插入1524 次删除
  1. 22 0
      .github/workflows/deploy.yml
  2. 5 40
      .vscode/launch.json
  3. 4 7
      .vscode/settings.json
  4. 10 0
      CHANGELOG.zh_CN.md
  5. 2 2
      build/vite/plugin/transform/dynamic-import/index.ts
  6. 81 34
      mock/sys/menu.ts
  7. 6 6
      package.json
  8. 0 6
      src/components/Breadcrumb/index.ts
  9. 0 96
      src/components/Breadcrumb/src/Breadcrumb.vue
  10. 0 57
      src/components/Breadcrumb/src/BreadcrumbItem.vue
  11. 2 1
      src/components/Menu/src/BasicMenu.tsx
  12. 1 1
      src/components/Menu/src/hooks/useOpenKeys.ts
  13. 1 1
      src/components/Menu/src/hooks/useSearchInput.ts
  14. 6 4
      src/components/Table/src/components/TableSetting.vue
  15. 2 0
      src/components/registerGlobComp.ts
  16. 0 18
      src/design/transition/breadcrumb.less
  17. 0 1
      src/design/transition/index.less
  18. 1 1
      src/enums/pageEnum.ts
  19. 4 3
      src/hooks/web/usePage.ts
  20. 7 10
      src/hooks/web/usePermission.ts
  21. 8 59
      src/hooks/web/useTabs.ts
  22. 2 4
      src/layouts/default/content/index.tsx
  23. 0 128
      src/layouts/default/header/LayoutBreadcrumb.tsx
  24. 79 0
      src/layouts/default/header/LayoutBreadcrumb.vue
  25. 1 1
      src/layouts/default/header/LayoutHeader.tsx
  26. 1 0
      src/layouts/default/header/LayoutMultipleHeader.less
  27. 45 11
      src/layouts/default/header/index.less
  28. 27 45
      src/layouts/default/multitabs/TabContent.tsx
  29. 0 90
      src/layouts/default/multitabs/data.ts
  30. 13 60
      src/layouts/default/multitabs/index.tsx
  31. 35 0
      src/layouts/default/multitabs/types.ts
  32. 55 10
      src/layouts/default/multitabs/useMultipleTabs.ts
  33. 108 174
      src/layouts/default/multitabs/useTabDropdown.ts
  34. 1 1
      src/layouts/iframe/index.vue
  35. 7 4
      src/layouts/iframe/useFrameKeepAlive.ts
  36. 0 79
      src/layouts/page/index.tsx
  37. 21 0
      src/layouts/page/index.vue
  38. 73 0
      src/layouts/parent/index.vue
  39. 52 0
      src/layouts/parent/useCache.ts
  40. 0 0
      src/layouts/parent/useTransition.ts
  41. 2 2
      src/locales/lang/en/component/form.ts
  42. 2 0
      src/locales/lang/en/layout/header.ts
  43. 3 0
      src/locales/lang/en/routes/demo/level.ts
  44. 2 0
      src/locales/lang/zh_CN/layout/header.ts
  45. 1 1
      src/locales/lang/zh_CN/routes/demo/feat.ts
  46. 3 0
      src/locales/lang/zh_CN/routes/demo/level.ts
  47. 33 6
      src/router/constant.ts
  48. 6 13
      src/router/guard/index.ts
  49. 2 1
      src/router/guard/pageLoadingGuard.ts
  50. 3 2
      src/router/guard/permissionGuard.ts
  51. 0 3
      src/router/guard/progressGuard.ts
  52. 5 0
      src/router/helper/dynamicImport.ts
  53. 6 7
      src/router/helper/menuHelper.ts
  54. 89 0
      src/router/helper/routeHelper.ts
  55. 1 1
      src/router/menus/index.ts
  56. 16 29
      src/router/menus/modules/dashboard.ts
  57. 39 0
      src/router/menus/modules/demo/level.ts
  58. 10 0
      src/router/menus/modules/home.ts
  59. 13 16
      src/router/routes/index.ts
  60. 11 24
      src/router/routes/modules/dashboard.ts
  61. 12 14
      src/router/routes/modules/demo/charts.ts
  62. 26 25
      src/router/routes/modules/demo/comp.ts
  63. 12 15
      src/router/routes/modules/demo/editor.ts
  64. 13 15
      src/router/routes/modules/demo/excel.ts
  65. 21 24
      src/router/routes/modules/demo/feat.ts
  66. 16 19
      src/router/routes/modules/demo/form.ts
  67. 12 14
      src/router/routes/modules/demo/iframe.ts
  68. 63 0
      src/router/routes/modules/demo/level.ts
  69. 15 8
      src/router/routes/modules/demo/page.ts
  70. 13 13
      src/router/routes/modules/demo/permission.ts
  71. 24 26
      src/router/routes/modules/demo/table.ts
  72. 12 14
      src/router/routes/modules/demo/tree.ts
  73. 28 0
      src/router/routes/modules/home.ts
  74. 14 13
      src/router/types.d.ts
  75. 8 12
      src/store/modules/permission.ts
  76. 193 114
      src/store/modules/tab.ts
  77. 0 5
      src/utils/helper/dynamicImport.ts
  78. 0 110
      src/utils/helper/routeHelper.ts
  79. 1 0
      src/views/demo/feat/copy/index.vue
  80. 11 0
      src/views/demo/level/Menu111.vue
  81. 11 0
      src/views/demo/level/Menu12.vue
  82. 13 0
      src/views/demo/level/Menu2.vue
  83. 5 1
      src/views/sys/redirect/index.vue
  84. 115 23
      yarn.lock

+ 22 - 0
.github/workflows/deploy.yml

@@ -0,0 +1,22 @@
+name: deploy
+
+on:
+  push:
+    branches:
+      - main
+
+jobs:
+  build-deploy:
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v1
+      - run: npm install
+      - run: npm run build
+
+      - name: Deploy
+        uses: peaceiris/actions-gh-pages@v2.5.0
+        env:
+          ACTIONS_DEPLOY_KEY: ${{secrets.ACTIONS_DEPLOY_KEY}}
+          PUBLISH_BRANCH: gh-pages
+          PUBLISH_DIR: dist

+ 5 - 40
.vscode/launch.json

@@ -1,48 +1,13 @@
 {
   "version": "0.2.0",
   "configurations": [
-    // node环境调试当前激活编辑器ts/js代码
     {
-      "type": "node",
+      "type": "chrome",
       "request": "launch",
-      "name": "file",
-      "cwd": "${workspaceFolder}",
-      "program": "${file}",
-      // .vscode 目录又不认识了???
-      "preLaunchTask": "tsc: 监视 - build/tsconfig.json", // cn
-      // "preLaunchTask": "tsc: watch - build/tsconfig.json", // en
-      "outFiles": ["${workspaceFolder}/compile/**/*.js"]
-      // "args": ["--experimental-modules", "--loader", "./loader.mjs"]
+      "name": "Launch Chrome",
+      "url": "http://localhost:3100",
+      "webRoot": "${workspaceFolder}/src",
+      "sourceMaps": true
     },
-    // 调试开发环境脚本
-    {
-      "type": "node",
-      "request": "launch",
-      "name": "dev",
-      // "stopOnEntry": true,
-      "cwd": "${workspaceFolder}",
-      "program": "${workspaceFolder}/node_modules/@vue/cli-service/bin/vue-cli-service.js",
-      "args": ["serve", "--open"]
-    },
-    // 调试生产环境脚本
-    {
-      "type": "node",
-      "request": "launch",
-      "name": "build",
-      // "stopOnEntry": true,
-      "cwd": "${workspaceFolder}",
-      "program": "${workspaceFolder}/node_modules/@vue/cli-service/bin/vue-cli-service.js",
-      "args": ["build"]
-    },
-    // 调试单元测试脚本
-    {
-      "type": "node",
-      "request": "launch",
-      "name": "test:unit",
-      // "stopOnEntry": true,
-      "cwd": "${workspaceFolder}",
-      "program": "${workspaceFolder}/node_modules/@vue/cli-service/bin/vue-cli-service.js",
-      "args": ["test:unit", "--detectOpenHandles"]
-    }
   ]
 }

+ 4 - 7
.vscode/settings.json

@@ -163,12 +163,6 @@
   "[typescriptreact]": {
     "editor.defaultFormatter": "esbenp.prettier-vscode"
   },
-  "[json]": {
-    "editor.defaultFormatter": "vscode.json-language-features"
-  },
-  "[jsonc]": {
-    "editor.defaultFormatter": "vscode.json-language-features"
-  },
   "[html]": {
     "editor.defaultFormatter": "esbenp.prettier-vscode"
   },
@@ -198,5 +192,8 @@
     "ts"
   ],
   "i18n-ally.sourceLanguage": "zh",
-  "i18n-ally.enabledFrameworks":["vue","react"]
+  "i18n-ally.enabledFrameworks": [
+    "vue",
+    "react"
+  ]
 }

+ 10 - 0
CHANGELOG.zh_CN.md

@@ -1,14 +1,24 @@
 ## Wip
 
+## (破坏性更新) Breaking changes
+
+- 路由重构, 不再支持以前的格式。改为支持 vue-router 最初的默认结构,具体格式可以参考示例更改。实现多级路由缓存,不再将路由转化为 2 级。
+- 重构面包屑,使用 antd 的面包屑组件。之前的组件已删除
+
 ### ✨ Features
 
 - 还原 antdv 默认 loading,重构 `Loading` 组件,增加`useLoading`和`v-loading`指令。并增加示例
 - i18n 支持 vscode `i18n-ally`插件
+- 新增多级路由缓存示例
 
 ### 🎫 Chores
 
 - 首屏 loading 修改
 
+### 🐛 Bug Fixes
+
+-修复表格 i18n 错误
+
 ## 2.0.0-rc.12 (2020-11-30)
 
 ## (破坏性更新) Breaking changes

+ 2 - 2
build/vite/plugin/transform/dynamic-import/index.ts

@@ -17,8 +17,8 @@ const dynamicImportTransform = function (enableDynamicImport: boolean): Transfor
     test({ path }) {
       // Only convert the file
       return (
-        path.includes('/src/utils/helper/dynamicImport.ts') ||
-        path.includes(`\\src\\utils\\helper\\dynamicImport.ts`)
+        path.includes('/src/router/helper/dynamicImport.ts') ||
+        path.includes(`\\src\\router\\helper\\dynamicImport.ts`)
       );
     },
     transform({ code }) {

+ 81 - 34
mock/sys/menu.ts

@@ -1,33 +1,23 @@
 import { resultSuccess } from '../_util';
 import { MockMethod } from 'vite-plugin-mock';
 
+// single
 const dashboardRoute = {
-  path: '/dashboard',
-  name: 'Dashboard',
-  component: 'PAGE_LAYOUT',
-  redirect: '/dashboard/welcome',
+  path: '/home',
+  name: 'Home',
+  component: '/dashboard/welcome/index',
   meta: {
+    title: 'routes.dashboard.welcome',
+    affix: true,
     icon: 'ant-design:home-outlined',
-    title: 'Dashboard',
   },
-  children: [
-    {
-      path: '/welcome',
-      name: 'Welcome',
-      component: '/dashboard/welcome/index',
-      meta: {
-        title: '欢迎页',
-        affix: true,
-      },
-    },
-  ],
 };
 
 const frontRoute = {
-  path: '/front',
+  path: 'front',
   name: 'PermissionFrontDemo',
   meta: {
-    title: '基于前端权限',
+    title: 'routes.demo.permission.front',
   },
   children: [
     {
@@ -35,7 +25,7 @@ const frontRoute = {
       name: 'FrontPageAuth',
       component: '/demo/permission/front/index',
       meta: {
-        title: '页面权限',
+        title: 'routes.demo.permission.frontPage',
       },
     },
     {
@@ -43,7 +33,7 @@ const frontRoute = {
       name: 'FrontBtnAuth',
       component: '/demo/permission/front/Btn',
       meta: {
-        title: '按钮权限',
+        title: 'routes.demo.permission.frontBtn',
       },
     },
     {
@@ -51,7 +41,7 @@ const frontRoute = {
       name: 'FrontAuthPageA',
       component: '/demo/permission/front/AuthPageA',
       meta: {
-        title: '权限测试页A',
+        title: 'routes.demo.permission.frontTestA',
       },
     },
     {
@@ -59,24 +49,25 @@ const frontRoute = {
       name: 'FrontAuthPageB',
       component: '/demo/permission/front/AuthPageB',
       meta: {
-        title: '权限测试页B',
+        title: 'routes.demo.permission.frontTestB',
       },
     },
   ],
 };
 const backRoute = {
-  path: '/back',
+  path: 'back',
   name: 'PermissionBackDemo',
   meta: {
-    title: '基于后台权限',
+    title: 'routes.demo.permission.back',
   },
+
   children: [
     {
       path: 'page',
       name: 'BackAuthPage',
       component: '/demo/permission/back/index',
       meta: {
-        title: '页面权限',
+        title: 'routes.demo.permission.backPage',
       },
     },
     {
@@ -84,7 +75,7 @@ const backRoute = {
       name: 'BackAuthBtn',
       component: '/demo/permission/back/Btn',
       meta: {
-        title: '按钮权限',
+        title: 'routes.demo.permission.backBtn',
       },
     },
   ],
@@ -92,11 +83,11 @@ const backRoute = {
 const authRoute = {
   path: '/permission',
   name: 'Permission',
-  component: 'PAGE_LAYOUT',
+  component: 'LAYOUT',
   redirect: '/permission/front/page',
   meta: {
-    icon: 'ant-design:home-outlined',
-    title: '权限管理',
+    icon: 'carbon:user-role',
+    title: 'routes.demo.permission.permission',
   },
   children: [frontRoute, backRoute],
 };
@@ -104,14 +95,70 @@ const authRoute = {
 const authRoute1 = {
   path: '/permission',
   name: 'Permission',
-  component: 'PAGE_LAYOUT',
+  component: 'LAYOUT',
   redirect: '/permission/front/page',
   meta: {
-    icon: 'ant-design:home-outlined',
-    title: '权限管理',
+    icon: 'carbon:user-role',
+    title: 'routes.demo.permission.permission',
   },
   children: [backRoute],
 };
+
+const levelRoute = {
+  path: '/level',
+  name: 'Level',
+  component: 'LAYOUT',
+  redirect: '/level/menu1/menu1-1',
+  meta: {
+    icon: 'carbon:user-role',
+    title: 'routes.demo.level.level',
+  },
+
+  children: [
+    {
+      path: 'menu1',
+      name: 'Menu1Demo',
+      meta: {
+        title: 'Menu1',
+      },
+      children: [
+        {
+          path: 'menu1-1',
+          name: 'Menu11Demo',
+          meta: {
+            title: 'Menu1-1',
+          },
+          children: [
+            {
+              path: 'menu1-1-1',
+              name: 'Menu111Demo',
+              component: '/demo/level/Menu111',
+              meta: {
+                title: 'Menu111',
+              },
+            },
+          ],
+        },
+        {
+          path: 'menu1-2',
+          name: 'Menu12Demo',
+          component: '/demo/level/Menu12',
+          meta: {
+            title: 'Menu1-2',
+          },
+        },
+      ],
+    },
+    {
+      path: 'menu2',
+      name: 'Menu2Demo',
+      component: '/demo/level/Menu2',
+      meta: {
+        title: 'Menu2',
+      },
+    },
+  ],
+};
 export default [
   {
     url: '/api/getMenuListById',
@@ -120,10 +167,10 @@ export default [
     response: ({ query }) => {
       const { id } = query;
       if (!id || id === '1') {
-        return resultSuccess([dashboardRoute, authRoute]);
+        return resultSuccess([dashboardRoute, authRoute, levelRoute]);
       }
       if (id === '2') {
-        return resultSuccess([dashboardRoute, authRoute1]);
+        return resultSuccess([dashboardRoute, authRoute1, levelRoute]);
       }
     },
   },

+ 6 - 6
package.json

@@ -35,7 +35,7 @@
     "qrcode": "^1.4.4",
     "sortablejs": "^1.12.0",
     "vditor": "^3.7.0",
-    "vue": "^3.0.3",
+    "vue": "^3.0.4",
     "vue-i18n": "^9.0.0-beta.8",
     "vue-router": "^4.0.0-rc.6",
     "vue-types": "^3.0.1",
@@ -47,7 +47,7 @@
   "devDependencies": {
     "@commitlint/cli": "^11.0.0",
     "@commitlint/config-conventional": "^11.0.0",
-    "@iconify/json": "^1.1.266",
+    "@iconify/json": "^1.1.267",
     "@ls-lint/ls-lint": "^1.9.2",
     "@purge-icons/generated": "^0.4.1",
     "@types/echarts": "^4.9.2",
@@ -60,25 +60,25 @@
     "@types/qrcode": "^1.3.5",
     "@types/rollup-plugin-visualizer": "^2.6.0",
     "@types/sortablejs": "^1.10.6",
-    "@types/yargs": "^15.0.10",
+    "@types/yargs": "^15.0.11",
     "@types/zxcvbn": "^4.4.0",
     "@typescript-eslint/eslint-plugin": "^4.9.0",
     "@typescript-eslint/parser": "^4.9.0",
-    "@vue/compiler-sfc": "^3.0.3",
+    "@vue/compiler-sfc": "^3.0.4",
     "@vuedx/typecheck": "^0.2.4-0",
     "@vuedx/typescript-plugin-vue": "^0.2.4-0",
     "autoprefixer": "^9.8.6",
     "commitizen": "^4.2.2",
     "conventional-changelog-cli": "^2.1.1",
     "conventional-changelog-custom-config": "^0.3.1",
-    "cross-env": "^7.0.2",
+    "cross-env": "^7.0.3",
     "dot-prop": "^6.0.1",
     "dotenv": "^8.2.0",
     "eslint": "^7.14.0",
     "eslint-config-prettier": "^6.15.0",
     "eslint-plugin-prettier": "^3.1.4",
     "eslint-plugin-vue": "^7.1.0",
-    "esno": "^0.2.4",
+    "esno": "^0.3.0",
     "fs-extra": "^9.0.1",
     "globrex": "^0.1.2",
     "husky": "^4.3.0",

+ 0 - 6
src/components/Breadcrumb/index.ts

@@ -1,6 +0,0 @@
-import BreadcrumbLib from './src/Breadcrumb.vue';
-import BreadcrumbItemLib from './src/BreadcrumbItem.vue';
-import { withInstall } from '../util';
-
-export const Breadcrumb = withInstall(BreadcrumbLib);
-export const BreadcrumbItem = withInstall(BreadcrumbItemLib);

+ 0 - 96
src/components/Breadcrumb/src/Breadcrumb.vue

@@ -1,96 +0,0 @@
-<template>
-  <div ref="breadcrumbRef" class="breadcrumb">
-    <slot />
-  </div>
-</template>
-
-<script lang="ts">
-  import { defineComponent, provide, ref } from 'vue';
-  import { propTypes } from '/@/utils/propTypes';
-
-  export default defineComponent({
-    name: 'Breadcrumb',
-    props: {
-      separator: propTypes.string.def('/'),
-      separatorClass: propTypes.string,
-    },
-    setup(props) {
-      const breadcrumbRef = ref<Nullable<HTMLElement>>(null);
-
-      provide('breadcrumb', props);
-
-      return {
-        breadcrumbRef,
-      };
-    },
-  });
-</script>
-<style lang="less">
-  @import (reference) '../../../design/index.less';
-
-  .breadcrumb {
-    .unselect();
-
-    height: @header-height;
-    padding-right: 20px;
-    font-size: 13px;
-    line-height: @header-height;
-    // line-height: 1;
-
-    &::after,
-    &::before {
-      display: table;
-      content: '';
-    }
-
-    &::after {
-      clear: both;
-    }
-
-    &__separator {
-      margin: 0 9px;
-      font-weight: 700;
-      color: @breadcrumb-item-normal-color;
-
-      &[class*='icon'] {
-        margin: 0 6px;
-        font-weight: 400;
-      }
-    }
-
-    &__item {
-      float: left;
-    }
-
-    &__inner {
-      color: @breadcrumb-item-normal-color;
-
-      &.is-link,
-      a {
-        font-weight: 500;
-        color: @text-color-base;
-        text-decoration: none;
-        transition: color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
-      }
-
-      a:hover,
-      &.is-link:hover {
-        color: @primary-color;
-        cursor: pointer;
-      }
-    }
-
-    &__item:last-child .breadcrumb__inner,
-    &__item:last-child &__inner a,
-    &__item:last-child &__inner a:hover,
-    &__item:last-child &__inner:hover {
-      font-weight: 400;
-      color: @breadcrumb-item-normal-color;
-      cursor: text;
-    }
-
-    &__item:last-child &__separator {
-      display: none;
-    }
-  }
-</style>

+ 0 - 57
src/components/Breadcrumb/src/BreadcrumbItem.vue

@@ -1,57 +0,0 @@
-<template>
-  <span class="breadcrumb__item">
-    <span ref="linkRef" :class="['breadcrumb__inner', to || isLink ? 'is-link' : '']">
-      <slot />
-    </span>
-    <i v-if="separatorClass" class="breadcrumb__separator" :class="separatorClass"></i>
-    <span v-else class="breadcrumb__separator">{{ separator }}</span>
-  </span>
-</template>
-
-<script lang="ts">
-  import { defineComponent, inject, ref, onMounted, unref } from 'vue';
-  import { useRouter } from 'vue-router';
-  import { useEventListener } from '/@/hooks/event/useEventListener';
-
-  import { propTypes } from '/@/utils/propTypes';
-
-  export default defineComponent({
-    name: 'BreadcrumbItem',
-    props: {
-      to: propTypes.oneOfType([propTypes.string, propTypes.object]),
-      replace: propTypes.bool,
-      isLink: propTypes.bool,
-    },
-    setup(props) {
-      const linkRef = ref<Nullable<HTMLElement>>(null);
-
-      const parent = inject('breadcrumb') as {
-        separator: string;
-        separatorClass: string;
-      };
-
-      const { push, replace } = useRouter();
-
-      onMounted(() => {
-        const link = unref(linkRef);
-        if (!link) return;
-        useEventListener({
-          el: link,
-          listener: () => {
-            const { to } = props;
-            if (!props.to) return;
-            props.replace ? replace(to) : push(to);
-          },
-          name: 'click',
-          wait: 0,
-        });
-      });
-
-      return {
-        linkRef,
-        separator: parent.separator && parent.separator,
-        separatorClass: parent.separatorClass && parent.separatorClass,
-      };
-    },
-  });
-</script>

+ 2 - 1
src/components/Menu/src/BasicMenu.tsx

@@ -36,6 +36,7 @@ import { getCurrentParentPath } from '/@/router/menus';
 
 import { basicProps } from './props';
 import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
+import { REDIRECT_NAME } from '/@/router/constant';
 export default defineComponent({
   name: 'BasicMenu',
   props: basicProps,
@@ -120,7 +121,7 @@ export default defineComponent({
     watch(
       () => currentRoute.value.name,
       (name: string) => {
-        if (name === 'Redirect') return;
+        if (name === REDIRECT_NAME) return;
         handleMenuChange();
         props.isHorizontal && appStore.getProjectConfig.menuSetting.split && getParentPath();
       }

+ 1 - 1
src/components/Menu/src/hooks/useOpenKeys.ts

@@ -4,7 +4,7 @@ import type { MenuState } from '../types';
 import type { Ref } from 'vue';
 
 import { unref } from 'vue';
-import { getAllParentPath } from '/@/utils/helper/menuHelper';
+import { getAllParentPath } from '/@/router/helper/menuHelper';
 import { es6Unique } from '/@/utils';
 import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
 

+ 1 - 1
src/components/Menu/src/hooks/useSearchInput.ts

@@ -5,7 +5,7 @@ import type { Ref } from 'vue';
 import { isString } from '/@/utils/is';
 import { unref } from 'vue';
 import { es6Unique } from '/@/utils';
-import { getAllParentPath } from '/@/utils/helper/menuHelper';
+import { getAllParentPath } from '/@/router/helper/menuHelper';
 
 interface UseSearchInputOptions {
   menuState: MenuState;

+ 6 - 4
src/components/Table/src/components/TableSetting.vue

@@ -33,7 +33,7 @@
 
     <Tooltip placement="top" v-if="getSetting.setting">
       <template #title>
-        <span>{{ t('settingColumn') }}</span>
+        <span>{{ t('component.table.settingColumn') }}</span>
       </template>
       <Popover
         placement="bottomLeft"
@@ -58,9 +58,11 @@
               v-model:checked="checkAll"
               @change="onCheckAllChange"
             >
-              {{ t('settingColumnShow') }}
+              {{ t('component.table.settingColumnShow') }}
             </Checkbox>
-            <a-button size="small" type="link" @click="reset"> {{ t('settingReset') }}</a-button>
+            <a-button size="small" type="link" @click="reset">
+              {{ t('component.table.settingReset') }}</a-button
+            >
           </div>
         </template>
         <SettingOutlined />
@@ -69,7 +71,7 @@
 
     <Tooltip placement="top" v-if="getSetting.fullScreen">
       <template #title>
-        <span>{{ t('settingFullScreen') }}</span>
+        <span>{{ t('component.table.settingFullScreen') }}</span>
       </template>
       <FullscreenOutlined @click="handleFullScreen" v-if="!isFullscreenRef" />
       <FullscreenExitOutlined @click="handleFullScreen" v-else />

+ 2 - 0
src/components/registerGlobComp.ts

@@ -33,6 +33,7 @@ import {
   Empty,
   Avatar,
   Menu,
+  Breadcrumb,
 } from 'ant-design-vue';
 import { getApp } from '/@/setup/App';
 
@@ -55,6 +56,7 @@ export function registerGlobComp() {
   getApp()
     .use(Select)
     .use(Alert)
+    .use(Breadcrumb)
     .use(Checkbox)
     .use(DatePicker)
     .use(Radio)

+ 0 - 18
src/design/transition/breadcrumb.less

@@ -1,18 +0,0 @@
-.breadcrumb-enter-active,
-.breadcrumb-leave-active {
-  transition: all 0.24s;
-}
-
-.breadcrumb-enter-from,
-.breadcrumb-leave-active {
-  opacity: 0;
-  transform: translateX(16px);
-}
-
-.breadcrumb-move {
-  transition: all 0.38s;
-}
-
-.breadcrumb-leave-active {
-  position: absolute;
-}

+ 0 - 1
src/design/transition/index.less

@@ -4,4 +4,3 @@
 @import './slide.less';
 @import './scroll.less';
 @import './zoom.less';
-@import './breadcrumb.less';

+ 1 - 1
src/enums/pageEnum.ts

@@ -2,7 +2,7 @@ export enum PageEnum {
   // basic login path
   BASE_LOGIN = '/login',
   // basic home path
-  BASE_HOME = '/dashboard',
+  BASE_HOME = '/home',
   // error page path
   ERROR_PAGE = '/exception',
   // error log page path

+ 4 - 3
src/hooks/web/usePage.ts

@@ -1,11 +1,12 @@
 import { appStore } from '/@/store/modules/app';
 import type { RouteLocationRaw } from 'vue-router';
 
-import { useRouter } from 'vue-router';
 import { PageEnum } from '/@/enums/pageEnum';
 import { isString } from '/@/utils/is';
 import { unref } from 'vue';
 
+import router from '/@/router';
+
 export type RouteLocationRawEx = Omit<RouteLocationRaw, 'path'> & { path: PageEnum };
 
 function handleError(e: Error) {
@@ -18,7 +19,7 @@ function handleError(e: Error) {
 
 // page switch
 export function useGo() {
-  const { push, replace } = useRouter();
+  const { push, replace } = router;
   function go(opt: PageEnum | RouteLocationRawEx | string = PageEnum.BASE_HOME, isReplace = false) {
     if (!opt) return;
     if (isString(opt)) {
@@ -35,7 +36,7 @@ export function useGo() {
  * @description: redo current page
  */
 export const useRedo = () => {
-  const { push, currentRoute } = useRouter();
+  const { push, currentRoute } = router;
   const { query, params } = currentRoute.value;
   function redo() {
     push({

+ 7 - 10
src/hooks/web/usePermission.ts

@@ -7,13 +7,14 @@ import { userStore } from '/@/store/modules/user';
 import { useTabs } from './useTabs';
 
 import router, { resetRouter } from '/@/router';
-import { RootRoute } from '/@/router/routes';
+// import { RootRoute } from '/@/router/routes';
 
 import { PermissionModeEnum } from '/@/enums/appEnum';
 import { RoleEnum } from '/@/enums/roleEnum';
 
 import { intersection } from 'lodash-es';
 import { isArray } from '/@/utils/is';
+import { tabStore } from '/@/store/modules/tab';
 
 // User permissions related operations
 export function usePermission() {
@@ -27,8 +28,7 @@ export function usePermission() {
           ? PermissionModeEnum.ROLE
           : PermissionModeEnum.BACK,
     });
-    resume();
-    // location.reload();
+    location.reload();
   }
 
   /**
@@ -36,18 +36,15 @@ export function usePermission() {
    * @param id
    */
   async function resume(id?: string | number) {
+    tabStore.commitClearCache();
     resetRouter();
     const routes = await permissionStore.buildRoutesAction(id);
     routes.forEach((route) => {
-      router.addRoute(RootRoute.name!, route as RouteRecordRaw);
+      router.addRoute(route as RouteRecordRaw);
     });
     permissionStore.commitLastBuildMenuTimeState();
-    const {
-      // closeAll,
-      closeOther,
-    } = useTabs();
-    // closeAll();
-    closeOther();
+    const { closeAll } = useTabs();
+    closeAll();
   }
 
   /**

+ 8 - 59
src/hooks/web/useTabs.ts

@@ -1,72 +1,21 @@
-import { TabItem, tabStore } from '/@/store/modules/tab';
+import { tabStore } from '/@/store/modules/tab';
 import { appStore } from '/@/store/modules/app';
 
-type RouteFn = (tabItem: TabItem) => void;
-
-interface TabFn {
-  refreshPageFn: RouteFn;
-  closeAllFn: Fn;
-  closeLeftFn: RouteFn;
-  closeRightFn: RouteFn;
-  closeOtherFn: RouteFn;
-  closeCurrentFn: RouteFn;
-}
-
-let refreshPage: RouteFn;
-let closeAll: Fn;
-let closeLeft: RouteFn;
-let closeRight: RouteFn;
-let closeOther: RouteFn;
-let closeCurrent: RouteFn;
-
-export let isInitUseTab = false;
-
 export function useTabs() {
-  function initTabFn({
-    refreshPageFn,
-    closeAllFn,
-    closeLeftFn,
-    closeRightFn,
-    closeOtherFn,
-    closeCurrentFn,
-  }: TabFn) {
-    if (isInitUseTab) return;
-
-    refreshPageFn && (refreshPage = refreshPageFn);
-    closeAllFn && (closeAll = closeAllFn);
-    closeLeftFn && (closeLeft = closeLeftFn);
-    closeRightFn && (closeRight = closeRightFn);
-    closeOtherFn && (closeOther = closeOtherFn);
-    closeCurrentFn && (closeCurrent = closeCurrentFn);
-    isInitUseTab = true;
-  }
-
-  function resetCache() {
-    const def = undefined as any;
-    refreshPage = def;
-    closeAll = def;
-    closeLeft = def;
-    closeRight = def;
-    closeOther = def;
-    closeCurrent = def;
-  }
-
   function canIUseFn(): boolean {
     const { multiTabsSetting: { show } = {} } = appStore.getProjectConfig;
     if (!show) {
-      throw new Error('当前未开启多标签页,请在设置中打开!');
+      throw new Error('The multi-tab page is currently not open, please open it in the settings!');
     }
     return !!show;
   }
 
   return {
-    initTabFn,
-    refreshPage: () => canIUseFn() && refreshPage(tabStore.getCurrentTab),
-    closeAll: () => canIUseFn() && closeAll(),
-    closeLeft: () => canIUseFn() && closeLeft(tabStore.getCurrentTab),
-    closeRight: () => canIUseFn() && closeRight(tabStore.getCurrentTab),
-    closeOther: () => canIUseFn() && closeOther(tabStore.getCurrentTab),
-    closeCurrent: () => canIUseFn() && closeCurrent(tabStore.getCurrentTab),
-    resetCache: () => canIUseFn() && resetCache(),
+    refreshPage: () => canIUseFn() && tabStore.commitRedoPage(),
+    closeAll: () => canIUseFn() && tabStore.closeAllTabAction(),
+    closeLeft: () => canIUseFn() && tabStore.closeLeftTabAction(tabStore.getCurrentTab),
+    closeRight: () => canIUseFn() && tabStore.closeRightTabAction(tabStore.getCurrentTab),
+    closeOther: () => canIUseFn() && tabStore.closeOtherTabAction(tabStore.getCurrentTab),
+    closeCurrent: () => canIUseFn() && tabStore.closeTabAction(tabStore.getCurrentTab),
   };
 }

+ 2 - 4
src/layouts/default/content/index.tsx

@@ -3,11 +3,9 @@ import './index.less';
 import { defineComponent, unref } from 'vue';
 import { Loading } from '/@/components/Loading';
 
-import { RouterView } from 'vue-router';
-
 import { useRootSetting } from '/@/hooks/setting/useRootSetting';
 import { useTransitionSetting } from '/@/hooks/setting/useTransitionSetting';
-
+import PageLayout from '/@/layouts/page/index.vue';
 export default defineComponent({
   name: 'LayoutContent',
   setup() {
@@ -20,7 +18,7 @@ export default defineComponent({
           {unref(getOpenPageLoading) && (
             <Loading loading={unref(getPageLoading)} absolute class="layout-content__loading" />
           )}
-          <RouterView />
+          <PageLayout />
         </div>
       );
     };

+ 0 - 128
src/layouts/default/header/LayoutBreadcrumb.tsx

@@ -1,128 +0,0 @@
-import type { AppRouteRecordRaw } from '/@/router/types';
-import type { RouteLocationMatched } from 'vue-router';
-import type { PropType } from 'vue';
-
-import { defineComponent, TransitionGroup, unref, watch, ref } from 'vue';
-import Icon from '/@/components/Icon';
-
-import { Breadcrumb, BreadcrumbItem } from '/@/components/Breadcrumb';
-
-import { useRouter } from 'vue-router';
-
-import { isBoolean } from '/@/utils/is';
-import { compile } from 'path-to-regexp';
-
-import router from '/@/router';
-
-import { PageEnum } from '/@/enums/pageEnum';
-import { useI18n } from '/@/hooks/web/useI18n';
-
-export default defineComponent({
-  name: 'BasicBreadcrumb',
-  props: {
-    showIcon: {
-      type: Boolean as PropType<boolean>,
-      default: false,
-    },
-  },
-  setup(props) {
-    const itemList = ref<AppRouteRecordRaw[]>([]);
-
-    const { currentRoute, push } = useRouter();
-    const { t } = useI18n();
-    watch(
-      () => currentRoute.value,
-      () => {
-        if (unref(currentRoute).name === 'Redirect') return;
-        getBreadcrumb();
-      },
-      { immediate: true }
-    );
-
-    function getBreadcrumb() {
-      const { matched } = unref(currentRoute);
-      const matchedList = matched.filter((item) => item.meta && item.meta.title).slice(1);
-      const firstItem = matchedList[0];
-      const ret = getHomeRoute(firstItem);
-      if (!isBoolean(ret)) {
-        matchedList.unshift(ret);
-      }
-      itemList.value = ((matchedList as any) as AppRouteRecordRaw[]).filter(
-        (item) => item.meta && item.meta.title && !item.meta.hideBreadcrumb
-      );
-    }
-
-    function getHomeRoute(firstItem: RouteLocationMatched) {
-      if (!firstItem || !firstItem.name) return false;
-      const routes = router.getRoutes();
-      const homeRoute = routes.find((item) => item.path === PageEnum.BASE_HOME);
-      if (!homeRoute) return false;
-      if (homeRoute.name === firstItem.name) return false;
-      return homeRoute;
-    }
-
-    function pathCompile(path: string) {
-      const { params } = unref(currentRoute);
-      const toPath = compile(path);
-      return toPath(params);
-    }
-
-    function handleItemClick(item: AppRouteRecordRaw) {
-      const { redirect, path, meta } = item;
-      if (meta.disabledRedirect) return;
-      if (redirect) {
-        push(redirect as string);
-        return;
-      }
-      return push(pathCompile(path));
-    }
-
-    function renderItemContent(item: AppRouteRecordRaw) {
-      return (
-        <>
-          {props.showIcon && item.meta.icon && item.meta.icon.trim() !== '' && (
-            <Icon
-              icon={item.meta.icon}
-              class="icon mr-1 "
-              style={{
-                marginBottom: '2px',
-              }}
-            />
-          )}
-          {t(item.meta.title)}
-        </>
-      );
-    }
-
-    function renderBreadcrumbItemList() {
-      return unref(itemList).map((item) => {
-        const isLink =
-          (!!item.redirect && !item.meta.disabledRedirect) ||
-          !item.children ||
-          item.children.length === 0;
-
-        return (
-          <BreadcrumbItem
-            key={item.path}
-            isLink={isLink}
-            onClick={handleItemClick.bind(null, item)}
-          >
-            {() => renderItemContent(item as AppRouteRecordRaw)}
-          </BreadcrumbItem>
-        );
-      });
-    }
-
-    function renderBreadcrumbDefault() {
-      return (
-        <TransitionGroup name="breadcrumb">{() => renderBreadcrumbItemList()}</TransitionGroup>
-      );
-    }
-
-    return () => (
-      <Breadcrumb class={['layout-breadcrumb', unref(itemList).length === 0 ? 'hidden' : '']}>
-        {() => renderBreadcrumbDefault()}
-      </Breadcrumb>
-    );
-  },
-});

+ 79 - 0
src/layouts/default/header/LayoutBreadcrumb.vue

@@ -0,0 +1,79 @@
+<template>
+  <div class="layout-breadcrumb">
+    <a-breadcrumb :routes="routes">
+      <template #itemRender="{ route, routes }">
+        <Icon :icon="route.meta.icon" v-if="showIcon && route.meta.icon" />
+        <span v-if="routes.indexOf(route) === routes.length - 1">
+          {{ t(route.meta.title) }}
+        </span>
+        <router-link v-else :to="route.path">
+          {{ t(route.meta.title) }}
+        </router-link>
+      </template>
+    </a-breadcrumb>
+  </div>
+</template>
+<script lang="ts">
+  import { PropType } from 'vue';
+  import { defineComponent, ref, toRaw, watchEffect } from 'vue';
+  import { useI18n } from 'vue-i18n';
+
+  import type { RouteLocationMatched } from 'vue-router';
+  import { useRouter } from 'vue-router';
+  import { filter } from '/@/utils/helper/treeHelper';
+  import { REDIRECT_NAME } from '/@/router/constant';
+  import Icon from '/@/components/Icon';
+
+  import { HomeOutlined } from '@ant-design/icons-vue';
+  import { PageEnum } from '/@/enums/pageEnum';
+  export default defineComponent({
+    name: 'LayoutBreadcrumb',
+    components: { HomeOutlined, Icon },
+    props: {
+      showIcon: {
+        type: Boolean as PropType<boolean>,
+        default: false,
+      },
+    },
+    setup() {
+      const routes = ref<RouteLocationMatched[]>([]);
+      const { currentRoute } = useRouter();
+
+      const { t } = useI18n();
+      watchEffect(() => {
+        if (currentRoute.value.name === REDIRECT_NAME) {
+          return;
+        }
+        const matched = currentRoute.value.matched;
+        if (!matched || matched.length === 0) return;
+
+        let breadcrumbList = filter(toRaw(matched), (item) => {
+          if (!item.meta) {
+            return false;
+          }
+          const { title, hideBreadcrumb } = item.meta;
+          if (!title || hideBreadcrumb) {
+            return false;
+          }
+          return true;
+        });
+
+        const filterBreadcrumbList = breadcrumbList.filter(
+          (item) => item.path !== PageEnum.BASE_HOME
+        );
+
+        if (filterBreadcrumbList.length === breadcrumbList.length) {
+          filterBreadcrumbList.unshift({
+            path: PageEnum.BASE_HOME,
+            meta: {
+              title: t('layout.header.home'),
+            },
+          });
+        }
+        routes.value = filterBreadcrumbList;
+      });
+
+      return { routes, t };
+    },
+  });
+</script>

+ 1 - 1
src/layouts/default/header/LayoutHeader.tsx

@@ -9,7 +9,7 @@ import { Layout, Tooltip, Badge } from 'ant-design-vue';
 import { AppLogo } from '/@/components/Application';
 import UserDropdown from './UserDropdown';
 import LayoutMenu from '../menu';
-import LayoutBreadcrumb from './LayoutBreadcrumb';
+import LayoutBreadcrumb from './LayoutBreadcrumb.vue';
 import LockAction from '../lock/LockAction';
 import LayoutTrigger from '../LayoutTrigger';
 import NoticeAction from './notice/NoticeActionItem.vue';

+ 1 - 0
src/layouts/default/header/LayoutMultipleHeader.less

@@ -1,5 +1,6 @@
 .multiple-tab-header {
   flex: 0 0 auto;
+  margin-left: -1px;
 
   &.fixed {
     position: fixed;

+ 45 - 11
src/layouts/default/header/index.less

@@ -21,11 +21,15 @@
 
   &__left {
     display: flex;
+    height: 100%;
     align-items: center;
 
     .layout-trigger {
+      display: flex;
+      height: 100%;
       padding: 1px 10px 0 16px;
       cursor: pointer;
+      align-items: center;
 
       .anticon {
         font-size: 17px;
@@ -49,12 +53,22 @@
     }
 
     .layout-breadcrumb {
+      display: flex;
       padding: 0 8px;
+      align-items: center;
+
+      .ant-breadcrumb-link {
+        .anticon {
+          margin-right: 4px;
+          margin-bottom: 2px;
+        }
+      }
     }
   }
 
   &__content {
     display: flex;
+    height: 100%;
     flex-grow: 1;
     align-items: center;
   }
@@ -72,6 +86,24 @@
       }
     }
 
+    .layout-breadcrumb {
+      .ant-breadcrumb-link {
+        color: @breadcrumb-item-normal-color;
+
+        a {
+          color: @text-color-base;
+
+          &:hover {
+            color: @primary-color;
+          }
+        }
+      }
+
+      .ant-breadcrumb-separator {
+        color: @breadcrumb-item-normal-color;
+      }
+    }
+
     .layout-header__logo {
       height: @header-height;
       color: @text-color-base;
@@ -152,20 +184,22 @@
       }
     }
 
-    .breadcrumb {
-      &__item:last-child .breadcrumb__inner,
-      &__item:last-child &__inner a,
-      &__item:last-child &__inner a:hover,
-      &__item:last-child &__inner:hover {
-        font-weight: 400;
+    .layout-breadcrumb {
+      .ant-breadcrumb-link {
         color: rgba(255, 255, 255, 0.6);
-        cursor: text;
+
+        a {
+          color: rgba(255, 255, 255, 0.8);
+
+          &:hover {
+            color: @white;
+          }
+        }
       }
 
-      &__inner,
-      &__inner.is-link,
-      &__separator {
-        color: @white;
+      .ant-breadcrumb-separator,
+      .anticon {
+        color: rgba(255, 255, 255, 0.8);
       }
     }
   }

+ 27 - 45
src/layouts/default/multitabs/TabContent.tsx

@@ -1,20 +1,19 @@
 import type { PropType } from 'vue';
+import { Dropdown } from '/@/components/Dropdown/index';
 
-import { defineComponent, unref, computed, FunctionalComponent } from 'vue';
+import { defineComponent, unref, FunctionalComponent } from 'vue';
 
-import { TabItem, tabStore } from '/@/store/modules/tab';
-import { getScaleAction, TabContentProps } from './data';
+import { TabContentProps } from './types';
 
-import { Dropdown } from '/@/components/Dropdown/index';
 import { RightOutlined } from '@ant-design/icons-vue';
 
-import { TabContentEnum } from './data';
+import { TabContentEnum } from './types';
+
 import { useTabDropdown } from './useTabDropdown';
-import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
-import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
-import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting';
 import { useI18n } from '/@/hooks/web/useI18n';
 
+import { RouteLocationNormalized } from 'vue-router';
+
 const { t: titleT } = useI18n();
 
 const ExtraContent: FunctionalComponent = () => {
@@ -25,21 +24,13 @@ const ExtraContent: FunctionalComponent = () => {
   );
 };
 
-const TabContent: FunctionalComponent<{ tabItem: TabItem }> = (props) => {
+const TabContent: FunctionalComponent<{ tabItem: RouteLocationNormalized; handler: Fn }> = (
+  props
+) => {
   const { tabItem: { meta } = {} } = props;
 
-  function handleContextMenu(e: Event) {
-    if (!props.tabItem) return;
-    const tableItem = props.tabItem;
-    e?.preventDefault();
-    const index = unref(tabStore.getTabsState).findIndex((tab) => tab.path === tableItem.path);
-
-    tabStore.commitCurrentContextMenuIndexState(index);
-    tabStore.commitCurrentContextMenuState(props.tabItem);
-  }
-
   return (
-    <div class={`multiple-tabs-content__content `} onContextmenu={handleContextMenu}>
+    <div class={`multiple-tabs-content__content `} onContextmenu={props.handler(props.tabItem)}>
       <span class="ml-1">{meta && titleT(meta.title)}</span>
     </div>
   );
@@ -49,7 +40,7 @@ export default defineComponent({
   name: 'TabContent',
   props: {
     tabItem: {
-      type: Object as PropType<TabItem>,
+      type: Object as PropType<RouteLocationNormalized>,
       default: null,
     },
 
@@ -59,36 +50,27 @@ export default defineComponent({
     },
   },
   setup(props) {
-    const { t } = useI18n();
-    const { getShowMenu } = useMenuSetting();
-    const { getShowHeader } = useHeaderSetting();
-    const { getShowQuick } = useMultipleTabSetting();
-
-    const getIsScale = computed(() => {
-      return !unref(getShowMenu) && !unref(getShowHeader);
-    });
-
-    const getIsTab = computed(() => {
-      return !unref(getShowQuick) ? true : props.type === TabContentEnum.TAB_TYPE;
-    });
-
-    const { getDropMenuList, handleMenuEvent } = useTabDropdown(props as TabContentProps);
+    const {
+      getDropMenuList,
+      handleMenuEvent,
+      handleContextMenu,
+      getTrigger,
+      isTabs,
+    } = useTabDropdown(props as TabContentProps);
 
     return () => {
-      const scaleAction = getScaleAction(
-        unref(getIsScale) ? t('layout.multipleTab.putAway') : t('layout.multipleTab.unfold'),
-        unref(getIsScale)
-      );
-      const dropMenuList = unref(getDropMenuList) || [];
-
-      const isTab = unref(getIsTab);
       return (
         <Dropdown
-          dropMenuList={!isTab ? [scaleAction, ...dropMenuList] : dropMenuList}
-          trigger={isTab ? ['contextmenu'] : ['click']}
+          dropMenuList={unref(getDropMenuList)}
+          trigger={unref(getTrigger)}
           onMenuEvent={handleMenuEvent}
         >
-          {() => (isTab ? <TabContent tabItem={props.tabItem} /> : <ExtraContent />)}
+          {() => {
+            if (!unref(isTabs)) {
+              return <ExtraContent />;
+            }
+            return <TabContent handler={handleContextMenu} tabItem={props.tabItem} />;
+          }}
         </Dropdown>
       );
     };

+ 0 - 90
src/layouts/default/multitabs/data.ts

@@ -1,90 +0,0 @@
-import { DropMenu } from '/@/components/Dropdown/index';
-import { AppRouteRecordRaw } from '/@/router/types';
-import type { TabItem } from '/@/store/modules/tab';
-
-import { useI18n } from '/@/hooks/web/useI18n';
-
-const { t } = useI18n();
-
-export enum TabContentEnum {
-  TAB_TYPE,
-  EXTRA_TYPE,
-}
-
-export interface TabContentProps {
-  tabItem: TabItem | AppRouteRecordRaw;
-  type?: TabContentEnum;
-  trigger?: Array<'click' | 'hover' | 'contextmenu'>;
-}
-
-/**
- * @description: 右键:下拉菜单文字
- */
-export enum MenuEventEnum {
-  // 刷新
-  REFRESH_PAGE,
-  // 关闭当前
-  CLOSE_CURRENT,
-  // 关闭左侧
-  CLOSE_LEFT,
-  // 关闭右侧
-  CLOSE_RIGHT,
-  // 关闭其他
-  CLOSE_OTHER,
-  // 关闭所有
-  CLOSE_ALL,
-  // 放大
-  SCALE,
-}
-
-export function getActions() {
-  const REFRESH_PAGE: DropMenu = {
-    icon: 'ant-design:reload-outlined',
-    event: MenuEventEnum.REFRESH_PAGE,
-    text: t('layout.multipleTab.redo'),
-    disabled: false,
-  };
-  const CLOSE_CURRENT: DropMenu = {
-    icon: 'ant-design:close-outlined',
-    event: MenuEventEnum.CLOSE_CURRENT,
-    text: t('layout.multipleTab.close'),
-    disabled: false,
-    divider: true,
-  };
-  const CLOSE_LEFT: DropMenu = {
-    icon: 'ant-design:pic-left-outlined',
-    event: MenuEventEnum.CLOSE_LEFT,
-    text: t('layout.multipleTab.closeLeft'),
-    disabled: false,
-    divider: false,
-  };
-  const CLOSE_RIGHT: DropMenu = {
-    icon: 'ant-design:pic-right-outlined',
-    event: MenuEventEnum.CLOSE_RIGHT,
-    text: t('layout.multipleTab.closeRight'),
-    disabled: false,
-    divider: true,
-  };
-  const CLOSE_OTHER: DropMenu = {
-    icon: 'ant-design:pic-center-outlined',
-    event: MenuEventEnum.CLOSE_OTHER,
-    text: t('layout.multipleTab.closeOther'),
-    disabled: false,
-  };
-  const CLOSE_ALL: DropMenu = {
-    icon: 'ant-design:line-outlined',
-    event: MenuEventEnum.CLOSE_ALL,
-    text: t('layout.multipleTab.closeAll'),
-    disabled: false,
-  };
-  return [REFRESH_PAGE, CLOSE_CURRENT, CLOSE_LEFT, CLOSE_RIGHT, CLOSE_OTHER, CLOSE_ALL];
-}
-
-export function getScaleAction(text: string, isZoom = false) {
-  return {
-    icon: isZoom ? 'codicon:screen-normal' : 'codicon:screen-full',
-    event: MenuEventEnum.SCALE,
-    text: text,
-    disabled: false,
-  };
-}

+ 13 - 60
src/layouts/default/multitabs/index.tsx

@@ -1,12 +1,8 @@
 import './index.less';
 
-import type { TabContentProps } from './data';
-import type { TabItem } from '/@/store/modules/tab';
-import type { AppRouteRecordRaw } from '/@/router/types';
-
-import { defineComponent, watch, computed, unref, ref, onMounted, nextTick } from 'vue';
-import Sortable from 'sortablejs';
+import type { TabContentProps } from './types';
 
+import { defineComponent, watch, computed, unref, ref } from 'vue';
 import { useRouter } from 'vue-router';
 
 import { Tabs } from 'ant-design-vue';
@@ -14,15 +10,12 @@ import TabContent from './TabContent';
 
 import { useGo } from '/@/hooks/web/usePage';
 
-import { TabContentEnum } from './data';
+import { TabContentEnum } from './types';
 
 import { tabStore } from '/@/store/modules/tab';
 import { userStore } from '/@/store/modules/user';
 
-import { closeTab } from './useTabDropdown';
-import { initAffixTabs } from './useMultipleTabs';
-import { isNullAndUnDef } from '/@/utils/is';
-import { useProjectSetting } from '/@/hooks/setting';
+import { initAffixTabs, useTabsDrag } from './useMultipleTabs';
 
 export default defineComponent({
   name: 'MultipleTabs',
@@ -31,28 +24,25 @@ export default defineComponent({
 
     const affixTextList = initAffixTabs();
 
-    const go = useGo();
+    useTabsDrag(affixTextList);
 
-    const { multiTabsSetting } = useProjectSetting();
+    const go = useGo();
 
     const { currentRoute } = useRouter();
 
     const getTabsState = computed(() => tabStore.getTabsState);
 
-    // If you monitor routing changes, tab switching will be stuck. So setting this method
     watch(
-      () => tabStore.getLastChangeRouteState,
+      () => tabStore.getLastChangeRouteState?.path,
       () => {
         const lastChangeRoute = unref(tabStore.getLastChangeRouteState);
-
         if (!lastChangeRoute || !userStore.getTokenState) return;
-
-        const { path, fullPath } = lastChangeRoute as AppRouteRecordRaw;
+        const { path, fullPath } = lastChangeRoute;
         const p = fullPath || path;
         if (activeKeyRef.value !== p) {
           activeKeyRef.value = p;
         }
-        tabStore.commitAddTab(lastChangeRoute);
+        tabStore.addTabAction(lastChangeRoute);
       },
       {
         immediate: true,
@@ -67,22 +57,19 @@ export default defineComponent({
     // Close the current tab
     function handleEdit(targetKey: string) {
       // Added operation to hide, currently only use delete operation
-      const index = unref(getTabsState).findIndex(
-        (item) => (item.fullPath || item.path) === targetKey
-      );
-      index !== -1 && closeTab(unref(getTabsState)[index]);
+      tabStore.closeTabByKeyAction(targetKey);
     }
 
     function renderQuick() {
       const tabContentProps: TabContentProps = {
-        tabItem: (currentRoute as unknown) as AppRouteRecordRaw,
+        tabItem: currentRoute.value,
         type: TabContentEnum.EXTRA_TYPE,
       };
-      return <TabContent {...(tabContentProps as any)} />;
+      return <TabContent {...tabContentProps} />;
     }
 
     function renderTabs() {
-      return unref(getTabsState).map((item: TabItem) => {
+      return unref(getTabsState).map((item) => {
         const key = item.query ? item.fullPath : item.path;
         const closable = !(item && item.meta && item.meta.affix);
 
@@ -97,40 +84,6 @@ export default defineComponent({
       });
     }
 
-    function initSortableTabs() {
-      if (!multiTabsSetting.canDrag) return;
-      nextTick(() => {
-        const el = document.querySelectorAll(
-          '.multiple-tabs .ant-tabs-nav > div'
-        )?.[0] as HTMLElement;
-
-        if (!el) return;
-        Sortable.create(el, {
-          animation: 500,
-          delay: 400,
-          delayOnTouchOnly: true,
-          filter: (e: ChangeEvent) => {
-            const text = e?.target?.innerText;
-            if (!text) return false;
-            return affixTextList.includes(text);
-          },
-          onEnd: (evt) => {
-            const { oldIndex, newIndex } = evt;
-
-            if (isNullAndUnDef(oldIndex) || isNullAndUnDef(newIndex) || oldIndex === newIndex) {
-              return;
-            }
-
-            tabStore.commitSortTabs({ oldIndex, newIndex });
-          },
-        });
-      });
-    }
-
-    onMounted(() => {
-      initSortableTabs();
-    });
-
     return () => {
       const slots = {
         default: () => renderTabs(),

+ 35 - 0
src/layouts/default/multitabs/types.ts

@@ -0,0 +1,35 @@
+import type { DropMenu } from '/@/components/Dropdown/index';
+import type { RouteLocationNormalized } from 'vue-router';
+
+export enum TabContentEnum {
+  TAB_TYPE,
+  EXTRA_TYPE,
+}
+
+export type { DropMenu };
+
+export interface TabContentProps {
+  tabItem: RouteLocationNormalized;
+  type?: TabContentEnum;
+  trigger?: ('click' | 'hover' | 'contextmenu')[];
+}
+
+/**
+ * @description: 右键:下拉菜单文字
+ */
+export enum MenuEventEnum {
+  // 刷新
+  REFRESH_PAGE,
+  // 关闭当前
+  CLOSE_CURRENT,
+  // 关闭左侧
+  CLOSE_LEFT,
+  // 关闭右侧
+  CLOSE_RIGHT,
+  // 关闭其他
+  CLOSE_OTHER,
+  // 关闭所有
+  CLOSE_ALL,
+  // 放大
+  SCALE,
+}

+ 55 - 10
src/layouts/default/multitabs/useMultipleTabs.ts

@@ -1,19 +1,22 @@
-import { toRaw, ref } from 'vue';
+import Sortable from 'sortablejs';
+import { toRaw, ref, nextTick, onMounted } from 'vue';
+import { RouteLocationNormalized } from 'vue-router';
+import { useProjectSetting } from '/@/hooks/setting';
 import router from '/@/router';
-import { AppRouteRecordRaw } from '/@/router/types';
-import { TabItem, tabStore } from '/@/store/modules/tab';
+import { tabStore } from '/@/store/modules/tab';
+import { isNullAndUnDef } from '/@/utils/is';
 
-export function initAffixTabs() {
-  const affixList = ref<TabItem[]>([]);
+export function initAffixTabs(): string[] {
+  const affixList = ref<RouteLocationNormalized[]>([]);
   /**
    * @description: Filter all fixed routes
    */
-  function filterAffixTabs(routes: AppRouteRecordRaw[]) {
-    const tabs: TabItem[] = [];
+  function filterAffixTabs(routes: RouteLocationNormalized[]) {
+    const tabs: RouteLocationNormalized[] = [];
     routes &&
       routes.forEach((route) => {
         if (route.meta && route.meta.affix) {
-          tabs.push(toRaw(route) as TabItem);
+          tabs.push(toRaw(route));
         }
       });
     return tabs;
@@ -23,10 +26,14 @@ export function initAffixTabs() {
    * @description: Set fixed tabs
    */
   function addAffixTabs(): void {
-    const affixTabs = filterAffixTabs((router.getRoutes() as unknown) as AppRouteRecordRaw[]);
+    const affixTabs = filterAffixTabs((router.getRoutes() as unknown) as RouteLocationNormalized[]);
     affixList.value = affixTabs;
     for (const tab of affixTabs) {
-      tabStore.commitAddTab(tab);
+      tabStore.addTabAction(({
+        meta: tab.meta,
+        name: tab.name,
+        path: tab.path,
+      } as unknown) as RouteLocationNormalized);
     }
   }
 
@@ -37,3 +44,41 @@ export function initAffixTabs() {
   }
   return affixList.value.map((item) => item.meta?.title).filter(Boolean);
 }
+
+export function useTabsDrag(affixTextList: string[]) {
+  const { multiTabsSetting } = useProjectSetting();
+
+  function initSortableTabs() {
+    if (!multiTabsSetting.canDrag) return;
+    nextTick(() => {
+      const el = document.querySelectorAll(
+        '.multiple-tabs .ant-tabs-nav > div'
+      )?.[0] as HTMLElement;
+
+      if (!el) return;
+      Sortable.create(el, {
+        animation: 500,
+        delay: 400,
+        delayOnTouchOnly: true,
+        filter: (e: ChangeEvent) => {
+          const text = e?.target?.innerText;
+          if (!text) return false;
+          return affixTextList.includes(text);
+        },
+        onEnd: (evt) => {
+          const { oldIndex, newIndex } = evt;
+
+          if (isNullAndUnDef(oldIndex) || isNullAndUnDef(newIndex) || oldIndex === newIndex) {
+            return;
+          }
+
+          tabStore.commitSortTabs({ oldIndex, newIndex });
+        },
+      });
+    });
+  }
+
+  onMounted(() => {
+    initSortableTabs();
+  });
+}

+ 108 - 174
src/layouts/default/multitabs/useTabDropdown.ts

@@ -1,168 +1,148 @@
-import type { AppRouteRecordRaw } from '/@/router/types';
-import type { TabContentProps } from './data';
-import type { Ref } from 'vue';
-import type { TabItem } from '/@/store/modules/tab';
+import type { TabContentProps } from './types';
 import type { DropMenu } from '/@/components/Dropdown';
 
-import { computed, unref } from 'vue';
-import { TabContentEnum, MenuEventEnum, getActions } from './data';
+import { computed, unref, reactive } from 'vue';
+import { TabContentEnum, MenuEventEnum } from './types';
 import { tabStore } from '/@/store/modules/tab';
-import { appStore } from '/@/store/modules/app';
-import { PageEnum } from '/@/enums/pageEnum';
-import { useGo, useRedo } from '/@/hooks/web/usePage';
 import router from '/@/router';
-import { useTabs, isInitUseTab } from '/@/hooks/web/useTabs';
-import { RouteLocationRaw } from 'vue-router';
+import { RouteLocationNormalized } from 'vue-router';
+import { useTabs } from '/@/hooks/web/useTabs';
+import { useI18n } from '/@/hooks/web/useI18n';
+import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
+import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
+import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting';
 
-const { initTabFn } = useTabs();
+const { t } = useI18n();
 
 export function useTabDropdown(tabContentProps: TabContentProps) {
-  const { currentRoute } = router;
-  const redo = useRedo();
-  const go = useGo();
-
-  const isTabsRef = computed(() => tabContentProps.type === TabContentEnum.TAB_TYPE);
-  const getCurrentTab: Ref<TabItem | AppRouteRecordRaw> = computed(() => {
-    return unref(isTabsRef)
-      ? tabContentProps.tabItem
-      : ((unref(currentRoute) as any) as AppRouteRecordRaw);
+  const state = reactive({
+    current: null as Nullable<RouteLocationNormalized>,
+    currentIndex: 0,
   });
 
-  // Current tab list
-  const getTabsState = computed(() => tabStore.getTabsState);
+  const { currentRoute } = router;
+
+  const { getShowMenu, setMenuSetting } = useMenuSetting();
+  const { getShowHeader, setHeaderSetting } = useHeaderSetting();
+  const { getShowQuick } = useMultipleTabSetting();
+
+  const isTabs = computed(() =>
+    !unref(getShowQuick) ? true : tabContentProps.type === TabContentEnum.TAB_TYPE
+  );
+
+  const getCurrentTab = computed(
+    (): RouteLocationNormalized => {
+      return unref(isTabs) ? tabContentProps.tabItem : unref(currentRoute);
+    }
+  );
+
+  const getIsScale = computed(() => {
+    return !unref(getShowMenu) && !unref(getShowHeader);
+  });
 
   /**
    * @description: drop-down list
    */
   const getDropMenuList = computed(() => {
-    const dropMenuList = getActions();
-    // Reset to initial state
-    for (const item of dropMenuList) {
-      item.disabled = false;
-    }
-
-    // No tab
-    if (!unref(getTabsState) || unref(getTabsState).length <= 0) {
-      return dropMenuList;
-    } else if (unref(getTabsState).length === 1) {
-      // Only one tab
-      for (const item of dropMenuList) {
-        if (item.event !== MenuEventEnum.REFRESH_PAGE) {
-          item.disabled = true;
-        }
-      }
-      return dropMenuList;
-    }
     if (!unref(getCurrentTab)) return;
-    const { meta, path } = unref(getCurrentTab);
+    const { meta } = unref(getCurrentTab);
+    const { path } = unref(currentRoute);
 
     // Refresh button
-    const curItem = tabStore.getCurrentContextMenuState;
-    const index = tabStore.getCurrentContextMenuIndexState;
+    const curItem = state.current;
+    const index = state.currentIndex;
     const refreshDisabled = curItem ? curItem.path !== path : true;
     // Close left
     const closeLeftDisabled = index === 0;
 
+    const disabled = tabStore.getTabsState.length === 1;
+
     // Close right
-    const closeRightDisabled = index === unref(getTabsState).length - 1;
-    // Currently fixed tab
-    // TODO PERf
-    dropMenuList[0].disabled = unref(isTabsRef) ? refreshDisabled : false;
-    if (meta && meta.affix) {
-      dropMenuList[1].disabled = true;
+    const closeRightDisabled =
+      index === tabStore.getTabsState.length - 1 && tabStore.getLastDragEndIndexState >= 0;
+    const dropMenuList: DropMenu[] = [
+      {
+        icon: 'ant-design:reload-outlined',
+        event: MenuEventEnum.REFRESH_PAGE,
+        text: t('layout.multipleTab.redo'),
+        disabled: refreshDisabled,
+      },
+      {
+        icon: 'ant-design:close-outlined',
+        event: MenuEventEnum.CLOSE_CURRENT,
+        text: t('layout.multipleTab.close'),
+        disabled: meta?.affix || disabled,
+        divider: true,
+      },
+      {
+        icon: 'ant-design:pic-left-outlined',
+        event: MenuEventEnum.CLOSE_LEFT,
+        text: t('layout.multipleTab.closeLeft'),
+        disabled: closeLeftDisabled,
+        divider: false,
+      },
+      {
+        icon: 'ant-design:pic-right-outlined',
+        event: MenuEventEnum.CLOSE_RIGHT,
+        text: t('layout.multipleTab.closeRight'),
+        disabled: closeRightDisabled,
+        divider: true,
+      },
+      {
+        icon: 'ant-design:pic-center-outlined',
+        event: MenuEventEnum.CLOSE_OTHER,
+        text: t('layout.multipleTab.closeOther'),
+        disabled: disabled,
+      },
+      {
+        icon: 'ant-design:line-outlined',
+        event: MenuEventEnum.CLOSE_ALL,
+        text: t('layout.multipleTab.closeAll'),
+        disabled: disabled,
+      },
+    ];
+
+    if (!unref(isTabs)) {
+      const isScale = unref(getIsScale);
+      dropMenuList.unshift({
+        icon: isScale ? 'codicon:screen-normal' : 'codicon:screen-full',
+        event: MenuEventEnum.SCALE,
+        text: isScale ? t('layout.multipleTab.putAway') : t('layout.multipleTab.unfold'),
+        disabled: false,
+      });
     }
-    dropMenuList[2].disabled = closeLeftDisabled;
-    dropMenuList[3].disabled = closeRightDisabled;
 
     return dropMenuList;
   });
 
-  /**
-   * @description: Jump to page when closing all pages
-   */
-  function gotoPage() {
-    const len = unref(getTabsState).length;
-    const { path } = unref(currentRoute);
-
-    let toPath: PageEnum | string = PageEnum.BASE_HOME;
-
-    if (len > 0) {
-      const page = unref(getTabsState)[len - 1];
-      const p = page.fullPath || page.path;
-      if (p) {
-        toPath = p;
-      }
-    }
-    // Jump to the current page and report an error
-    path !== toPath && go(toPath as PageEnum, true);
-  }
-
-  function isGotoPage(currentTab?: TabItem) {
-    const { path } = unref(currentRoute);
-    const currentPath = (currentTab || unref(getCurrentTab)).path;
-    // Not the current tab, when you close the left/right side, you need to jump to the page
-    if (path !== currentPath) {
-      go(currentPath as PageEnum, true);
-    }
-  }
-  function refreshPage(tabItem?: TabItem) {
-    try {
-      tabStore.commitCloseTabKeepAlive(tabItem || unref(getCurrentTab));
-    } catch (error) {}
-    redo();
-  }
-
-  function closeAll() {
-    tabStore.commitCloseAllTab();
-    gotoPage();
-  }
-
-  function closeLeft(tabItem?: TabItem) {
-    tabStore.closeLeftTabAction(tabItem || unref(getCurrentTab));
-    isGotoPage(tabItem);
-  }
-
-  function closeRight(tabItem?: TabItem) {
-    tabStore.closeRightTabAction(tabItem || unref(getCurrentTab));
-    isGotoPage(tabItem);
-  }
-
-  function closeOther(tabItem?: TabItem) {
-    tabStore.closeOtherTabAction(tabItem || unref(getCurrentTab));
-    isGotoPage(tabItem);
-  }
+  const getTrigger = computed(() => {
+    return unref(isTabs) ? ['contextmenu'] : ['click'];
+  });
 
-  function closeCurrent(tabItem?: TabItem) {
-    closeTab(unref(tabItem || unref(getCurrentTab)));
+  function handleContextMenu(tabItem: RouteLocationNormalized) {
+    return (e: Event) => {
+      if (!tabItem) return;
+      e?.preventDefault();
+      const index = tabStore.getTabsState.findIndex((tab) => tab.path === tabItem.path);
+      state.current = tabItem;
+      state.currentIndex = index;
+    };
   }
 
   function scaleScreen() {
-    const {
-      headerSetting: { show: showHeader },
-      menuSetting: { show: showMenu },
-    } = appStore.getProjectConfig;
-    const isScale = !showHeader && !showMenu;
-    appStore.commitProjectConfigState({
-      headerSetting: { show: isScale },
-      menuSetting: { show: isScale },
+    const isScale = !unref(getShowMenu) && !unref(getShowHeader);
+    setMenuSetting({
+      show: isScale,
     });
-  }
-
-  if (!isInitUseTab) {
-    initTabFn({
-      refreshPageFn: refreshPage,
-      closeAllFn: closeAll,
-      closeCurrentFn: closeCurrent,
-      closeLeftFn: closeLeft,
-      closeOtherFn: closeOther,
-      closeRightFn: closeRight,
+    setHeaderSetting({
+      show: isScale,
     });
   }
 
   // Handle right click event
   function handleMenuEvent(menu: DropMenu): void {
+    const { refreshPage, closeAll, closeCurrent, closeLeft, closeOther, closeRight } = useTabs();
     const { event } = menu;
-
     switch (event) {
       case MenuEventEnum.SCALE:
         scaleScreen();
@@ -193,51 +173,5 @@ export function useTabDropdown(tabContentProps: TabContentProps) {
         break;
     }
   }
-  return { getDropMenuList, handleMenuEvent };
-}
-
-export function getObj(tabItem: TabItem) {
-  const { params, path, query } = tabItem;
-  return {
-    params: params || {},
-    path,
-    query: query || {},
-  };
-}
-
-export function closeTab(closedTab: TabItem | AppRouteRecordRaw) {
-  const { currentRoute, replace } = router;
-  // Current tab list
-  const getTabsState = computed(() => tabStore.getTabsState);
-
-  const { path } = unref(currentRoute);
-  if (path !== closedTab.path) {
-    // Closed is not the activation tab
-    tabStore.commitCloseTab(closedTab);
-    return;
-  }
-
-  // Closed is activated atb
-  let toObj: RouteLocationRaw = {};
-
-  const index = unref(getTabsState).findIndex((item) => item.path === path);
-
-  // If the current is the leftmost tab
-  if (index === 0) {
-    // There is only one tab, then jump to the homepage, otherwise jump to the right tab
-    if (unref(getTabsState).length === 1) {
-      toObj = PageEnum.BASE_HOME;
-    } else {
-      //  Jump to the right tab
-      const page = unref(getTabsState)[index + 1];
-      toObj = getObj(page);
-    }
-  } else {
-    // Close the current tab
-    const page = unref(getTabsState)[index - 1];
-    toObj = getObj(page);
-  }
-  const route = (unref(currentRoute) as unknown) as AppRouteRecordRaw;
-  tabStore.commitCloseTab(route);
-  replace(toObj);
+  return { getDropMenuList, handleMenuEvent, handleContextMenu, getTrigger, isTabs };
 }

+ 1 - 1
src/layouts/iframe/index.vue

@@ -1,7 +1,7 @@
 <template>
   <template v-for="frame in getFramePages" :key="frame.path">
     <FramePage
-      v-if="frame.meta.frameSrc && hasRenderFrame(frame.path)"
+      v-if="frame.meta.frameSrc && hasRenderFrame(frame.name)"
       v-show="showIframe(frame)"
       :frameSrc="frame.meta.frameSrc"
     />

+ 7 - 4
src/layouts/iframe/useFrameKeepAlive.ts

@@ -23,7 +23,7 @@ export function useFrameKeepAlive() {
   const getOpenTabList = computed((): string[] => {
     return tabStore.getTabsState.reduce((prev: string[], next) => {
       if (next.meta && Reflect.has(next.meta, 'frameSrc')) {
-        prev.push(next.path!);
+        prev.push(next.name as string);
       }
       return prev;
     }, []);
@@ -45,11 +45,14 @@ export function useFrameKeepAlive() {
   }
 
   function showIframe(item: AppRouteRecordRaw) {
-    return item.path === unref(currentRoute).path;
+    return item.name === unref(currentRoute).name;
   }
 
-  function hasRenderFrame(path: string) {
-    return unref(getShowMultipleTab) ? unref(getOpenTabList).includes(path) : true;
+  function hasRenderFrame(name: string) {
+    if (!unref(getShowMultipleTab)) {
+      return true;
+    }
+    return unref(getOpenTabList).includes(name);
   }
   return { hasRenderFrame, getFramePages, showIframe, getAllFramePages };
 }

+ 0 - 79
src/layouts/page/index.tsx

@@ -1,79 +0,0 @@
-import type { FunctionalComponent } from 'vue';
-
-import { computed, defineComponent, unref, Transition, KeepAlive } from 'vue';
-import { RouterView, RouteLocation } from 'vue-router';
-
-import FrameLayout from '/@/layouts/iframe/index.vue';
-
-import { useTransition } from './useTransition';
-import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
-import { useRootSetting } from '/@/hooks/setting/useRootSetting';
-import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting';
-
-import { tabStore } from '/@/store/modules/tab';
-import { useTransitionSetting } from '/@/hooks/setting/useTransitionSetting';
-
-interface DefaultContext {
-  Component: FunctionalComponent;
-  route: RouteLocation;
-}
-
-export default defineComponent({
-  name: 'PageLayout',
-  setup() {
-    const { getShowMenu } = useMenuSetting();
-
-    const { getOpenKeepAlive, getCanEmbedIFramePage } = useRootSetting();
-
-    const { getBasicTransition, getEnableTransition } = useTransitionSetting();
-
-    const { getMax } = useMultipleTabSetting();
-
-    const transitionEvent = useTransition();
-
-    const openCacheRef = computed(() => unref(getOpenKeepAlive) && unref(getShowMenu));
-
-    const getCacheTabsRef = computed(() => tabStore.getKeepAliveTabsState as string[]);
-
-    return () => {
-      return (
-        <div>
-          <RouterView>
-            {{
-              default: ({ Component, route }: DefaultContext) => {
-                // No longer show animations that are already in the tab
-                const cacheTabs = unref(getCacheTabsRef);
-                const isInCache = cacheTabs.includes(route.name as string);
-                const name = isInCache && route.meta.inTab ? 'fade-slide' : null;
-
-                const renderComp = () => <Component key={route.fullPath} />;
-
-                const PageContent = unref(openCacheRef) ? (
-                  <KeepAlive max={unref(getMax)} include={cacheTabs}>
-                    {renderComp()}
-                  </KeepAlive>
-                ) : (
-                  renderComp()
-                );
-
-                return unref(getEnableTransition) ? (
-                  <Transition
-                    {...transitionEvent}
-                    name={name || route.meta.transitionName || unref(getBasicTransition)}
-                    mode="out-in"
-                    appear={true}
-                  >
-                    {() => PageContent}
-                  </Transition>
-                ) : (
-                  PageContent
-                );
-              },
-            }}
-          </RouterView>
-          {unref(getCanEmbedIFramePage) && <FrameLayout />}
-        </div>
-      );
-    };
-  },
-});

+ 21 - 0
src/layouts/page/index.vue

@@ -0,0 +1,21 @@
+<template>
+  <ParentLayout :isPage="true" />
+  <FrameLayout v-if="getCanEmbedIFramePage" />
+</template>
+<script lang="ts">
+  import { defineComponent } from 'vue';
+
+  import FrameLayout from '/@/layouts/iframe/index.vue';
+
+  import { useRootSetting } from '/@/hooks/setting/useRootSetting';
+
+  import ParentLayout from '/@/layouts/parent/index.vue';
+  export default defineComponent({
+    components: { ParentLayout, FrameLayout },
+    setup() {
+      const { getCanEmbedIFramePage } = useRootSetting();
+
+      return { getCanEmbedIFramePage };
+    },
+  });
+</script>

+ 73 - 0
src/layouts/parent/index.vue

@@ -0,0 +1,73 @@
+<!--
+ * @Description: The reason is that tsx will report warnings under multi-level nesting.
+-->
+<template>
+  <div>
+    <router-view>
+      <template #default="{ Component, route }">
+        <transition v-bind="transitionEvent" :name="getName(route)" mode="out-in" appear>
+          <keep-alive v-if="openCache" :include="getCaches">
+            <component :max="getMax" :is="Component" :key="route.fullPath" />
+          </keep-alive>
+          <component v-else :max="getMax" :is="Component" :key="route.fullPath" />
+        </transition>
+      </template>
+    </router-view>
+  </div>
+</template>
+<script lang="ts">
+  import { computed, defineComponent, unref } from 'vue';
+  import { RouteLocationNormalized } from 'vue-router';
+
+  import { useTransition } from './useTransition';
+  import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
+  import { useRootSetting } from '/@/hooks/setting/useRootSetting';
+  import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting';
+
+  import { useTransitionSetting } from '/@/hooks/setting/useTransitionSetting';
+  import { useCache } from './useCache';
+
+  export default defineComponent({
+    props: {
+      isPage: {
+        type: Boolean,
+      },
+    },
+    setup(props) {
+      const { getCaches } = useCache(props.isPage);
+
+      const { getShowMenu } = useMenuSetting();
+
+      const { getOpenKeepAlive } = useRootSetting();
+
+      const { getBasicTransition, getEnableTransition } = useTransitionSetting();
+
+      const { getMax } = useMultipleTabSetting();
+
+      const transitionEvent = useTransition();
+
+      const openCache = computed(() => unref(getOpenKeepAlive) && unref(getShowMenu));
+
+      function getName(route: RouteLocationNormalized) {
+        if (!unref(getEnableTransition)) {
+          return null;
+        }
+        const cacheTabs = unref(getCaches);
+        const isInCache = cacheTabs.includes(route.name as string);
+        const name = isInCache && route.meta.inTab ? 'fade-slide' : null;
+
+        return name || route.meta.transitionName || unref(getBasicTransition);
+      }
+
+      return {
+        getCaches,
+        getMax,
+        transitionEvent,
+        getBasicTransition,
+        getName,
+        openCache,
+        getEnableTransition,
+      };
+    },
+  });
+</script>

+ 52 - 0
src/layouts/parent/useCache.ts

@@ -0,0 +1,52 @@
+import { computed, ref, unref } from 'vue';
+import { useRootSetting } from '/@/hooks/setting/useRootSetting';
+import { tryTsxEmit } from '/@/utils/helper/vueHelper';
+import { tabStore, PAGE_LAYOUT_KEY } from '/@/store/modules/tab';
+
+import { useRouter } from 'vue-router';
+
+const ParentLayoutName = 'ParentLayout';
+export function useCache(isPage: boolean) {
+  const name = ref('');
+  const { currentRoute } = useRouter();
+
+  tryTsxEmit((instance: any) => {
+    const routeName = instance.ctx.$options.name;
+
+    if (routeName && ![ParentLayoutName].includes(routeName)) {
+      name.value = routeName;
+    } else {
+      const matched = currentRoute.value.matched;
+      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 = tabStore.getCachedMapState;
+
+    if (isPage) {
+      //  page Layout
+      // not parent 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 };
+}

+ 0 - 0
src/layouts/page/useTransition.ts → src/layouts/parent/useTransition.ts


+ 2 - 2
src/locales/lang/en/component/form.ts

@@ -4,8 +4,8 @@ export default {
   putAway: 'Put away',
   unfold: 'Unfold',
 
-  input: 'Please Input',
-  choose: 'Please Choose',
+  input: 'Please Input ',
+  choose: 'Please Choose ',
 
   maxTip: 'The number of characters should be less than {0}',
 };

+ 2 - 0
src/locales/lang/en/layout/header.ts

@@ -15,4 +15,6 @@ export default {
   lockScreen: 'Lock screen',
   lockScreenBtn: 'Locking',
   notLockScreenPassword: 'No password lock screen',
+
+  home: 'Home',
 };

+ 3 - 0
src/locales/lang/en/routes/demo/level.ts

@@ -0,0 +1,3 @@
+export default {
+  level: 'Multi menu cache',
+};

+ 2 - 0
src/locales/lang/zh_CN/layout/header.ts

@@ -16,4 +16,6 @@ export default {
   lockScreen: '锁定屏幕',
   lockScreenBtn: '锁定',
   notLockScreenPassword: '不设置密码锁屏',
+
+  home: '首页',
 };

+ 1 - 1
src/locales/lang/zh_CN/routes/demo/feat.ts

@@ -1,5 +1,5 @@
 export default {
-  feat: '页面功能',
+  feat: '功能',
   icon: '图标',
   tabs: '标签页操作',
   contextMenu: '右键菜单',

+ 3 - 0
src/locales/lang/zh_CN/routes/demo/level.ts

@@ -0,0 +1,3 @@
+export default {
+  level: '多级菜单缓存',
+};

+ 33 - 6
src/router/constant.ts

@@ -1,16 +1,30 @@
 import type { AppRouteRecordRaw } from '/@/router/types';
+import ParentLayout from '/@/layouts/parent/index.vue';
 
 const EXCEPTION_COMPONENT = () => import('../views/sys/exception/Exception');
 
 /**
  * @description: default layout
  */
-export const DEFAULT_LAYOUT_COMPONENT = () => import('/@/layouts/default/index');
+export const LAYOUT = () => import('/@/layouts/default/index');
 
 /**
  * @description: page-layout
  */
-export const PAGE_LAYOUT_COMPONENT = () => import('/@/layouts/page/index');
+export const PAGE_LAYOUT_COMPONENT = () => import('/@/layouts/page/index.vue');
+
+/**
+ * @description: page-layout
+ */
+export const getParentLayout = (name: string) => {
+  return () =>
+    new Promise((resolve) => {
+      resolve({
+        ...ParentLayout,
+        name,
+      });
+    });
+};
 
 // 404 on a page
 export const PAGE_NOT_FOUND_ROUTE: AppRouteRecordRaw = {
@@ -23,12 +37,25 @@ export const PAGE_NOT_FOUND_ROUTE: AppRouteRecordRaw = {
   },
 };
 
+export const REDIRECT_NAME = 'Redirect';
+
 export const REDIRECT_ROUTE: AppRouteRecordRaw = {
-  path: '/redirect/:path(.*)*',
-  name: 'Redirect',
-  component: () => import('/@/views/sys/redirect/index.vue'),
+  path: '/redirect',
+  name: REDIRECT_NAME,
+  component: LAYOUT,
   meta: {
-    title: 'Redirect',
+    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,
+      },
+    },
+  ],
 };

+ 6 - 13
src/router/guard/index.ts

@@ -8,17 +8,19 @@ import { createPageLoadingGuard } from './pageLoadingGuard';
 
 import { useGlobSetting, useProjectSetting } from '/@/hooks/setting';
 
-import { getIsOpenTab, setCurrentTo } from '/@/utils/helper/routeHelper';
+import { getIsOpenTab, getRoute } from '/@/router/helper/routeHelper';
 import { setTitle } from '/@/utils/browser';
 import { AxiosCanceler } from '/@/utils/http/axios/axiosCancel';
 
 import { tabStore } from '/@/store/modules/tab';
 import { useI18n } from '/@/hooks/web/useI18n';
+import { REDIRECT_NAME } from '/@/router/constant';
 
 const { closeMessageOnSwitch, removeAllHttpPending } = useProjectSetting();
 const globSetting = useGlobSetting();
+
 export function createGuard(router: Router) {
-  let axiosCanceler: AxiosCanceler | null;
+  let axiosCanceler: Nullable<AxiosCanceler>;
   if (removeAllHttpPending) {
     axiosCanceler = new AxiosCanceler();
   }
@@ -30,15 +32,7 @@ export function createGuard(router: Router) {
     to.meta.inTab = isOpen;
 
     // Notify routing changes
-    const { fullPath, path, query, params, name, meta } = to;
-    tabStore.commitLastChangeRouteState({
-      fullPath,
-      path,
-      query,
-      params,
-      name,
-      meta,
-    } as any);
+    tabStore.commitLastChangeRouteState(getRoute(to));
 
     try {
       if (closeMessageOnSwitch) {
@@ -50,14 +44,13 @@ export function createGuard(router: Router) {
     } catch (error) {
       console.warn('basic guard error:' + error);
     }
-    setCurrentTo(to);
     return true;
   });
 
   router.afterEach((to) => {
     const { t } = useI18n();
     // change html title
-    to.name !== 'Redirect' && setTitle(t(to.meta.title), globSetting.title);
+    to.name !== REDIRECT_NAME && setTitle(t(to.meta.title), globSetting.title);
   });
   createProgressGuard(router);
   createPermissionGuard(router);

+ 2 - 1
src/router/guard/pageLoadingGuard.ts

@@ -2,7 +2,7 @@ import type { Router } from 'vue-router';
 import { tabStore } from '/@/store/modules/tab';
 import { appStore } from '/@/store/modules/app';
 import { userStore } from '/@/store/modules/user';
-import { getParams } from '/@/utils/helper/routeHelper';
+import { getParams } from '/@/router/helper/routeHelper';
 import { useTransitionSetting } from '/@/hooks/setting/useTransitionSetting';
 import { unref } from 'vue';
 
@@ -14,6 +14,7 @@ export function createPageLoadingGuard(router: Router) {
     if (!userStore.getTokenState) {
       return true;
     }
+
     if (!unref(getEnableTransition) && unref(getOpenPageLoading)) {
       appStore.commitPageLoadingState(true);
       return true;

+ 3 - 2
src/router/guard/permissionGuard.ts

@@ -7,7 +7,7 @@ import { PageEnum } from '/@/enums/pageEnum';
 import { getToken } from '/@/utils/auth';
 
 import { PAGE_NOT_FOUND_ROUTE } from '/@/router/constant';
-import { RootRoute } from '../routes/index';
+// import { RootRoute } from '../routes/index';
 
 const LOGIN_PATH = PageEnum.BASE_LOGIN;
 
@@ -59,7 +59,8 @@ export function createPermissionGuard(router: Router) {
     }
     const routes = await permissionStore.buildRoutesAction();
     routes.forEach((route) => {
-      router.addRoute(RootRoute.name!, route as RouteRecordRaw);
+      // router.addRoute(RootRoute.name!, route as RouteRecordRaw);
+      router.addRoute(route as RouteRecordRaw);
     });
 
     const redirectPath = (from.query.redirect || to.path) as string;

+ 0 - 3
src/router/guard/progressGuard.ts

@@ -9,9 +9,6 @@ import { unref } from 'vue';
 const { getOpenNProgress } = useTransitionSetting();
 
 export function createProgressGuard(router: Router) {
-  // NProgress.inc(0.1);
-  // NProgress.configure({ easing: 'ease', speed: 200, showSpinner: false });
-
   router.beforeEach(async (to) => {
     !to.meta.inTab && unref(getOpenNProgress) && NProgress.start();
     return true;

+ 5 - 0
src/router/helper/dynamicImport.ts

@@ -0,0 +1,5 @@
+// The content here is just for type approval. The actual file content is overwritten by transform
+// For specific coverage, see build/vite/plugin/transform/dynamic-import/index.ts
+export default function (name: string) {
+  return name as any;
+}

+ 6 - 7
src/utils/helper/menuHelper.ts → src/router/helper/menuHelper.ts

@@ -1,7 +1,7 @@
-import { AppRouteModule, RouteModule } from '/@/router/types.d';
+import { AppRouteModule } from '/@/router/types.d';
 import type { MenuModule, Menu, AppRouteRecordRaw } from '/@/router/types';
 
-import { findPath, forEach, treeMap, treeToList } from './treeHelper';
+import { findPath, forEach, treeMap, treeToList } from '/@/utils/helper/treeHelper';
 import { cloneDeep } from 'lodash-es';
 
 export function getAllParentPath(treeData: any[], path: string) {
@@ -48,12 +48,11 @@ export function transformRouteToMenu(routeModList: AppRouteModule[]) {
   const cloneRouteModList = cloneDeep(routeModList);
   const routeList: AppRouteRecordRaw[] = [];
   cloneRouteModList.forEach((item) => {
-    const { layout, routes, children } = item as RouteModule;
-    if (layout) {
-      layout.children = routes || children;
-      routeList.push(layout);
+    if (item.meta?.single) {
+      const realItem = item?.children?.[0];
+      realItem && routeList.push(realItem);
     } else {
-      routes && routeList.push(...routes);
+      routeList.push(item);
     }
   });
   return treeMap(routeList, {

+ 89 - 0
src/router/helper/routeHelper.ts

@@ -0,0 +1,89 @@
+import type { AppRouteModule, AppRouteRecordRaw } from '/@/router/types';
+import type { RouteLocationNormalized, RouteRecordNormalized } from 'vue-router';
+
+import { appStore } from '/@/store/modules/app';
+import { tabStore } from '/@/store/modules/tab';
+import { getParentLayout, LAYOUT } from '/@/router/constant';
+import dynamicImport from './dynamicImport';
+import { cloneDeep } from 'lodash-es';
+
+// 动态引入
+function asyncImportRoute(routes: AppRouteRecordRaw[] | undefined) {
+  if (!routes) return;
+  routes.forEach((item) => {
+    const { component, name } = item;
+    const { children } = item;
+    if (component) {
+      item.component = dynamicImport(component);
+    } else if (name) {
+      item.component = getParentLayout(name);
+    }
+    children && asyncImportRoute(children);
+  });
+}
+
+function getLayoutComp(comp: string) {
+  return comp === 'LAYOUT' ? LAYOUT : '';
+}
+
+// Turn background objects into routing objects
+export function transformObjToRoute<T = AppRouteModule>(routeList: AppRouteModule[]): T[] {
+  routeList.forEach((route) => {
+    if (route.component) {
+      if ((route.component as string).toUpperCase() === 'LAYOUT') {
+        route.component = getLayoutComp(route.component);
+      } else {
+        route.children = [cloneDeep(route)];
+        route.component = LAYOUT;
+        route.name = `${route.name}Parent`;
+        route.path = '';
+        const meta = route.meta || {};
+        meta.single = true;
+        meta.affix = false;
+        route.meta = meta;
+      }
+    }
+    route.children && asyncImportRoute(route.children);
+  });
+  return (routeList as unknown) as T[];
+}
+
+/**
+ *  Determine whether the tab has been opened
+ * @param toPath
+ */
+export function getIsOpenTab(toPath: string) {
+  const { openKeepAlive, multiTabsSetting: { show } = {} } = appStore.getProjectConfig;
+
+  if (show && openKeepAlive) {
+    const tabList = tabStore.getTabsState;
+    return tabList.some((tab) => tab.path === toPath);
+  }
+  return false;
+}
+
+export function getParams(data: any = {}) {
+  const { params = {} } = data;
+  let ret = '';
+  Object.keys(params).forEach((key) => {
+    const p = params[key];
+    ret += `/${p}`;
+  });
+  return ret;
+}
+
+// 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[],
+  };
+}

+ 1 - 1
src/router/menus/index.ts

@@ -2,7 +2,7 @@ import type { Menu, MenuModule } from '/@/router/types';
 import type { RouteRecordNormalized } from 'vue-router';
 import { appStore } from '/@/store/modules/app';
 import { permissionStore } from '/@/store/modules/permission';
-import { transformMenuModule, flatMenus, getAllParentPath } from '/@/utils/helper/menuHelper';
+import { transformMenuModule, flatMenus, getAllParentPath } from '/@/router/helper/menuHelper';
 import { filter } from '/@/utils/helper/treeHelper';
 import router from '/@/router';
 import { PermissionModeEnum } from '/@/enums/appEnum';

+ 16 - 29
src/router/menus/modules/dashboard.ts

@@ -1,33 +1,20 @@
 import type { MenuModule } from '/@/router/types.d';
 
-const menu: MenuModule[] = [
-  {
-    orderNo: 0,
-    menu: {
-      path: '/dashboard/welcome',
-      name: 'routes.dashboard.welcome',
-    },
+const menu: MenuModule = {
+  orderNo: 10,
+  menu: {
+    name: 'routes.dashboard.dashboard',
+    path: '/dashboard',
+    children: [
+      {
+        path: '/workbench',
+        name: 'routes.dashboard.workbench',
+      },
+      {
+        path: '/analysis',
+        name: 'routes.dashboard.analysis',
+      },
+    ],
   },
-  {
-    orderNo: 10,
-    menu: {
-      name: 'routes.dashboard.dashboard',
-      path: '/dashboard',
-      children: [
-        {
-          path: '/workbench',
-          name: 'routes.dashboard.workbench',
-        },
-        {
-          path: '/analysis',
-          name: 'routes.dashboard.analysis',
-        },
-        // {
-        //   path: '/welcome',
-        //   name: 'routes.dashboard.welcome',
-        // },
-      ],
-    },
-  },
-];
+};
 export default menu;

+ 39 - 0
src/router/menus/modules/demo/level.ts

@@ -0,0 +1,39 @@
+import type { MenuModule } from '/@/router/types.d';
+
+const menu: MenuModule = {
+  orderNo: 2000,
+  menu: {
+    name: 'routes.demo.level.level',
+    path: '/level',
+    tag: {
+      dot: true,
+    },
+    children: [
+      {
+        path: 'menu1',
+        name: 'Menu1',
+        children: [
+          {
+            path: 'menu1-1',
+            name: 'Menu1-1',
+            children: [
+              {
+                path: 'menu1-1-1',
+                name: 'Menu1-1-1',
+              },
+            ],
+          },
+          {
+            path: 'menu1-2',
+            name: 'Menu1-2',
+          },
+        ],
+      },
+      {
+        path: 'menu2',
+        name: 'Menu2',
+      },
+    ],
+  },
+};
+export default menu;

+ 10 - 0
src/router/menus/modules/home.ts

@@ -0,0 +1,10 @@
+import type { MenuModule } from '/@/router/types.d';
+
+const menu: MenuModule = {
+  orderNo: 0,
+  menu: {
+    path: '/home/welcome',
+    name: 'routes.dashboard.welcome',
+  },
+};
+export default menu;

+ 13 - 16
src/router/routes/index.ts

@@ -1,31 +1,28 @@
 import type { AppRouteRecordRaw, AppRouteModule } from '/@/router/types';
 
-import { DEFAULT_LAYOUT_COMPONENT, PAGE_NOT_FOUND_ROUTE, REDIRECT_ROUTE } from '../constant';
-import { genRouteModule } from '/@/utils/helper/routeHelper';
+import { PAGE_NOT_FOUND_ROUTE, REDIRECT_ROUTE, LAYOUT } from '../constant';
+import { PageEnum } from '/@/enums/pageEnum';
+
 import modules from 'globby!/@/router/routes/modules/**/*.@(ts)';
 
 const routeModuleList: AppRouteModule[] = [];
 
 Object.keys(modules).forEach((key) => {
-  routeModuleList.push(modules[key]);
+  const mod = Array.isArray(modules[key]) ? [...modules[key]] : [modules[key]];
+  routeModuleList.push(...mod);
 });
 
-export const asyncRoutes = [
-  REDIRECT_ROUTE,
-  PAGE_NOT_FOUND_ROUTE,
-  ...genRouteModule(routeModuleList),
-];
+export const asyncRoutes = [PAGE_NOT_FOUND_ROUTE, ...routeModuleList];
 
-// 主框架根路由
-export const RootRoute: AppRouteRecordRaw = {
+const MainRoute: AppRouteModule = {
   path: '/',
-  name: 'Root',
-  component: DEFAULT_LAYOUT_COMPONENT,
-  redirect: '/dashboard',
+  name: 'MainRoute',
+  component: LAYOUT,
+  redirect: PageEnum.BASE_HOME,
   meta: {
-    title: 'Root',
+    icon: 'ant-design:home-outlined',
+    title: 'routes.dashboard.dashboard',
   },
-  children: [],
 };
 
 export const LoginRoute: AppRouteRecordRaw = {
@@ -38,4 +35,4 @@ export const LoginRoute: AppRouteRecordRaw = {
 };
 
 // 基础路由 不用权限
-export const basicRoutes = [LoginRoute, RootRoute];
+export const basicRoutes = [LoginRoute, MainRoute, REDIRECT_ROUTE];

+ 11 - 24
src/router/routes/modules/dashboard.ts

@@ -1,32 +1,19 @@
 import type { AppRouteModule } from '/@/router/types';
 
-import { PAGE_LAYOUT_COMPONENT } from '/@/router/constant';
+import { LAYOUT } from '/@/router/constant';
 
 const dashboard: AppRouteModule = {
-  layout: {
-    path: '/dashboard',
-    name: 'Dashboard',
-    component: PAGE_LAYOUT_COMPONENT,
-    redirect: '/dashboard/welcome',
-    meta: {
-      icon: 'ant-design:home-outlined',
-      title: 'routes.dashboard.dashboard',
-    },
+  path: '/dashboard',
+  name: 'Dashboard',
+  component: LAYOUT,
+  redirect: '/dashboard/welcome',
+  meta: {
+    icon: 'ant-design:home-outlined',
+    title: 'routes.dashboard.dashboard',
   },
-
-  routes: [
-    {
-      path: '/welcome',
-      name: 'Welcome',
-      component: () => import('/@/views/dashboard/welcome/index.vue'),
-      meta: {
-        title: 'routes.dashboard.welcome',
-        affix: true,
-        icon: 'ant-design:home-outlined',
-      },
-    },
+  children: [
     {
-      path: '/workbench',
+      path: 'workbench',
       name: 'Workbench',
       component: () => import('/@/views/dashboard/workbench/index.vue'),
       meta: {
@@ -34,7 +21,7 @@ const dashboard: AppRouteModule = {
       },
     },
     {
-      path: '/analysis',
+      path: 'analysis',
       name: 'Analysis',
       component: () => import('/@/views/dashboard/analysis/index.vue'),
       meta: {

+ 12 - 14
src/router/routes/modules/demo/charts.ts

@@ -1,23 +1,21 @@
 import type { AppRouteModule } from '/@/router/types';
 
-import { PAGE_LAYOUT_COMPONENT } from '/@/router/constant';
+import { getParentLayout, LAYOUT } from '/@/router/constant';
 
 const charts: AppRouteModule = {
-  layout: {
-    path: '/charts',
-    name: 'Charts',
-    component: PAGE_LAYOUT_COMPONENT,
-    redirect: '/charts/apexChart',
-    meta: {
-      icon: 'ant-design:area-chart-outlined',
-      title: 'routes.demo.charts.charts',
-    },
+  path: '/charts',
+  name: 'Charts',
+  component: LAYOUT,
+  redirect: '/charts/apexChart',
+  meta: {
+    icon: 'ant-design:area-chart-outlined',
+    title: 'routes.demo.charts.charts',
   },
-
-  routes: [
+  children: [
     {
-      path: '/echarts',
+      path: 'echarts',
       name: 'Echarts',
+      component: getParentLayout('Echarts'),
       meta: {
         title: 'Echarts',
       },
@@ -49,7 +47,7 @@ const charts: AppRouteModule = {
       ],
     },
     {
-      path: '/apexChart',
+      path: 'apexChart',
       name: 'ApexChart',
       meta: {
         title: 'routes.demo.charts.apexChart',

+ 26 - 25
src/router/routes/modules/demo/comp.ts

@@ -1,22 +1,20 @@
 import type { AppRouteModule } from '/@/router/types';
 
-import { PAGE_LAYOUT_COMPONENT } from '/@/router/constant';
+import { getParentLayout, LAYOUT } from '/@/router/constant';
 
 const comp: AppRouteModule = {
-  layout: {
-    path: '/comp',
-    name: 'Comp',
-    component: PAGE_LAYOUT_COMPONENT,
-    redirect: '/comp/basic',
-    meta: {
-      icon: 'ant-design:table-outlined',
-      title: 'routes.demo.comp.comp',
-    },
+  path: '/comp',
+  name: 'Comp',
+  component: LAYOUT,
+  redirect: '/comp/basic',
+  meta: {
+    icon: 'ant-design:table-outlined',
+    title: 'routes.demo.comp.comp',
   },
 
-  routes: [
+  children: [
     {
-      path: '/basic',
+      path: 'basic',
       name: 'BasicDemo',
       component: () => import('/@/views/demo/comp/button/index.vue'),
       meta: {
@@ -24,7 +22,7 @@ const comp: AppRouteModule = {
       },
     },
     {
-      path: '/transition',
+      path: 'transition',
       name: 'transitionDemo',
       component: () => import('/@/views/demo/comp/transition/index.vue'),
       meta: {
@@ -32,7 +30,7 @@ const comp: AppRouteModule = {
       },
     },
     {
-      path: '/countTo',
+      path: 'countTo',
       name: 'CountTo',
       component: () => import('/@/views/demo/comp/count-to/index.vue'),
       meta: {
@@ -41,9 +39,10 @@ const comp: AppRouteModule = {
     },
 
     {
-      path: '/scroll',
+      path: 'scroll',
       name: 'ScrollDemo',
       redirect: '/comp/scroll/basic',
+      component: getParentLayout('ScrollDemo'),
       meta: {
         title: 'routes.demo.comp.scroll',
       },
@@ -76,7 +75,7 @@ const comp: AppRouteModule = {
     },
 
     {
-      path: '/modal',
+      path: 'modal',
       name: 'ModalDemo',
       component: () => import('/@/views/demo/comp/modal/index.vue'),
       meta: {
@@ -84,7 +83,7 @@ const comp: AppRouteModule = {
       },
     },
     {
-      path: '/drawer',
+      path: 'drawer',
       name: 'DrawerDemo',
       component: () => import('/@/views/demo/comp/drawer/index.vue'),
       meta: {
@@ -92,7 +91,7 @@ const comp: AppRouteModule = {
       },
     },
     {
-      path: '/desc',
+      path: 'desc',
       name: 'DescDemo',
       component: () => import('/@/views/demo/comp/desc/index.vue'),
       meta: {
@@ -101,8 +100,9 @@ const comp: AppRouteModule = {
     },
 
     {
-      path: '/lazy',
-      name: 'lazyDemo',
+      path: 'lazy',
+      name: 'LazyDemo',
+      component: getParentLayout('LazyDemo'),
       redirect: '/comp/lazy/basic',
       meta: {
         title: 'routes.demo.comp.lazy',
@@ -127,8 +127,9 @@ const comp: AppRouteModule = {
       ],
     },
     {
-      path: '/verify',
+      path: 'verify',
       name: 'VerifyDemo',
+      component: getParentLayout('VerifyDemo'),
       redirect: '/comp/verify/drag',
       meta: {
         title: 'routes.demo.comp.verify',
@@ -155,7 +156,7 @@ const comp: AppRouteModule = {
     //
 
     {
-      path: '/qrcode',
+      path: 'qrcode',
       name: 'QrCodeDemo',
       component: () => import('/@/views/demo/comp/qrcode/index.vue'),
       meta: {
@@ -163,7 +164,7 @@ const comp: AppRouteModule = {
       },
     },
     {
-      path: '/strength-meter',
+      path: 'strength-meter',
       name: 'StrengthMeterDemo',
       component: () => import('/@/views/demo/comp/strength-meter/index.vue'),
       meta: {
@@ -171,7 +172,7 @@ const comp: AppRouteModule = {
       },
     },
     {
-      path: '/upload',
+      path: 'upload',
       name: 'UploadDemo',
       component: () => import('/@/views/demo/comp/upload/index.vue'),
       meta: {
@@ -179,7 +180,7 @@ const comp: AppRouteModule = {
       },
     },
     {
-      path: '/loading',
+      path: 'loading',
       name: 'LoadingDemo',
       component: () => import('/@/views/demo/comp/loading/index.vue'),
       meta: {

+ 12 - 15
src/router/routes/modules/demo/editor.ts

@@ -1,22 +1,19 @@
 import type { AppRouteModule } from '/@/router/types';
 
-import { PAGE_LAYOUT_COMPONENT } from '/@/router/constant';
+import { getParentLayout, LAYOUT } from '/@/router/constant';
 
 const editor: AppRouteModule = {
-  layout: {
-    path: '/editor',
-    name: 'Editor',
-    component: PAGE_LAYOUT_COMPONENT,
-    redirect: '/editor/markdown',
-    meta: {
-      icon: 'ant-design:table-outlined',
-      title: 'routes.demo.editor.editor',
-    },
+  path: '/editor',
+  name: 'Editor',
+  component: LAYOUT,
+  redirect: '/editor/markdown',
+  meta: {
+    icon: 'ant-design:table-outlined',
+    title: 'routes.demo.editor.editor',
   },
-
-  routes: [
+  children: [
     {
-      path: '/markdown',
+      path: 'markdown',
       name: 'MarkdownDemo',
       component: () => import('/@/views/demo/editor/Markdown.vue'),
       meta: {
@@ -24,7 +21,8 @@ const editor: AppRouteModule = {
       },
     },
     {
-      path: '/tinymce',
+      path: 'tinymce',
+      component: getParentLayout('TinymceDemo'),
       name: 'TinymceDemo',
       meta: {
         title: 'routes.demo.editor.tinymce',
@@ -39,7 +37,6 @@ const editor: AppRouteModule = {
             title: 'routes.demo.editor.tinymceBasic',
           },
         },
-        // TODO
         {
           path: 'editor',
           name: 'TinymceFormDemo',

+ 13 - 15
src/router/routes/modules/demo/excel.ts

@@ -1,22 +1,20 @@
 import type { AppRouteModule } from '/@/router/types';
 
-import { PAGE_LAYOUT_COMPONENT } from '/@/router/constant';
+import { LAYOUT } from '/@/router/constant';
 
 const excel: AppRouteModule = {
-  layout: {
-    path: '/excel',
-    name: 'Excel',
-    component: PAGE_LAYOUT_COMPONENT,
-    redirect: '/excel/customExport',
-    meta: {
-      icon: 'mdi:microsoft-excel',
-      title: 'routes.demo.excel.excel',
-    },
+  path: '/excel',
+  name: 'Excel',
+  component: LAYOUT,
+  redirect: '/excel/customExport',
+  meta: {
+    icon: 'mdi:microsoft-excel',
+    title: 'routes.demo.excel.excel',
   },
 
-  routes: [
+  children: [
     {
-      path: '/customExport',
+      path: 'customExport',
       name: 'CustomExport',
       component: () => import('/@/views/demo/excel/CustomExport.vue'),
       meta: {
@@ -24,7 +22,7 @@ const excel: AppRouteModule = {
       },
     },
     {
-      path: '/jsonExport',
+      path: 'jsonExport',
       name: 'JsonExport',
       component: () => import('/@/views/demo/excel/JsonExport.vue'),
       meta: {
@@ -32,7 +30,7 @@ const excel: AppRouteModule = {
       },
     },
     {
-      path: '/arrayExport',
+      path: 'arrayExport',
       name: 'ArrayExport',
       component: () => import('/@/views/demo/excel/ArrayExport.vue'),
       meta: {
@@ -40,7 +38,7 @@ const excel: AppRouteModule = {
       },
     },
     {
-      path: '/importExcel',
+      path: 'importExcel',
       name: 'ImportExcel',
       component: () => import('/@/views/demo/excel/ImportExcel.vue'),
       meta: {

+ 21 - 24
src/router/routes/modules/demo/feat.ts

@@ -1,22 +1,19 @@
 import type { AppRouteModule } from '/@/router/types';
 
-import { PAGE_LAYOUT_COMPONENT } from '/@/router/constant';
+import { LAYOUT } from '/@/router/constant';
 
 const feat: AppRouteModule = {
-  layout: {
-    path: '/feat',
-    name: 'FeatDemo',
-    component: PAGE_LAYOUT_COMPONENT,
-    redirect: '/feat/icon',
-    meta: {
-      icon: 'ic:outline-featured-play-list',
-      title: 'routes.demo.feat.feat',
-    },
+  path: '/feat',
+  name: 'FeatDemo',
+  component: LAYOUT,
+  redirect: '/feat/icon',
+  meta: {
+    icon: 'ic:outline-featured-play-list',
+    title: 'routes.demo.feat.feat',
   },
-
-  routes: [
+  children: [
     {
-      path: '/icon',
+      path: 'icon',
       name: 'IconDemo',
       component: () => import('/@/views/demo/feat/icon/index.vue'),
       meta: {
@@ -24,7 +21,7 @@ const feat: AppRouteModule = {
       },
     },
     {
-      path: '/tabs',
+      path: 'tabs',
       name: 'TabsDemo',
       component: () => import('/@/views/demo/feat/tabs/index.vue'),
       meta: {
@@ -33,7 +30,7 @@ const feat: AppRouteModule = {
     },
 
     {
-      path: '/context-menu',
+      path: 'context-menu',
       name: 'ContextMenuDemo',
       component: () => import('/@/views/demo/feat/context-menu/index.vue'),
       meta: {
@@ -41,7 +38,7 @@ const feat: AppRouteModule = {
       },
     },
     {
-      path: '/download',
+      path: 'download',
       name: 'DownLoadDemo',
       component: () => import('/@/views/demo/feat/download/index.vue'),
       meta: {
@@ -49,7 +46,7 @@ const feat: AppRouteModule = {
       },
     },
     {
-      path: '/click-out-side',
+      path: 'click-out-side',
       name: 'ClickOutSideDemo',
       component: () => import('/@/views/demo/feat/click-out-side/index.vue'),
       meta: {
@@ -57,7 +54,7 @@ const feat: AppRouteModule = {
       },
     },
     {
-      path: '/img-preview',
+      path: 'img-preview',
       name: 'ImgPreview',
       component: () => import('/@/views/demo/feat/img-preview/index.vue'),
       meta: {
@@ -65,7 +62,7 @@ const feat: AppRouteModule = {
       },
     },
     {
-      path: '/copy',
+      path: 'copy',
       name: 'CopyDemo',
       component: () => import('/@/views/demo/feat/copy/index.vue'),
       meta: {
@@ -73,7 +70,7 @@ const feat: AppRouteModule = {
       },
     },
     {
-      path: '/msg',
+      path: 'msg',
       name: 'MsgDemo',
       component: () => import('/@/views/demo/feat/msg/index.vue'),
       meta: {
@@ -81,7 +78,7 @@ const feat: AppRouteModule = {
       },
     },
     {
-      path: '/watermark',
+      path: 'watermark',
       name: 'WatermarkDemo',
       component: () => import('/@/views/demo/feat/watermark/index.vue'),
       meta: {
@@ -89,7 +86,7 @@ const feat: AppRouteModule = {
       },
     },
     {
-      path: '/full-screen',
+      path: 'full-screen',
       name: 'FullScreenDemo',
       component: () => import('/@/views/demo/feat/full-screen/index.vue'),
       meta: {
@@ -97,7 +94,7 @@ const feat: AppRouteModule = {
       },
     },
     {
-      path: '/error-log',
+      path: 'error-log',
       name: 'ErrorLog',
       component: () => import('/@/views/sys/error-log/index.vue'),
       meta: {
@@ -105,7 +102,7 @@ const feat: AppRouteModule = {
       },
     },
     {
-      path: '/testTab/:id',
+      path: 'testTab/:id',
       name: 'TestTab',
       component: () => import('/@/views/demo/feat/tab-params/index.vue'),
       meta: {

+ 16 - 19
src/router/routes/modules/demo/form.ts

@@ -1,22 +1,19 @@
 import type { AppRouteModule } from '/@/router/types';
 
-import { PAGE_LAYOUT_COMPONENT } from '/@/router/constant';
+import { LAYOUT } from '/@/router/constant';
 
 const form: AppRouteModule = {
-  layout: {
-    path: '/form',
-    name: 'FormDemo',
-    component: PAGE_LAYOUT_COMPONENT,
-    redirect: '/form/basic',
-    meta: {
-      icon: 'ant-design:table-outlined',
-      title: 'routes.demo.form.form',
-    },
+  path: '/form',
+  name: 'FormDemo',
+  component: LAYOUT,
+  redirect: '/form/basic',
+  meta: {
+    icon: 'ant-design:table-outlined',
+    title: 'routes.demo.form.form',
   },
-
-  routes: [
+  children: [
     {
-      path: '/basic',
+      path: 'basic',
       name: 'FormBasicDemo',
       component: () => import('/@/views/demo/form/index.vue'),
       meta: {
@@ -24,7 +21,7 @@ const form: AppRouteModule = {
       },
     },
     {
-      path: '/useForm',
+      path: 'useForm',
       name: 'UseFormDemo',
       component: () => import('/@/views/demo/form/UseForm.vue'),
       meta: {
@@ -32,7 +29,7 @@ const form: AppRouteModule = {
       },
     },
     {
-      path: '/refForm',
+      path: 'refForm',
       name: 'RefFormDemo',
       component: () => import('/@/views/demo/form/RefForm.vue'),
       meta: {
@@ -40,7 +37,7 @@ const form: AppRouteModule = {
       },
     },
     {
-      path: '/advancedForm',
+      path: 'advancedForm',
       name: 'AdvancedFormDemo',
       component: () => import('/@/views/demo/form/AdvancedForm.vue'),
       meta: {
@@ -48,7 +45,7 @@ const form: AppRouteModule = {
       },
     },
     {
-      path: '/ruleForm',
+      path: 'ruleForm',
       name: 'RuleFormDemo',
       component: () => import('/@/views/demo/form/RuleForm.vue'),
       meta: {
@@ -56,7 +53,7 @@ const form: AppRouteModule = {
       },
     },
     {
-      path: '/dynamicForm',
+      path: 'dynamicForm',
       name: 'DynamicFormDemo',
       component: () => import('/@/views/demo/form/DynamicForm.vue'),
       meta: {
@@ -64,7 +61,7 @@ const form: AppRouteModule = {
       },
     },
     {
-      path: '/customerForm',
+      path: 'customerForm',
       name: 'CustomerFormDemo',
       component: () => import('/@/views/demo/form/CustomerForm.vue'),
       meta: {

+ 12 - 14
src/router/routes/modules/demo/iframe.ts

@@ -1,23 +1,21 @@
 import type { AppRouteModule } from '/@/router/types';
 
-import { PAGE_LAYOUT_COMPONENT } from '/@/router/constant';
+import { LAYOUT } from '/@/router/constant';
 const IFrame = () => import('/@/views/sys/iframe/FrameBlank.vue');
 
 const iframe: AppRouteModule = {
-  layout: {
-    path: '/frame',
-    name: 'Frame',
-    component: PAGE_LAYOUT_COMPONENT,
-    redirect: '/frame/antv',
-    meta: {
-      icon: 'mdi:page-next-outline',
-      title: 'routes.demo.iframe.frame',
-    },
+  path: '/frame',
+  name: 'Frame',
+  component: LAYOUT,
+  redirect: '/frame/antv',
+  meta: {
+    icon: 'mdi:page-next-outline',
+    title: 'routes.demo.iframe.frame',
   },
 
-  routes: [
+  children: [
     {
-      path: '/antv',
+      path: 'antv',
       name: 'Antv',
       component: IFrame,
       meta: {
@@ -27,7 +25,7 @@ const iframe: AppRouteModule = {
       },
     },
     {
-      path: '/doc',
+      path: 'doc',
       name: 'Doc',
       component: IFrame,
       meta: {
@@ -37,7 +35,7 @@ const iframe: AppRouteModule = {
       },
     },
     {
-      path: '/docExternal',
+      path: 'docExternal',
       name: 'DocExternal',
       component: IFrame,
       meta: {

+ 63 - 0
src/router/routes/modules/demo/level.ts

@@ -0,0 +1,63 @@
+import type { AppRouteModule } from '/@/router/types';
+
+import { getParentLayout, LAYOUT } from '/@/router/constant';
+
+const permission: AppRouteModule = {
+  path: '/level',
+  name: 'Level',
+  component: LAYOUT,
+  redirect: '/level/menu1/menu1-1',
+  meta: {
+    icon: 'carbon:user-role',
+    title: 'routes.demo.level.level',
+  },
+
+  children: [
+    {
+      path: 'menu1',
+      name: 'Menu1Demo',
+      component: getParentLayout('Menu1Demo'),
+      meta: {
+        title: 'Menu1',
+      },
+      children: [
+        {
+          path: 'menu1-1',
+          name: 'Menu11Demo',
+          component: getParentLayout('Menu11Demo'),
+          meta: {
+            title: 'Menu1-1',
+          },
+          children: [
+            {
+              path: 'menu1-1-1',
+              name: 'Menu111Demo',
+              component: () => import('/@/views/demo/level/Menu111.vue'),
+              meta: {
+                title: 'Menu111',
+              },
+            },
+          ],
+        },
+        {
+          path: 'menu1-2',
+          name: 'Menu12Demo',
+          component: () => import('/@/views/demo/level/Menu12.vue'),
+          meta: {
+            title: 'Menu1-2',
+          },
+        },
+      ],
+    },
+    {
+      path: 'menu2',
+      name: 'Menu2Demo',
+      component: () => import('/@/views/demo/level/Menu2.vue'),
+      meta: {
+        title: 'Menu2',
+      },
+    },
+  ],
+};
+
+export default permission;

+ 15 - 8
src/router/routes/modules/demo/page.ts

@@ -1,6 +1,6 @@
 import type { AppRouteModule } from '/@/router/types';
 
-import { PAGE_LAYOUT_COMPONENT } from '/@/router/constant';
+import { getParentLayout, LAYOUT } from '/@/router/constant';
 import { ExceptionEnum } from '/@/enums/exceptionEnum';
 
 const ExceptionPage = () => import('/@/views/sys/exception/Exception');
@@ -8,7 +8,7 @@ const ExceptionPage = () => import('/@/views/sys/exception/Exception');
 const page: AppRouteModule = {
   path: '/page-demo',
   name: 'PageDemo',
-  component: PAGE_LAYOUT_COMPONENT,
+  component: LAYOUT,
   redirect: '/page-demo/exception',
   meta: {
     icon: 'mdi:page-next-outline',
@@ -17,9 +17,10 @@ const page: AppRouteModule = {
   children: [
     // =============================form start=============================
     {
-      path: '/form',
+      path: 'form',
       name: 'FormPage',
       redirect: '/page-demo/form/basic',
+      component: getParentLayout('FormPage'),
       meta: {
         title: 'routes.demo.page.form',
       },
@@ -53,8 +54,9 @@ const page: AppRouteModule = {
     // =============================form end=============================
     // =============================desc start=============================
     {
-      path: '/desc',
+      path: 'desc',
       name: 'DescPage',
+      component: getParentLayout('DescPage'),
       redirect: '/page-demo/desc/basic',
       meta: {
         title: 'routes.demo.page.desc',
@@ -82,9 +84,11 @@ const page: AppRouteModule = {
 
     // =============================result start=============================
     {
-      path: '/result',
+      path: 'result',
       name: 'ResultPage',
       redirect: '/page-demo/result/success',
+      component: getParentLayout('ResultPage'),
+
       meta: {
         title: 'routes.demo.page.result',
       },
@@ -111,8 +115,9 @@ const page: AppRouteModule = {
 
     // =============================account start=============================
     {
-      path: '/account',
+      path: 'account',
       name: 'AccountPage',
+      component: getParentLayout('AccountPage'),
       redirect: '/page-demo/account/setting',
       meta: {
         title: 'routes.demo.page.account',
@@ -139,8 +144,9 @@ const page: AppRouteModule = {
     // =============================account end=============================
     // =============================exception start=============================
     {
-      path: '/exception',
+      path: 'exception',
       name: 'ExceptionPage',
+      component: getParentLayout('ExceptionPage'),
       redirect: '/page-demo/exception/404',
       meta: {
         title: 'routes.demo.page.exception',
@@ -211,8 +217,9 @@ const page: AppRouteModule = {
     // =============================exception end=============================
     // =============================list start=============================
     {
-      path: '/list',
+      path: 'list',
       name: 'ListPage',
+      component: getParentLayout('ListPage'),
       redirect: '/page-demo/list/card',
       meta: {
         title: 'routes.demo.page.list',

+ 13 - 13
src/router/routes/modules/demo/permission.ts

@@ -1,24 +1,23 @@
 import type { AppRouteModule } from '/@/router/types';
 
-import { PAGE_LAYOUT_COMPONENT } from '/@/router/constant';
+import { getParentLayout, LAYOUT } from '/@/router/constant';
 import { RoleEnum } from '/@/enums/roleEnum';
 
 const permission: AppRouteModule = {
-  layout: {
-    path: '/permission',
-    name: 'Permission',
-    component: PAGE_LAYOUT_COMPONENT,
-    redirect: '/permission/front/page',
-    meta: {
-      icon: 'carbon:user-role',
-      title: 'routes.demo.permission.permission',
-    },
+  path: '/permission',
+  name: 'Permission',
+  component: LAYOUT,
+  redirect: '/permission/front/page',
+  meta: {
+    icon: 'carbon:user-role',
+    title: 'routes.demo.permission.permission',
   },
 
-  routes: [
+  children: [
     {
-      path: '/front',
+      path: 'front',
       name: 'PermissionFrontDemo',
+      component: getParentLayout('PermissionFrontDemo'),
       meta: {
         title: 'routes.demo.permission.front',
       },
@@ -60,8 +59,9 @@ const permission: AppRouteModule = {
       ],
     },
     {
-      path: '/back',
+      path: 'back',
       name: 'PermissionBackDemo',
+      component: getParentLayout('PermissionBackDemo'),
       meta: {
         title: 'routes.demo.permission.back',
       },

+ 24 - 26
src/router/routes/modules/demo/table.ts

@@ -1,22 +1,20 @@
 import type { AppRouteModule } from '/@/router/types';
 
-import { PAGE_LAYOUT_COMPONENT } from '/@/router/constant';
+import { LAYOUT } from '/@/router/constant';
 
 const table: AppRouteModule = {
-  layout: {
-    path: '/table',
-    name: 'TableDemo',
-    component: PAGE_LAYOUT_COMPONENT,
-    redirect: '/table/basic',
-    meta: {
-      icon: 'ant-design:table-outlined',
-      title: 'routes.demo.table.table',
-    },
+  path: '/table',
+  name: 'TableDemo',
+  component: LAYOUT,
+  redirect: '/table/basic',
+  meta: {
+    icon: 'ant-design:table-outlined',
+    title: 'routes.demo.table.table',
   },
 
-  routes: [
+  children: [
     {
-      path: '/basic',
+      path: 'basic',
       name: 'TableBasicDemo',
       component: () => import('/@/views/demo/table/Basic.vue'),
       meta: {
@@ -24,7 +22,7 @@ const table: AppRouteModule = {
       },
     },
     {
-      path: '/treeTable',
+      path: 'treeTable',
       name: 'TreeTableDemo',
       component: () => import('/@/views/demo/table/TreeTable.vue'),
       meta: {
@@ -32,7 +30,7 @@ const table: AppRouteModule = {
       },
     },
     {
-      path: '/fetchTable',
+      path: 'fetchTable',
       name: 'FetchTableDemo',
       component: () => import('/@/views/demo/table/FetchTable.vue'),
       meta: {
@@ -40,7 +38,7 @@ const table: AppRouteModule = {
       },
     },
     {
-      path: '/fixedColumn',
+      path: 'fixedColumn',
       name: 'FixedColumnDemo',
       component: () => import('/@/views/demo/table/FixedColumn.vue'),
       meta: {
@@ -48,7 +46,7 @@ const table: AppRouteModule = {
       },
     },
     {
-      path: '/customerCell',
+      path: 'customerCell',
       name: 'CustomerCellDemo',
       component: () => import('/@/views/demo/table/CustomerCell.vue'),
       meta: {
@@ -56,7 +54,7 @@ const table: AppRouteModule = {
       },
     },
     {
-      path: '/formTable',
+      path: 'formTable',
       name: 'FormTableDemo',
       component: () => import('/@/views/demo/table/FormTable.vue'),
       meta: {
@@ -64,7 +62,7 @@ const table: AppRouteModule = {
       },
     },
     {
-      path: '/useTable',
+      path: 'useTable',
       name: 'UseTableDemo',
       component: () => import('/@/views/demo/table/UseTable.vue'),
       meta: {
@@ -72,7 +70,7 @@ const table: AppRouteModule = {
       },
     },
     {
-      path: '/refTable',
+      path: 'refTable',
       name: 'RefTableDemo',
       component: () => import('/@/views/demo/table/RefTable.vue'),
       meta: {
@@ -80,7 +78,7 @@ const table: AppRouteModule = {
       },
     },
     {
-      path: '/multipleHeader',
+      path: 'multipleHeader',
       name: 'MultipleHeaderDemo',
       component: () => import('/@/views/demo/table/MultipleHeader.vue'),
       meta: {
@@ -88,7 +86,7 @@ const table: AppRouteModule = {
       },
     },
     {
-      path: '/mergeHeader',
+      path: 'mergeHeader',
       name: 'MergeHeaderDemo',
       component: () => import('/@/views/demo/table/MergeHeader.vue'),
       meta: {
@@ -96,7 +94,7 @@ const table: AppRouteModule = {
       },
     },
     {
-      path: '/expandTable',
+      path: 'expandTable',
       name: 'ExpandTableDemo',
       component: () => import('/@/views/demo/table/ExpandTable.vue'),
       meta: {
@@ -104,7 +102,7 @@ const table: AppRouteModule = {
       },
     },
     {
-      path: '/fixedHeight',
+      path: 'fixedHeight',
       name: 'FixedHeightDemo',
       component: () => import('/@/views/demo/table/FixedHeight.vue'),
       meta: {
@@ -112,7 +110,7 @@ const table: AppRouteModule = {
       },
     },
     {
-      path: '/footerTable',
+      path: 'footerTable',
       name: 'FooterTableDemo',
       component: () => import('/@/views/demo/table/FooterTable.vue'),
       meta: {
@@ -120,7 +118,7 @@ const table: AppRouteModule = {
       },
     },
     {
-      path: '/editCellTable',
+      path: 'editCellTable',
       name: 'EditCellTableDemo',
       component: () => import('/@/views/demo/table/EditCellTable.vue'),
       meta: {
@@ -128,7 +126,7 @@ const table: AppRouteModule = {
       },
     },
     {
-      path: '/editRowTable',
+      path: 'editRowTable',
       name: 'EditRowTableDemo',
       component: () => import('/@/views/demo/table/EditRowTable.vue'),
       meta: {

+ 12 - 14
src/router/routes/modules/demo/tree.ts

@@ -1,21 +1,19 @@
 import type { AppRouteModule } from '/@/router/types';
 
-import { PAGE_LAYOUT_COMPONENT } from '/@/router/constant';
+import { LAYOUT } from '/@/router/constant';
 
 const tree: AppRouteModule = {
-  layout: {
-    path: '/tree',
-    name: 'TreeDemo',
-    component: PAGE_LAYOUT_COMPONENT,
-    redirect: '/tree/basic',
-    meta: {
-      icon: 'clarity:tree-view-line',
-      title: 'routes.demo.tree.tree',
-    },
+  path: '/tree',
+  name: 'TreeDemo',
+  component: LAYOUT,
+  redirect: '/tree/basic',
+  meta: {
+    icon: 'clarity:tree-view-line',
+    title: 'routes.demo.tree.tree',
   },
-  routes: [
+  children: [
     {
-      path: '/basic',
+      path: 'basic',
       name: 'BasicTreeDemo',
       component: () => import('/@/views/demo/tree/index.vue'),
       meta: {
@@ -23,7 +21,7 @@ const tree: AppRouteModule = {
       },
     },
     {
-      path: '/editTree',
+      path: 'editTree',
       name: 'EditTreeDemo',
       component: () => import('/@/views/demo/tree/EditTree.vue'),
       meta: {
@@ -31,7 +29,7 @@ const tree: AppRouteModule = {
       },
     },
     {
-      path: '/actionTree',
+      path: 'actionTree',
       name: 'ActionTreeDemo',
       component: () => import('/@/views/demo/tree/ActionTree.vue'),
       meta: {

+ 28 - 0
src/router/routes/modules/home.ts

@@ -0,0 +1,28 @@
+import type { AppRouteModule } from '/@/router/types';
+
+import { LAYOUT } from '/@/router/constant';
+
+const dashboard: AppRouteModule = {
+  path: '/home',
+  name: 'Home',
+  component: LAYOUT,
+  redirect: '/home/welcome',
+  meta: {
+    icon: 'ant-design:home-outlined',
+    title: 'routes.dashboard.welcome',
+  },
+  children: [
+    {
+      path: 'welcome',
+      name: 'Welcome',
+      component: () => import('/@/views/dashboard/welcome/index.vue'),
+      meta: {
+        title: 'routes.dashboard.welcome',
+        affix: true,
+        icon: 'ant-design:home-outlined',
+      },
+    },
+  ],
+};
+
+export default dashboard;

+ 14 - 13
src/router/types.d.ts

@@ -1,5 +1,6 @@
 import type { RouteRecordRaw } from 'vue-router';
 import { RoleEnum } from '/@/enums/roleEnum';
+import Component from '/@/components/types';
 export interface RouteMeta {
   // title
   title: string;
@@ -24,24 +25,23 @@ export interface RouteMeta {
   // Whether the route has been dynamically added
   hideBreadcrumb?: boolean;
 
-  // disabled redirect
-  disabledRedirect?: boolean;
-
   // close loading
   afterCloseLoading?: boolean;
   // Is it in the tab
   inTab?: boolean;
   // Carrying parameters
   carryParam?: boolean;
+
+  single?: boolean;
 }
 
 export interface AppRouteRecordRaw extends Omit<RouteRecordRaw, 'meta'> {
   name: string;
   meta: RouteMeta;
-  component?: any;
-  components?: any;
+  component?: Component;
+  components?: Component;
   children?: AppRouteRecordRaw[];
-  props?: any;
+  props?: Record<string, any>;
   fullPath?: string;
 }
 export interface MenuTag {
@@ -75,11 +75,12 @@ export interface MenuModule {
   menu: Menu;
 }
 
-interface RouteModule {
-  layout: AppRouteRecordRaw;
-  routes: AppRouteRecordRaw[];
-  children?: AppRouteRecordRaw[];
-  component?: any;
-}
+// interface RouteModule {
+//   layout: AppRouteRecordRaw;
+//   routes: AppRouteRecordRaw[];
+//   children?: AppRouteRecordRaw[];
+//   component?: Component;
+// }
 
-export type AppRouteModule = RouteModule | AppRouteRecordRaw;
+// export type AppRouteModule = RouteModule | AppRouteRecordRaw;
+export type AppRouteModule = AppRouteRecordRaw;

+ 8 - 12
src/store/modules/permission.ts

@@ -1,4 +1,3 @@
-import { REDIRECT_ROUTE } from '/@/router/constant';
 import type { AppRouteRecordRaw, Menu } from '/@/router/types';
 import store from '/@/store/index';
 import { hotModuleUnregisterModule } from '/@/utils/helper/vuexHelper';
@@ -15,15 +14,13 @@ import { filter } from '/@/utils/helper/treeHelper';
 import { toRaw } from 'vue';
 import { getMenuListById } from '/@/api/sys/menu';
 
-import { genRouteModule, transformObjToRoute } from '/@/utils/helper/routeHelper';
-import { transformRouteToMenu } from '/@/utils/helper/menuHelper';
+import { transformObjToRoute } from '/@/router/helper/routeHelper';
+import { transformRouteToMenu } from '/@/router/helper/menuHelper';
 
 import { useMessage } from '/@/hooks/web/useMessage';
 // import { warn } from '/@/utils/log';
 import { useI18n } from '/@/hooks/web/useI18n';
 
-const { t } = useI18n();
-
 const { createMessage } = useMessage();
 const NAME = 'permission';
 hotModuleUnregisterModule(NAME);
@@ -87,6 +84,7 @@ class Permission extends VuexModule {
 
   @Action
   async buildRoutesAction(id?: number | string): Promise<AppRouteRecordRaw[]> {
+    const { t } = useI18n();
     let routes: AppRouteRecordRaw[] = [];
     const roleList = toRaw(userStore.getRoleListState);
 
@@ -95,17 +93,15 @@ class Permission extends VuexModule {
     // role permissions
     if (permissionMode === PermissionModeEnum.ROLE) {
       routes = filter(asyncRoutes, (route) => {
-        const { meta } = route;
-        const { roles } = meta!;
+        const { meta } = route as AppRouteRecordRaw;
+        const { roles } = meta || {};
         if (!roles) return true;
         return roleList.some((role) => roles.includes(role));
       });
       //  如果确定不需要做后台动态权限,请将下面整个判断注释
     } else if (permissionMode === PermissionModeEnum.BACK) {
-      const messageKey = 'loadMenu';
       createMessage.loading({
         content: t('sys.app.menuLoading'),
-        key: messageKey,
         duration: 1,
       });
       // 这里获取后台路由菜单逻辑自行修改
@@ -118,10 +114,10 @@ class Permission extends VuexModule {
       routeList = transformObjToRoute(routeList);
       //  后台路由转菜单结构
       const backMenuList = transformRouteToMenu(routeList);
+
       this.commitBackMenuListState(backMenuList);
-      // 生成路由
-      routes = genRouteModule(routeList) as AppRouteRecordRaw[];
-      routes.push(REDIRECT_ROUTE);
+
+      routes = routeList;
     }
     return routes;
   }

+ 193 - 114
src/store/modules/tab.ts

@@ -1,53 +1,43 @@
-import { computed, toRaw } from 'vue';
-import type { AppRouteRecordRaw, RouteMeta } from '/@/router/types.d';
+import { toRaw } from 'vue';
 
 import { unref } from 'vue';
 import { Action, Module, Mutation, VuexModule, getModule } from 'vuex-module-decorators';
 import { hotModuleUnregisterModule } from '/@/utils/helper/vuexHelper';
 
 import { PageEnum } from '/@/enums/pageEnum';
-import { appStore } from '/@/store/modules/app';
 import { userStore } from './user';
 
 import store from '/@/store';
 import router from '/@/router';
 import { PAGE_NOT_FOUND_ROUTE, REDIRECT_ROUTE } from '/@/router/constant';
-import { getCurrentTo } from '/@/utils/helper/routeHelper';
+import { RouteLocationNormalized, RouteLocationRaw } from 'vue-router';
+import { getRoute } from '/@/router/helper/routeHelper';
+import { useGo, useRedo } from '/@/hooks/web/usePage';
 
-type CacheName = string | symbol | null | undefined;
-
-/**
- * @description:  vuex Tab模块
- */
 // declare namespace TabsStore {
-export interface TabItem {
-  fullPath: string;
-  path?: string;
-  params?: any;
-  query?: any;
-  name?: CacheName;
-  meta?: RouteMeta;
-}
 
 const NAME = 'tab';
 
 hotModuleUnregisterModule(NAME);
 
-const getOpenKeepAliveRef = computed(() => appStore.getProjectConfig.openKeepAlive);
+export const PAGE_LAYOUT_KEY = '__PAGE_LAYOUT__';
+
+function isGotoPage() {
+  const go = useGo();
+  go(unref(router.currentRoute).path, true);
+}
 
 @Module({ namespaced: true, name: NAME, dynamic: true, store })
 class Tab extends VuexModule {
-  // tab list
-  tabsState: TabItem[] = [];
-  // tab cache list
-  keepAliveTabsState: CacheName[] = [];
+  cachedMapState = new Map<string, string[]>();
 
-  currentContextMenuIndexState = -1;
-
-  currentContextMenuState: TabItem | null = null;
+  // tab list
+  tabsState: RouteLocationNormalized[] = [];
 
   // Last route change
-  lastChangeRouteState: AppRouteRecordRaw | null = null;
+  lastChangeRouteState: RouteLocationNormalized | null = null;
+
+  lastDragEndIndexState = 0;
 
   get getTabsState() {
     return this.tabsState;
@@ -57,56 +47,93 @@ class Tab extends VuexModule {
     return this.lastChangeRouteState;
   }
 
-  get getCurrentContextMenuIndexState() {
-    return this.currentContextMenuIndexState;
-  }
-
-  get getCurrentContextMenuState() {
-    return this.currentContextMenuState;
+  get getCurrentTab(): RouteLocationNormalized {
+    const route = unref(router.currentRoute);
+    return this.tabsState.find((item) => item.path === route.path)!;
   }
 
-  get getKeepAliveTabsState() {
-    return this.keepAliveTabsState;
+  get getCachedMapState(): Map<string, string[]> {
+    return this.cachedMapState;
   }
 
-  get getCurrentTab(): TabItem {
-    const route = unref(router.currentRoute);
-    return this.tabsState.find((item) => item.path === route.path)!;
+  get getLastDragEndIndexState(): number {
+    return this.lastDragEndIndexState;
   }
 
   @Mutation
-  commitLastChangeRouteState(route: AppRouteRecordRaw): void {
+  commitLastChangeRouteState(route: RouteLocationNormalized): void {
     if (!userStore.getTokenState) return;
     this.lastChangeRouteState = route;
   }
 
   @Mutation
   commitClearCache(): void {
-    this.keepAliveTabsState = [];
+    this.cachedMapState = new Map();
   }
 
   @Mutation
-  commitCurrentContextMenuIndexState(index: number): void {
-    this.currentContextMenuIndexState = index;
+  goToPage() {
+    const go = useGo();
+    const len = this.tabsState.length;
+    const { path } = unref(router.currentRoute);
+
+    let toPath: PageEnum | string = PageEnum.BASE_HOME;
+
+    if (len > 0) {
+      const page = this.tabsState[len - 1];
+      const p = page.fullPath || page.path;
+      if (p) {
+        toPath = p;
+      }
+    }
+    // Jump to the current page and report an error
+    path !== toPath && go(toPath as PageEnum, true);
   }
 
   @Mutation
-  commitCurrentContextMenuState(item: TabItem): void {
-    this.currentContextMenuState = item;
+  commitCachedMapState(): void {
+    const cacheMap = new Map<string, string[]>();
+
+    const pageCacheSet = new Set<string>();
+    this.tabsState.forEach((tab) => {
+      const item = getRoute(tab);
+      const needAuth = !item.meta.ignoreAuth;
+      if (item.meta.affix) {
+        const name = item.name as string;
+        pageCacheSet.add(name);
+      } else if (item.matched && needAuth) {
+        const matched = item.matched;
+        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 || needAuth)) {
+              const mapList = cacheMap.get(key) || [];
+              if (!mapList.includes(name as string)) {
+                mapList.push(name as string);
+              }
+              cacheMap.set(key, mapList);
+            }
+          }
+        }
+      }
+    });
+
+    cacheMap.set(PAGE_LAYOUT_KEY, Array.from(pageCacheSet));
+    this.cachedMapState = cacheMap;
   }
 
-  /**
-   * @description: add tab
-   */
   @Mutation
-  commitAddTab(route: AppRouteRecordRaw | TabItem): void {
-    const { path, name, meta, fullPath, params, query } = route as TabItem;
-    // 404  页面不需要添加tab
-    if (path === PageEnum.ERROR_PAGE || !name) {
-      return;
-    } else if ([REDIRECT_ROUTE.name, PAGE_NOT_FOUND_ROUTE.name].includes(name as string)) {
-      return;
-    }
+  commitTabRoutesState(route: RouteLocationNormalized) {
+    const { path, fullPath, params, query } = route;
 
     let updateIndex = -1;
     // 已经存在的页面,不重复添加tab
@@ -123,39 +150,18 @@ class Tab extends VuexModule {
       this.tabsState.splice(updateIndex, 1, curTab);
       return;
     }
-    this.tabsState.push({ path, fullPath, name, meta, params, query });
-    if (unref(getOpenKeepAliveRef) && name) {
-      const noKeepAlive = meta && meta.ignoreKeepAlive;
-      const hasName = this.keepAliveTabsState.includes(name);
-      !noKeepAlive && !hasName && this.keepAliveTabsState.push(name);
-    }
+    this.tabsState.push(route);
   }
 
   /**
    * @description: close tab
    */
   @Mutation
-  commitCloseTab(route: AppRouteRecordRaw | TabItem): void {
-    try {
-      const { fullPath, name, meta: { affix } = {} } = route;
-      if (affix) return;
-      const index = this.tabsState.findIndex((item) => item.fullPath === fullPath);
-      index !== -1 && this.tabsState.splice(index, 1);
-
-      if (unref(getOpenKeepAliveRef) && name) {
-        const i = this.keepAliveTabsState.findIndex((item) => item === name);
-        i !== -1 && this.keepAliveTabsState.splice(i, 1);
-      }
-    } catch (error) {}
-  }
-
-  @Mutation
-  commitCloseTabKeepAlive(route: AppRouteRecordRaw | TabItem): void {
-    const { name } = route;
-    if (unref(getOpenKeepAliveRef) && name) {
-      const i = this.keepAliveTabsState.findIndex((item) => item === name);
-      i !== -1 && toRaw(this.keepAliveTabsState).splice(i, 1);
-    }
+  commitCloseTab(route: RouteLocationNormalized): void {
+    const { fullPath, meta: { affix } = {} } = route;
+    if (affix) return;
+    const index = this.tabsState.findIndex((item) => item.fullPath === fullPath);
+    index !== -1 && this.tabsState.splice(index, 1);
   }
 
   @Mutation
@@ -163,16 +169,12 @@ class Tab extends VuexModule {
     this.tabsState = this.tabsState.filter((item) => {
       return item.meta && item.meta.affix;
     });
-    const names = this.tabsState.map((item) => item.name);
-    this.keepAliveTabsState = names as string[];
   }
 
   @Mutation
   commitResetState(): void {
     this.tabsState = [];
-    this.currentContextMenuState = null;
-    this.currentContextMenuIndexState = -1;
-    this.keepAliveTabsState = [];
+    this.cachedMapState = new Map();
   }
 
   @Mutation
@@ -181,73 +183,149 @@ class Tab extends VuexModule {
 
     this.tabsState.splice(oldIndex, 1);
     this.tabsState.splice(newIndex, 0, currentTab);
+    this.lastDragEndIndexState = this.lastDragEndIndexState + 1;
   }
 
   @Mutation
-  closeMultipleTab({ pathList, nameList }: { pathList: string[]; nameList: string[] }): void {
+  closeMultipleTab({ pathList }: { pathList: string[] }): void {
     this.tabsState = toRaw(this.tabsState).filter((item) => !pathList.includes(item.fullPath));
-    if (unref(getOpenKeepAliveRef) && nameList) {
-      this.keepAliveTabsState = toRaw(this.keepAliveTabsState).filter(
-        (item) => !nameList.includes(item as string)
-      );
+  }
+
+  @Action
+  addTabAction(route: RouteLocationNormalized) {
+    const { path, name } = route;
+    // 404  页面不需要添加tab
+    if (
+      path === PageEnum.ERROR_PAGE ||
+      !name ||
+      [REDIRECT_ROUTE.name, PAGE_NOT_FOUND_ROUTE.name].includes(name as string)
+    ) {
+      return;
+    }
+    this.commitTabRoutesState(getRoute(route));
+
+    this.commitCachedMapState();
+  }
+
+  @Mutation
+  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 redo = useRedo();
+    redo();
+  }
+
+  @Action
+  closeAllTabAction() {
+    this.commitCloseAllTab();
+    this.commitClearCache();
+    this.goToPage();
+  }
+
+  @Action
+  closeTabAction(tab: RouteLocationNormalized) {
+    function getObj(tabItem: RouteLocationNormalized) {
+      const { params, path, query } = tabItem;
+      return {
+        params: params || {},
+        path,
+        query: query || {},
+      };
+    }
+    const { currentRoute, replace } = router;
+
+    const { path } = unref(currentRoute);
+    if (path !== tab.path) {
+      // Closed is not the activation tab
+      this.commitCloseTab(tab);
+      return;
+    }
+
+    // Closed is activated atb
+    let toObj: RouteLocationRaw = {};
+
+    const index = this.getTabsState.findIndex((item) => item.path === path);
+
+    // If the current is the leftmost tab
+    if (index === 0) {
+      // There is only one tab, then jump to the homepage, otherwise jump to the right tab
+      if (this.getTabsState.length === 1) {
+        toObj = PageEnum.BASE_HOME;
+      } else {
+        //  Jump to the right tab
+        const page = this.getTabsState[index + 1];
+        toObj = getObj(page);
+      }
+    } else {
+      // Close the current tab
+      const page = this.getTabsState[index - 1];
+      toObj = getObj(page);
     }
+    this.commitCloseTab(currentRoute.value);
+    replace(toObj);
   }
 
   @Action
-  closeLeftTabAction(route: AppRouteRecordRaw | TabItem): void {
+  closeTabByKeyAction(key: string) {
+    const index = this.tabsState.findIndex((item) => (item.fullPath || item.path) === key);
+    index !== -1 && this.closeTabAction(this.tabsState[index]);
+  }
+
+  @Action
+  closeLeftTabAction(route: RouteLocationNormalized): void {
     const index = this.tabsState.findIndex((item) => item.path === route.path);
 
     if (index > 0) {
       const leftTabs = this.tabsState.slice(0, index);
       const pathList: string[] = [];
-      const nameList: string[] = [];
       for (const item of leftTabs) {
         const affix = item.meta ? item.meta.affix : false;
         if (!affix) {
           pathList.push(item.fullPath);
-          nameList.push(item.name as string);
         }
       }
-      this.closeMultipleTab({ pathList, nameList });
-    }
-  }
-
-  @Action
-  addTabByPathAction(): void {
-    const toRoute = getCurrentTo();
-    if (!toRoute) return;
-    const { meta } = toRoute;
-    if (meta && meta.affix) {
-      return;
+      this.closeMultipleTab({ pathList });
     }
-    this.commitAddTab((toRoute as unknown) as AppRouteRecordRaw);
+    this.commitCachedMapState();
+    isGotoPage();
   }
 
   @Action
-  closeRightTabAction(route: AppRouteRecordRaw | TabItem): void {
+  closeRightTabAction(route: RouteLocationNormalized): void {
     const index = this.tabsState.findIndex((item) => item.fullPath === route.fullPath);
 
     if (index >= 0 && index < this.tabsState.length - 1) {
       const rightTabs = this.tabsState.slice(index + 1, this.tabsState.length);
 
       const pathList: string[] = [];
-      const nameList: string[] = [];
       for (const item of rightTabs) {
         const affix = item.meta ? item.meta.affix : false;
         if (!affix) {
           pathList.push(item.fullPath);
-          nameList.push(item.name as string);
         }
       }
-      this.closeMultipleTab({ pathList, nameList });
+      this.closeMultipleTab({ pathList });
     }
+    this.commitCachedMapState();
+    isGotoPage();
   }
 
   @Action
-  closeOtherTabAction(route: AppRouteRecordRaw | TabItem): void {
+  closeOtherTabAction(route: RouteLocationNormalized): void {
     const closePathList = this.tabsState.map((item) => item.fullPath);
     const pathList: string[] = [];
-    const nameList: string[] = [];
     closePathList.forEach((path) => {
       if (path !== route.fullPath) {
         const closeItem = this.tabsState.find((item) => item.path === path);
@@ -255,11 +333,12 @@ class Tab extends VuexModule {
         const affix = closeItem.meta ? closeItem.meta.affix : false;
         if (!affix) {
           pathList.push(closeItem.fullPath);
-          nameList.push(closeItem.name as string);
         }
       }
     });
-    this.closeMultipleTab({ pathList, nameList });
+    this.closeMultipleTab({ pathList });
+    this.commitCachedMapState();
+    isGotoPage();
   }
 }
 export const tabStore = getModule<Tab>(Tab);

+ 0 - 5
src/utils/helper/dynamicImport.ts

@@ -1,5 +0,0 @@
-// The content here is just for type approval. The actual file content is overwritten by transform
-export default function (id: string) {
-  const dynamicImportModule: any = id;
-  return dynamicImportModule;
-}

+ 0 - 110
src/utils/helper/routeHelper.ts

@@ -1,110 +0,0 @@
-import type { AppRouteModule, AppRouteRecordRaw, RouteModule } from '/@/router/types';
-import type { RouteLocationNormalized, RouteRecordRaw } from 'vue-router';
-import { createRouter, createWebHashHistory } from 'vue-router';
-
-import { appStore } from '/@/store/modules/app';
-import { tabStore } from '/@/store/modules/tab';
-import { toRaw } from 'vue';
-import { PAGE_LAYOUT_COMPONENT } from '/@/router/constant';
-// import { isDevMode } from '/@/utils/env';
-import dynamicImport from './dynamicImport';
-import { omit } from 'lodash-es';
-
-let currentTo: RouteLocationNormalized | null = null;
-
-export function getCurrentTo() {
-  return currentTo;
-}
-
-export function setCurrentTo(to: RouteLocationNormalized) {
-  currentTo = to;
-}
-// 转化路由模块
-// 将多级转成2层。keepAlive问题
-export function genRouteModule(moduleList: AppRouteModule[] | AppRouteRecordRaw[]) {
-  const ret: AppRouteRecordRaw[] = [];
-  for (const routeMod of moduleList) {
-    let routes: RouteRecordRaw[] = [];
-    let layout: AppRouteRecordRaw | undefined;
-    if (Reflect.has(routeMod, 'routes')) {
-      routes = (routeMod as RouteModule).routes as any;
-      layout = (routeMod as RouteModule).layout;
-    } else if (Reflect.has(routeMod, 'path')) {
-      layout = omit(routeMod, 'children') as any;
-      routes = (routeMod.children as RouteRecordRaw[]) || ([] as RouteRecordRaw[]);
-    }
-
-    const router = createRouter({ routes, history: createWebHashHistory() });
-
-    const flatList = (toRaw(router.getRoutes()).filter(
-      (item) => item.children.length === 0
-    ) as unknown) as AppRouteRecordRaw[];
-    flatList.forEach((item) => {
-      item.path = `${layout ? layout.path : ''}${item.path}`;
-    });
-    if (layout) {
-      layout.children = flatList;
-      ret.push(layout);
-    } else {
-      ret.push(...flatList);
-    }
-  }
-  return ret as RouteRecordRaw[];
-}
-
-// 动态引入
-function asyncImportRoute(routes: AppRouteRecordRaw[] | undefined) {
-  if (!routes) return;
-  routes.forEach((item) => {
-    const { component } = item;
-    const { children } = item;
-    if (component) {
-      item.component = dynamicImport(component);
-    }
-
-    children && asyncImportRoute(children);
-  });
-}
-
-function getLayoutComp(comp: string) {
-  return comp === 'PAGE_LAYOUT' ? PAGE_LAYOUT_COMPONENT : '';
-}
-
-// 将后台对象转成路由对象
-export function transformObjToRoute<T = any>(routeList: AppRouteModule[]): T[] {
-  routeList.forEach((route) => {
-    asyncImportRoute(
-      Reflect.has(route, 'routes') ? (route as RouteModule).routes : route.children || []
-    );
-    if ((route as RouteModule).layout) {
-      (route as RouteModule).layout.component = getLayoutComp(
-        (route as RouteModule).layout.component
-      );
-    } else {
-      route.component = getLayoutComp(route.component);
-      (route as RouteModule).layout = omit(route, 'children') as any;
-    }
-  });
-  return (routeList as unknown) as T[];
-}
-
-//
-export function getIsOpenTab(toPath: string) {
-  const { openKeepAlive, multiTabsSetting: { show } = {} } = appStore.getProjectConfig;
-
-  if (show && openKeepAlive) {
-    const tabList = tabStore.getTabsState;
-    return tabList.some((tab) => tab.path === toPath);
-  }
-  return false;
-}
-
-export function getParams(data: any = {}) {
-  const { params = {} } = data;
-  let ret = '';
-  Object.keys(params).forEach((key) => {
-    const p = params[key];
-    ret += `/${p}`;
-  });
-  return ret;
-}

+ 1 - 0
src/views/demo/feat/copy/index.vue

@@ -15,6 +15,7 @@
   import { useMessage } from '/@/hooks/web/useMessage';
 
   export default defineComponent({
+    name: 'Copy',
     components: { CollapseContainer },
     setup() {
       const valueRef = ref('');

+ 11 - 0
src/views/demo/level/Menu111.vue

@@ -0,0 +1,11 @@
+<template>
+  <div class="p-5">
+    多层级缓存-页面1-1-1
+    <br />
+    <input />
+  </div>
+</template>
+<script lang="ts">
+  import { defineComponent } from 'vue';
+  export default defineComponent({ name: 'Menu111Demo' });
+</script>

+ 11 - 0
src/views/demo/level/Menu12.vue

@@ -0,0 +1,11 @@
+<template>
+  <div class="p-5">
+    多层级缓存-页面1-2
+    <br />
+    <input />
+  </div>
+</template>
+<script lang="ts">
+  import { defineComponent } from 'vue';
+  export default defineComponent({ name: 'Menu12Demo' });
+</script>

+ 13 - 0
src/views/demo/level/Menu2.vue

@@ -0,0 +1,13 @@
+<template>
+  <div class="p-5">
+    多层级缓存-页面2
+    <br />
+    <input />
+  </div>
+</template>
+<script lang="ts">
+  import { defineComponent } from 'vue';
+  export default defineComponent({
+    name: 'Menu2Demo',
+  });
+</script>

+ 5 - 1
src/views/sys/redirect/index.vue

@@ -1,3 +1,6 @@
+<template>
+  <div />
+</template>
 <script lang="ts">
   import { defineComponent, unref } from 'vue';
 
@@ -18,12 +21,13 @@
         path: '/' + _path,
         query,
       });
+      // close loading
       if (unref(getEnableTransition) && unref(getOpenPageLoading)) {
         setTimeout(() => {
           appStore.setPageLoadingAction(false);
         }, 0);
       }
-      return () => null;
+      return {};
     },
   });
 </script>

+ 115 - 23
yarn.lock

@@ -1061,10 +1061,10 @@
   resolved "https://registry.npmjs.org/@iconify/iconify/-/iconify-2.0.0-rc.2.tgz#c4a95ddc06ca9b9496df03604e66fdefb39f4c4b"
   integrity sha512-BybEHU5/I9EQ0CcwKAqmreZ2bMnAXrqLCTptAc6vPetHMbrXdZfejP5mt57e/8PNSt/qE7BHniU5PCYA+PGIHw==
 
-"@iconify/json@^1.1.266":
-  version "1.1.266"
-  resolved "https://registry.npmjs.org/@iconify/json/-/json-1.1.266.tgz#3537de808399652b3ca2c89a561216324121b785"
-  integrity sha512-I8S9lChQATaRroMGccdOQkFbBtMt4C2V/PQGiSjDq9yzdyqDCrPNN9X1qM4FoQt84zfW/+JMHIgShi42E+SXeA==
+"@iconify/json@^1.1.267":
+  version "1.1.267"
+  resolved "https://registry.npmjs.org/@iconify/json/-/json-1.1.267.tgz#52ab5390fcaf95e0d68260523a3a3fbc575dfe01"
+  integrity sha512-VKNvyALvbuwsXO7r2XvdoqdctmvJzp1/XYOXRfhJ4w+sjtWYp8T3oRGDJ0AZTafzGiBBUaMwCZVP+j87rqgD3w==
 
 "@koa/cors@^3.1.0":
   version "3.1.0"
@@ -1535,10 +1535,10 @@
   resolved "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d"
   integrity sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw==
 
-"@types/yargs@^15.0.10":
-  version "15.0.10"
-  resolved "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.10.tgz#0fe3c8173a0d5c3e780b389050140c3f5ea6ea74"
-  integrity sha512-z8PNtlhrj7eJNLmrAivM7rjBESG6JwC5xP3RVk12i/8HVP7Xnx/sEmERnRImyEuUaJfO942X0qMOYsoupaJbZQ==
+"@types/yargs@^15.0.11":
+  version "15.0.11"
+  resolved "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.11.tgz#361d7579ecdac1527687bcebf9946621c12ab78c"
+  integrity sha512-jfcNBxHFYJ4nPIacsi3woz1+kvUO6s1CyeEhtnDHBjHUMNj5UlW2GynmnSgiJJEdNg9yW5C8lfoNRZrHGv5EqA==
   dependencies:
     "@types/yargs-parser" "*"
 
@@ -1644,6 +1644,17 @@
     estree-walker "^2.0.1"
     source-map "^0.6.1"
 
+"@vue/compiler-core@3.0.4":
+  version "3.0.4"
+  resolved "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.0.4.tgz#0122aca6eada4cb28b39ed930af917444755e330"
+  integrity sha512-snpMICsbWTZqBFnPB03qr4DtiSxVYfDF3DvbDSkN9Z9NTM8Chl8E/lYhKBSsvauq91DAWAh8PU3lr9vrLyQsug==
+  dependencies:
+    "@babel/parser" "^7.12.0"
+    "@babel/types" "^7.12.0"
+    "@vue/shared" "3.0.4"
+    estree-walker "^2.0.1"
+    source-map "^0.6.1"
+
 "@vue/compiler-dom@3.0.2":
   version "3.0.2"
   resolved "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.0.2.tgz#1d40de04bcdf9aabb79fb6a802dd70a2f3c2992a"
@@ -1660,6 +1671,14 @@
     "@vue/compiler-core" "3.0.3"
     "@vue/shared" "3.0.3"
 
+"@vue/compiler-dom@3.0.4":
+  version "3.0.4"
+  resolved "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.0.4.tgz#834fd4b15c5698cf9f4505c2bfbccca058a843eb"
+  integrity sha512-FOxbHBIkkGjYQeTz1DlXQjS1Ms8EPXQWsdTdTPeohoS0KzCz6RiOjiAG+jLtMi6Nr5GX2h0TlCvcnI8mcsicFQ==
+  dependencies:
+    "@vue/compiler-core" "3.0.4"
+    "@vue/shared" "3.0.4"
+
 "@vue/compiler-sfc@*", "@vue/compiler-sfc@^3.0.0-rc.5":
   version "3.0.2"
   resolved "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.0.2.tgz#22c70fed72c347a4d5fa2db2e80594b3193dce57"
@@ -1704,6 +1723,28 @@
     postcss-selector-parser "^6.0.4"
     source-map "^0.6.1"
 
+"@vue/compiler-sfc@^3.0.4":
+  version "3.0.4"
+  resolved "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.0.4.tgz#2119fe1e68d2c268aafa20461c82c139a9adf8e0"
+  integrity sha512-brDn6HTuK6R3oBCjtMPPsIpyJEZFinlnxjtBXww/goFJOJBAU9CrsdegwyZItNnixCFUIg4CLv4Nj1Eg/eKlfg==
+  dependencies:
+    "@babel/parser" "^7.12.0"
+    "@babel/types" "^7.12.0"
+    "@vue/compiler-core" "3.0.4"
+    "@vue/compiler-dom" "3.0.4"
+    "@vue/compiler-ssr" "3.0.4"
+    "@vue/shared" "3.0.4"
+    consolidate "^0.16.0"
+    estree-walker "^2.0.1"
+    hash-sum "^2.0.0"
+    lru-cache "^5.1.1"
+    magic-string "^0.25.7"
+    merge-source-map "^1.1.0"
+    postcss "^7.0.32"
+    postcss-modules "^3.2.2"
+    postcss-selector-parser "^6.0.4"
+    source-map "^0.6.1"
+
 "@vue/compiler-ssr@3.0.2":
   version "3.0.2"
   resolved "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.0.2.tgz#73af4d274a79bfcc72a996a9b45f1072e7deaa26"
@@ -1720,6 +1761,14 @@
     "@vue/compiler-dom" "3.0.3"
     "@vue/shared" "3.0.3"
 
+"@vue/compiler-ssr@3.0.4":
+  version "3.0.4"
+  resolved "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.0.4.tgz#ccbd1f55734d51d1402fad825ac102002a7a07c7"
+  integrity sha512-4aYWQEL4+LS4+D44K9Z7xMOWMEjBsz4Li9nMcj2rxRQ35ewK6uFPodvs6ORP60iBDSkwUFZoldFlNemQlu1BFw==
+  dependencies:
+    "@vue/compiler-dom" "3.0.4"
+    "@vue/shared" "3.0.4"
+
 "@vue/reactivity@3.0.2":
   version "3.0.2"
   resolved "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.0.2.tgz#42ed5af6025b494a5e69b05169fcddf04eebfe77"
@@ -1734,6 +1783,13 @@
   dependencies:
     "@vue/shared" "3.0.3"
 
+"@vue/reactivity@3.0.4":
+  version "3.0.4"
+  resolved "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.0.4.tgz#b6599dd8271a745960a03f05744ccf7991ba5d8d"
+  integrity sha512-AFTABrLhUYZY2on3ea9FxeXal7w3f6qIp9gT+/oG93H7dFTL5LvVnxygCopv7tvkIl/GSGQb/yK1D1gmXx1Pww==
+  dependencies:
+    "@vue/shared" "3.0.4"
+
 "@vue/runtime-core@3.0.2":
   version "3.0.2"
   resolved "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.0.2.tgz#d7ed462af1cb0bf9836668e4e6fab3f2f4b1bc00"
@@ -1750,6 +1806,14 @@
     "@vue/reactivity" "3.0.3"
     "@vue/shared" "3.0.3"
 
+"@vue/runtime-core@3.0.4":
+  version "3.0.4"
+  resolved "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.0.4.tgz#a5b9a001560b1fd8c01a43f68b764c555de7836c"
+  integrity sha512-qH9e4kqU7b3u1JewvLmGmoAGY+mnuBqz7aEKb2mhpEgwa1yFv496BRuUfMXXMCix3+TndUVMJ8jt41FSdNppwg==
+  dependencies:
+    "@vue/reactivity" "3.0.4"
+    "@vue/shared" "3.0.4"
+
 "@vue/runtime-dom@3.0.3":
   version "3.0.3"
   resolved "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.0.3.tgz#5e3e5e5418b9defcac988d2be0cf65596fa2cc03"
@@ -1759,6 +1823,15 @@
     "@vue/shared" "3.0.3"
     csstype "^2.6.8"
 
+"@vue/runtime-dom@3.0.4":
+  version "3.0.4"
+  resolved "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.0.4.tgz#6f81aec545f24511d2c28a315aa3391420b69c68"
+  integrity sha512-BGIoiTSESzWUhN0Ofi2X/q+HN8f6IUFmUEyyBGKbmx7DTAJNZhFfjqsepfXQrM5IGeTfJLB1ZEVyroDQJNXq3g==
+  dependencies:
+    "@vue/runtime-core" "3.0.4"
+    "@vue/shared" "3.0.4"
+    csstype "^2.6.8"
+
 "@vue/runtime-dom@^3.0.0":
   version "3.0.2"
   resolved "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.0.2.tgz#9d166d03225558025d3d80f5039b646e0051b71c"
@@ -1778,6 +1851,11 @@
   resolved "https://registry.npmjs.org/@vue/shared/-/shared-3.0.3.tgz#ef12ebff93a446df281e8a0fd765b5aea8e7745b"
   integrity sha512-yGgkF7u4W0Dmwri9XdeY50kOowN4UIX7aBQ///jbxx37itpzVjK7QzvD3ltQtPfWaJDGBfssGL0wpAgwX9OJpQ==
 
+"@vue/shared@3.0.4":
+  version "3.0.4"
+  resolved "https://registry.npmjs.org/@vue/shared/-/shared-3.0.4.tgz#6dc50f593bdfdeaa6183d1dbc15e2d45e7c6b8b3"
+  integrity sha512-Swfbz31AaMX48CpFl+YmIrqOH9MgJMTrltG9e26A4ZxYx9LjGuMV+41WnxFzS3Bc9nbrc6sDPM37G6nIT8NJSg==
+
 "@vuedx/analyze@0.2.4-0":
   version "0.2.4-0"
   resolved "https://registry.npmjs.org/@vuedx/analyze/-/analyze-0.2.4-0.tgz#52766a6dcd2867320409fe517540fd0bf0394d48"
@@ -3013,10 +3091,10 @@ crc-32@~1.2.0:
     exit-on-epipe "~1.0.1"
     printj "~1.1.0"
 
-cross-env@^7.0.2:
-  version "7.0.2"
-  resolved "https://registry.npmjs.org/cross-env/-/cross-env-7.0.2.tgz#bd5ed31339a93a3418ac4f3ca9ca3403082ae5f9"
-  integrity sha512-KZP/bMEOJEDCkDQAyRhu3RL2ZO/SUVrxQVI0G3YEQ+OLbRA3c6zgixe8Mq8a/z7+HKlNEjo8oiLUs8iRijY2Rw==
+cross-env@^7.0.3:
+  version "7.0.3"
+  resolved "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf"
+  integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==
   dependencies:
     cross-spawn "^7.0.1"
 
@@ -3449,17 +3527,17 @@ es-module-lexer@^0.3.25:
   resolved "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.3.26.tgz#7b507044e97d5b03b01d4392c74ffeb9c177a83b"
   integrity sha512-Va0Q/xqtrss45hWzP8CZJwzGSZJjDM5/MJRE3IXXnUCcVLElR9BRaE9F62BopysASyc4nM3uwhSW7FFB9nlWAA==
 
-esbuild-register@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.npmjs.org/esbuild-register/-/esbuild-register-1.1.0.tgz#8ec1fbf6b84f0d7654b87eec04029a383dcb539d"
-  integrity sha512-A+KGHDc7me/ATyNqnVQKsHxt2A/ORVvV2gmukx5ZtVcy5HVf19QBbHdfdP5QHFA8rF/WHmcnDxaxewu+VUvUhQ==
+esbuild-register@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.npmjs.org/esbuild-register/-/esbuild-register-1.1.1.tgz#7d50e87ac0b9000085d9e6d9a78e4c2223fcce83"
+  integrity sha512-hAPWuaUkPDLXCENc/AigJZaaDCvCkpmghRw8XPyT+rk08JHcIgUrmw1uabbUTfa6B6J9Wo2bFufb01JjbmzcfQ==
   dependencies:
     joycon "^2.2.5"
     pirates "^4.0.1"
     source-map-support "^0.5.19"
     strip-json-comments "^3.1.1"
 
-esbuild@^0.7.17, esbuild@^0.7.19:
+esbuild@^0.7.19:
   version "0.7.22"
   resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.7.22.tgz#9149b903f8128b7c45a754046c24199d76bbe08e"
   integrity sha512-B43SYg8LGWYTCv9Gs0RnuLNwjzpuWOoCaZHTWEDEf5AfrnuDMerPVMdCEu7xOdhFvQ+UqfP2MGU9lxEy0JzccA==
@@ -3469,6 +3547,11 @@ esbuild@^0.8.12:
   resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.8.15.tgz#cbc4d82a7fc4571d455233456e6fba83fd0364f1"
   integrity sha512-mSaLo9t/oYtQE6FRUEdO47Pr8PisSPzHtgr+LcihIcjBEhbYwjT6WLCQ7noDoTBfIatBCw229rtmIwl9u9UQwg==
 
+esbuild@^0.8.17:
+  version "0.8.17"
+  resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.8.17.tgz#1c16c6d5988dcfdcf27a7e1612b7fd05e1477c54"
+  integrity sha512-ReHap+Iyn5BQF0B8F3xrLwu+j57ri5uDUw2ej9XTPAuFDebYiWwRzBY4jhF610bklveXLbCGim/8/2wQKQlu1w==
+
 escalade@^3.1.1:
   version "3.1.1"
   resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
@@ -3581,13 +3664,13 @@ esm@^3.2.25:
   resolved "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10"
   integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==
 
-esno@^0.2.4:
-  version "0.2.4"
-  resolved "https://registry.npmjs.org/esno/-/esno-0.2.4.tgz#b04a368181bc03e5d11d5147106bf0d0ac4f3a48"
-  integrity sha512-XlgsQe2va257kc1xsZg/X22fRyLVRNkCKEXjONoltA7HeXtmhrQ3n19all0eK0X6YRNE8X9qiVyWV0vMLZvY3w==
+esno@^0.3.0:
+  version "0.3.0"
+  resolved "https://registry.npmjs.org/esno/-/esno-0.3.0.tgz#c818996bdaaf2deaf81413d6f45538ffa6e41b42"
+  integrity sha512-4sF/j8jruQv9jScU8tNkgoDFLjyGxTTB8bmjRmWHyNNygra3WS3X0U1Cc7GuOvfSEjn3NDS57P0LRnzgiupKJg==
   dependencies:
-    esbuild "^0.7.17"
-    esbuild-register "^1.1.0"
+    esbuild "^0.8.17"
+    esbuild-register "^1.1.1"
     esm "^3.2.25"
 
 espree@^6.2.1:
@@ -8335,6 +8418,15 @@ vue@^3.0.3:
     "@vue/runtime-dom" "3.0.3"
     "@vue/shared" "3.0.3"
 
+vue@^3.0.4:
+  version "3.0.4"
+  resolved "https://registry.npmjs.org/vue/-/vue-3.0.4.tgz#872c65c143f5717bd5387c61613d9f55f4cc0f43"
+  integrity sha512-2o+AiQF8sAupyhbyl3oxVCl3WCwC/n5NI7VMM+gVQ231qvSB8eI7sCBloloqDJK6yA367EEtmRSeSCf4sxCC+A==
+  dependencies:
+    "@vue/compiler-dom" "3.0.4"
+    "@vue/runtime-dom" "3.0.4"
+    "@vue/shared" "3.0.4"
+
 vuex-module-decorators@^1.0.1:
   version "1.0.1"
   resolved "https://registry.npmjs.org/vuex-module-decorators/-/vuex-module-decorators-1.0.1.tgz#d34dafb5428a3636f1c26d3d014c15fc9659ccd0"