vben
2020-12-03 c303ec1a23c4b1fbad4fbda9007af2147dc327e2
refactor: refactor route
9个文件已删除
17个文件已添加
56个文件已修改
2 文件已重命名
3107 ■■■■ 已修改文件
.github/workflows/deploy.yml 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.vscode/launch.json 45 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.vscode/settings.json 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
CHANGELOG.zh_CN.md 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
build/vite/plugin/transform/dynamic-import/index.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mock/sys/menu.ts 115 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package.json 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Breadcrumb/index.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Breadcrumb/src/Breadcrumb.vue 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Breadcrumb/src/BreadcrumbItem.vue 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Menu/src/BasicMenu.tsx 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Menu/src/hooks/useOpenKeys.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Menu/src/hooks/useSearchInput.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Table/src/components/TableSetting.vue 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/registerGlobComp.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/design/transition/breadcrumb.less 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/design/transition/index.less 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/enums/pageEnum.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/hooks/web/usePage.ts 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/hooks/web/usePermission.ts 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/hooks/web/useTabs.ts 67 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/default/content/index.tsx 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/default/header/LayoutBreadcrumb.tsx 128 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/default/header/LayoutBreadcrumb.vue 79 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/default/header/LayoutHeader.tsx 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/default/header/LayoutMultipleHeader.less 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/default/header/index.less 56 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/default/multitabs/TabContent.tsx 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/default/multitabs/data.ts 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/default/multitabs/index.tsx 73 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/default/multitabs/types.ts 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/default/multitabs/useMultipleTabs.ts 65 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/default/multitabs/useTabDropdown.ts 282 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/iframe/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/iframe/useFrameKeepAlive.ts 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/page/index.tsx 79 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/page/index.vue 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/parent/index.vue 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/parent/useCache.ts 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/parent/useTransition.ts 补丁 | 查看 | 原始文档 | blame | 历史
src/locales/lang/en/component/form.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/locales/lang/en/layout/header.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/locales/lang/en/routes/demo/level.ts 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/locales/lang/zh_CN/layout/header.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/locales/lang/zh_CN/routes/demo/feat.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/locales/lang/zh_CN/routes/demo/level.ts 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/constant.ts 39 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/guard/index.ts 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/guard/pageLoadingGuard.ts 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/guard/permissionGuard.ts 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/guard/progressGuard.ts 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/helper/dynamicImport.ts 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/helper/menuHelper.ts 13 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/helper/routeHelper.ts 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/menus/index.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/menus/modules/dashboard.ts 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/menus/modules/demo/level.ts 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/menus/modules/home.ts 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/routes/index.ts 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/routes/modules/dashboard.ts 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/routes/modules/demo/charts.ts 26 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/routes/modules/demo/comp.ts 51 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/routes/modules/demo/editor.ts 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/routes/modules/demo/excel.ts 28 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/routes/modules/demo/feat.ts 45 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/routes/modules/demo/form.ts 35 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/routes/modules/demo/iframe.ts 26 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/routes/modules/demo/level.ts 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/routes/modules/demo/page.ts 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/routes/modules/demo/permission.ts 26 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/routes/modules/demo/table.ts 50 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/routes/modules/demo/tree.ts 26 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/routes/modules/home.ts 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/types.d.ts 27 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/modules/permission.ts 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/modules/tab.ts 319 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/helper/dynamicImport.ts 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/helper/routeHelper.ts 110 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/demo/feat/copy/index.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/demo/level/Menu111.vue 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/demo/level/Menu12.vue 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/demo/level/Menu2.vue 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/sys/redirect/index.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yarn.lock 138 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.github/workflows/deploy.yml
New file
@@ -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
.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"]
    }
  ]
}
.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"
  ]
}
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
build/vite/plugin/transform/dynamic-import/index.ts
@@ -17,8 +17,8 @@
    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 }) {
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 @@
      name: 'FrontPageAuth',
      component: '/demo/permission/front/index',
      meta: {
        title: '页面权限',
        title: 'routes.demo.permission.frontPage',
      },
    },
    {
@@ -43,7 +33,7 @@
      name: 'FrontBtnAuth',
      component: '/demo/permission/front/Btn',
      meta: {
        title: '按钮权限',
        title: 'routes.demo.permission.frontBtn',
      },
    },
    {
@@ -51,7 +41,7 @@
      name: 'FrontAuthPageA',
      component: '/demo/permission/front/AuthPageA',
      meta: {
        title: '权限测试页A',
        title: 'routes.demo.permission.frontTestA',
      },
    },
    {
@@ -59,24 +49,25 @@
      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 @@
      name: 'BackAuthBtn',
      component: '/demo/permission/back/Btn',
      meta: {
        title: '按钮权限',
        title: 'routes.demo.permission.backBtn',
      },
    },
  ],
@@ -92,11 +83,11 @@
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,13 +95,69 @@
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 [
  {
@@ -120,10 +167,10 @@
    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]);
      }
    },
  },
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",
src/components/Breadcrumb/index.ts
File was deleted
src/components/Breadcrumb/src/Breadcrumb.vue
File was deleted
src/components/Breadcrumb/src/BreadcrumbItem.vue
File was deleted
src/components/Menu/src/BasicMenu.tsx
@@ -36,6 +36,7 @@
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 @@
    watch(
      () => currentRoute.value.name,
      (name: string) => {
        if (name === 'Redirect') return;
        if (name === REDIRECT_NAME) return;
        handleMenuChange();
        props.isHorizontal && appStore.getProjectConfig.menuSetting.split && getParentPath();
      }
src/components/Menu/src/hooks/useOpenKeys.ts
@@ -4,7 +4,7 @@
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';
src/components/Menu/src/hooks/useSearchInput.ts
@@ -5,7 +5,7 @@
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;
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 />
src/components/registerGlobComp.ts
@@ -33,6 +33,7 @@
  Empty,
  Avatar,
  Menu,
  Breadcrumb,
} from 'ant-design-vue';
import { getApp } from '/@/setup/App';
@@ -55,6 +56,7 @@
  getApp()
    .use(Select)
    .use(Alert)
    .use(Breadcrumb)
    .use(Checkbox)
    .use(DatePicker)
    .use(Radio)
src/design/transition/breadcrumb.less
File was deleted
src/design/transition/index.less
@@ -4,4 +4,3 @@
@import './slide.less';
@import './scroll.less';
@import './zoom.less';
@import './breadcrumb.less';
src/enums/pageEnum.ts
@@ -2,7 +2,7 @@
  // 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
src/hooks/web/usePage.ts
@@ -1,10 +1,11 @@
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 };
@@ -18,7 +19,7 @@
// 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 @@
 * @description: redo current page
 */
export const useRedo = () => {
  const { push, currentRoute } = useRouter();
  const { push, currentRoute } = router;
  const { query, params } = currentRoute.value;
  function redo() {
    push({
src/hooks/web/usePermission.ts
@@ -7,13 +7,14 @@
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 @@
          ? PermissionModeEnum.ROLE
          : PermissionModeEnum.BACK,
    });
    resume();
    // location.reload();
    location.reload();
  }
  /**
@@ -36,18 +36,15 @@
   * @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();
  }
  /**
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),
  };
}
src/layouts/default/content/index.tsx
@@ -3,11 +3,9 @@
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 @@
          {unref(getOpenPageLoading) && (
            <Loading loading={unref(getPageLoading)} absolute class="layout-content__loading" />
          )}
          <RouterView />
          <PageLayout />
        </div>
      );
    };
src/layouts/default/header/LayoutBreadcrumb.tsx
File was deleted
src/layouts/default/header/LayoutBreadcrumb.vue
New file
@@ -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>
src/layouts/default/header/LayoutHeader.tsx
@@ -9,7 +9,7 @@
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';
src/layouts/default/header/LayoutMultipleHeader.less
@@ -1,5 +1,6 @@
.multiple-tab-header {
  flex: 0 0 auto;
  margin-left: -1px;
  &.fixed {
    position: fixed;
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;
  }
@@ -69,6 +83,24 @@
      .ant-menu-submenu {
        height: @header-height;
        line-height: @header-height;
      }
    }
    .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;
      }
    }
@@ -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);
      }
    }
  }
src/layouts/default/multitabs/TabContent.tsx
@@ -1,19 +1,18 @@
import type { PropType } from 'vue';
import { defineComponent, unref, computed, FunctionalComponent } from 'vue';
import { TabItem, tabStore } from '/@/store/modules/tab';
import { getScaleAction, TabContentProps } from './data';
import { Dropdown } from '/@/components/Dropdown/index';
import { defineComponent, unref, FunctionalComponent } from 'vue';
import { TabContentProps } from './types';
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();
@@ -25,21 +24,13 @@
  );
};
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 @@
  name: 'TabContent',
  props: {
    tabItem: {
      type: Object as PropType<TabItem>,
      type: Object as PropType<RouteLocationNormalized>,
      default: null,
    },
@@ -59,36 +50,27 @@
    },
  },
  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>
      );
    };
src/layouts/default/multitabs/data.ts
File was deleted
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 type { TabContentProps } from './types';
import { defineComponent, watch, computed, unref, ref, onMounted, nextTick } from 'vue';
import Sortable from 'sortablejs';
import { defineComponent, watch, computed, unref, ref } from 'vue';
import { useRouter } from 'vue-router';
import { Tabs } from 'ant-design-vue';
@@ -14,15 +10,12 @@
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 @@
    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 @@
    // 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);
@@ -96,40 +83,6 @@
        );
      });
    }
    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 = {
src/layouts/default/multitabs/types.ts
New file
@@ -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,
}
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 @@
   * @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 @@
  }
  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();
  });
}
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);
  const getTrigger = computed(() => {
    return unref(isTabs) ? ['contextmenu'] : ['click'];
  });
    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);
  }
  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 @@
        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 };
}
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"
    />
src/layouts/iframe/useFrameKeepAlive.ts
@@ -23,7 +23,7 @@
  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 @@
  }
  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 };
}
src/layouts/page/index.tsx
File was deleted
src/layouts/page/index.vue
New file
@@ -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>
src/layouts/parent/index.vue
New file
@@ -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>
src/layouts/parent/useCache.ts
New file
@@ -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 };
}
src/layouts/parent/useTransition.ts
src/locales/lang/en/component/form.ts
@@ -4,8 +4,8 @@
  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}',
};
src/locales/lang/en/layout/header.ts
@@ -15,4 +15,6 @@
  lockScreen: 'Lock screen',
  lockScreenBtn: 'Locking',
  notLockScreenPassword: 'No password lock screen',
  home: 'Home',
};
src/locales/lang/en/routes/demo/level.ts
New file
@@ -0,0 +1,3 @@
export default {
  level: 'Multi menu cache',
};
src/locales/lang/zh_CN/layout/header.ts
@@ -16,4 +16,6 @@
  lockScreen: '锁定屏幕',
  lockScreenBtn: '锁定',
  notLockScreenPassword: '不设置密码锁屏',
  home: '首页',
};
src/locales/lang/zh_CN/routes/demo/feat.ts
@@ -1,5 +1,5 @@
export default {
  feat: '页面功能',
  feat: '功能',
  icon: '图标',
  tabs: '标签页操作',
  contextMenu: '右键菜单',
src/locales/lang/zh_CN/routes/demo/level.ts
New file
@@ -0,0 +1,3 @@
export default {
  level: '多级菜单缓存',
};
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 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,
      },
    },
  ],
};
src/router/guard/index.ts
@@ -8,17 +8,19 @@
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 @@
    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 @@
    } 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);
src/router/guard/pageLoadingGuard.ts
@@ -2,7 +2,7 @@
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 @@
    if (!userStore.getTokenState) {
      return true;
    }
    if (!unref(getEnableTransition) && unref(getOpenPageLoading)) {
      appStore.commitPageLoadingState(true);
      return true;
src/router/guard/permissionGuard.ts
@@ -7,7 +7,7 @@
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 @@
    }
    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;
src/router/guard/progressGuard.ts
@@ -9,9 +9,6 @@
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;
src/router/helper/dynamicImport.ts
New file
@@ -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;
}
src/router/helper/menuHelper.ts
File was renamed from src/utils/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 @@
  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, {
src/router/helper/routeHelper.ts
New file
@@ -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[],
  };
}
src/router/menus/index.ts
@@ -2,7 +2,7 @@
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';
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;
src/router/menus/modules/demo/level.ts
New file
@@ -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;
src/router/menus/modules/home.ts
New file
@@ -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;
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 basicRoutes = [LoginRoute, RootRoute];
export const basicRoutes = [LoginRoute, MainRoute, REDIRECT_ROUTE];
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: [
  children: [
    {
      path: '/welcome',
      name: 'Welcome',
      component: () => import('/@/views/dashboard/welcome/index.vue'),
      meta: {
        title: 'routes.dashboard.welcome',
        affix: true,
        icon: 'ant-design:home-outlined',
      },
    },
    {
      path: '/workbench',
      path: 'workbench',
      name: 'Workbench',
      component: () => import('/@/views/dashboard/workbench/index.vue'),
      meta: {
@@ -34,7 +21,7 @@
      },
    },
    {
      path: '/analysis',
      path: 'analysis',
      name: 'Analysis',
      component: () => import('/@/views/dashboard/analysis/index.vue'),
      meta: {
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 @@
      ],
    },
    {
      path: '/apexChart',
      path: 'apexChart',
      name: 'ApexChart',
      meta: {
        title: 'routes.demo.charts.apexChart',
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 @@
      },
    },
    {
      path: '/transition',
      path: 'transition',
      name: 'transitionDemo',
      component: () => import('/@/views/demo/comp/transition/index.vue'),
      meta: {
@@ -32,7 +30,7 @@
      },
    },
    {
      path: '/countTo',
      path: 'countTo',
      name: 'CountTo',
      component: () => import('/@/views/demo/comp/count-to/index.vue'),
      meta: {
@@ -41,9 +39,10 @@
    },
    {
      path: '/scroll',
      path: 'scroll',
      name: 'ScrollDemo',
      redirect: '/comp/scroll/basic',
      component: getParentLayout('ScrollDemo'),
      meta: {
        title: 'routes.demo.comp.scroll',
      },
@@ -76,7 +75,7 @@
    },
    {
      path: '/modal',
      path: 'modal',
      name: 'ModalDemo',
      component: () => import('/@/views/demo/comp/modal/index.vue'),
      meta: {
@@ -84,7 +83,7 @@
      },
    },
    {
      path: '/drawer',
      path: 'drawer',
      name: 'DrawerDemo',
      component: () => import('/@/views/demo/comp/drawer/index.vue'),
      meta: {
@@ -92,7 +91,7 @@
      },
    },
    {
      path: '/desc',
      path: 'desc',
      name: 'DescDemo',
      component: () => import('/@/views/demo/comp/desc/index.vue'),
      meta: {
@@ -101,8 +100,9 @@
    },
    {
      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 @@
      ],
    },
    {
      path: '/verify',
      path: 'verify',
      name: 'VerifyDemo',
      component: getParentLayout('VerifyDemo'),
      redirect: '/comp/verify/drag',
      meta: {
        title: 'routes.demo.comp.verify',
@@ -155,7 +156,7 @@
    //
    {
      path: '/qrcode',
      path: 'qrcode',
      name: 'QrCodeDemo',
      component: () => import('/@/views/demo/comp/qrcode/index.vue'),
      meta: {
@@ -163,7 +164,7 @@
      },
    },
    {
      path: '/strength-meter',
      path: 'strength-meter',
      name: 'StrengthMeterDemo',
      component: () => import('/@/views/demo/comp/strength-meter/index.vue'),
      meta: {
@@ -171,7 +172,7 @@
      },
    },
    {
      path: '/upload',
      path: 'upload',
      name: 'UploadDemo',
      component: () => import('/@/views/demo/comp/upload/index.vue'),
      meta: {
@@ -179,7 +180,7 @@
      },
    },
    {
      path: '/loading',
      path: 'loading',
      name: 'LoadingDemo',
      component: () => import('/@/views/demo/comp/loading/index.vue'),
      meta: {
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 @@
      },
    },
    {
      path: '/tinymce',
      path: 'tinymce',
      component: getParentLayout('TinymceDemo'),
      name: 'TinymceDemo',
      meta: {
        title: 'routes.demo.editor.tinymce',
@@ -39,7 +37,6 @@
            title: 'routes.demo.editor.tinymceBasic',
          },
        },
        // TODO
        {
          path: 'editor',
          name: 'TinymceFormDemo',
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 @@
      },
    },
    {
      path: '/jsonExport',
      path: 'jsonExport',
      name: 'JsonExport',
      component: () => import('/@/views/demo/excel/JsonExport.vue'),
      meta: {
@@ -32,7 +30,7 @@
      },
    },
    {
      path: '/arrayExport',
      path: 'arrayExport',
      name: 'ArrayExport',
      component: () => import('/@/views/demo/excel/ArrayExport.vue'),
      meta: {
@@ -40,7 +38,7 @@
      },
    },
    {
      path: '/importExcel',
      path: 'importExcel',
      name: 'ImportExcel',
      component: () => import('/@/views/demo/excel/ImportExcel.vue'),
      meta: {
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 @@
      },
    },
    {
      path: '/tabs',
      path: 'tabs',
      name: 'TabsDemo',
      component: () => import('/@/views/demo/feat/tabs/index.vue'),
      meta: {
@@ -33,7 +30,7 @@
    },
    {
      path: '/context-menu',
      path: 'context-menu',
      name: 'ContextMenuDemo',
      component: () => import('/@/views/demo/feat/context-menu/index.vue'),
      meta: {
@@ -41,7 +38,7 @@
      },
    },
    {
      path: '/download',
      path: 'download',
      name: 'DownLoadDemo',
      component: () => import('/@/views/demo/feat/download/index.vue'),
      meta: {
@@ -49,7 +46,7 @@
      },
    },
    {
      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 @@
      },
    },
    {
      path: '/img-preview',
      path: 'img-preview',
      name: 'ImgPreview',
      component: () => import('/@/views/demo/feat/img-preview/index.vue'),
      meta: {
@@ -65,7 +62,7 @@
      },
    },
    {
      path: '/copy',
      path: 'copy',
      name: 'CopyDemo',
      component: () => import('/@/views/demo/feat/copy/index.vue'),
      meta: {
@@ -73,7 +70,7 @@
      },
    },
    {
      path: '/msg',
      path: 'msg',
      name: 'MsgDemo',
      component: () => import('/@/views/demo/feat/msg/index.vue'),
      meta: {
@@ -81,7 +78,7 @@
      },
    },
    {
      path: '/watermark',
      path: 'watermark',
      name: 'WatermarkDemo',
      component: () => import('/@/views/demo/feat/watermark/index.vue'),
      meta: {
@@ -89,7 +86,7 @@
      },
    },
    {
      path: '/full-screen',
      path: 'full-screen',
      name: 'FullScreenDemo',
      component: () => import('/@/views/demo/feat/full-screen/index.vue'),
      meta: {
@@ -97,7 +94,7 @@
      },
    },
    {
      path: '/error-log',
      path: 'error-log',
      name: 'ErrorLog',
      component: () => import('/@/views/sys/error-log/index.vue'),
      meta: {
@@ -105,7 +102,7 @@
      },
    },
    {
      path: '/testTab/:id',
      path: 'testTab/:id',
      name: 'TestTab',
      component: () => import('/@/views/demo/feat/tab-params/index.vue'),
      meta: {
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 @@
      },
    },
    {
      path: '/useForm',
      path: 'useForm',
      name: 'UseFormDemo',
      component: () => import('/@/views/demo/form/UseForm.vue'),
      meta: {
@@ -32,7 +29,7 @@
      },
    },
    {
      path: '/refForm',
      path: 'refForm',
      name: 'RefFormDemo',
      component: () => import('/@/views/demo/form/RefForm.vue'),
      meta: {
@@ -40,7 +37,7 @@
      },
    },
    {
      path: '/advancedForm',
      path: 'advancedForm',
      name: 'AdvancedFormDemo',
      component: () => import('/@/views/demo/form/AdvancedForm.vue'),
      meta: {
@@ -48,7 +45,7 @@
      },
    },
    {
      path: '/ruleForm',
      path: 'ruleForm',
      name: 'RuleFormDemo',
      component: () => import('/@/views/demo/form/RuleForm.vue'),
      meta: {
@@ -56,7 +53,7 @@
      },
    },
    {
      path: '/dynamicForm',
      path: 'dynamicForm',
      name: 'DynamicFormDemo',
      component: () => import('/@/views/demo/form/DynamicForm.vue'),
      meta: {
@@ -64,7 +61,7 @@
      },
    },
    {
      path: '/customerForm',
      path: 'customerForm',
      name: 'CustomerFormDemo',
      component: () => import('/@/views/demo/form/CustomerForm.vue'),
      meta: {
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 @@
      },
    },
    {
      path: '/doc',
      path: 'doc',
      name: 'Doc',
      component: IFrame,
      meta: {
@@ -37,7 +35,7 @@
      },
    },
    {
      path: '/docExternal',
      path: 'docExternal',
      name: 'DocExternal',
      component: IFrame,
      meta: {
src/router/routes/modules/demo/level.ts
New file
@@ -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;
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 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 @@
  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 @@
    // =============================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 @@
    // =============================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 @@
    // =============================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 @@
    // =============================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 @@
    // =============================exception end=============================
    // =============================list start=============================
    {
      path: '/list',
      path: 'list',
      name: 'ListPage',
      component: getParentLayout('ListPage'),
      redirect: '/page-demo/list/card',
      meta: {
        title: 'routes.demo.page.list',
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 @@
      ],
    },
    {
      path: '/back',
      path: 'back',
      name: 'PermissionBackDemo',
      component: getParentLayout('PermissionBackDemo'),
      meta: {
        title: 'routes.demo.permission.back',
      },
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 @@
      },
    },
    {
      path: '/treeTable',
      path: 'treeTable',
      name: 'TreeTableDemo',
      component: () => import('/@/views/demo/table/TreeTable.vue'),
      meta: {
@@ -32,7 +30,7 @@
      },
    },
    {
      path: '/fetchTable',
      path: 'fetchTable',
      name: 'FetchTableDemo',
      component: () => import('/@/views/demo/table/FetchTable.vue'),
      meta: {
@@ -40,7 +38,7 @@
      },
    },
    {
      path: '/fixedColumn',
      path: 'fixedColumn',
      name: 'FixedColumnDemo',
      component: () => import('/@/views/demo/table/FixedColumn.vue'),
      meta: {
@@ -48,7 +46,7 @@
      },
    },
    {
      path: '/customerCell',
      path: 'customerCell',
      name: 'CustomerCellDemo',
      component: () => import('/@/views/demo/table/CustomerCell.vue'),
      meta: {
@@ -56,7 +54,7 @@
      },
    },
    {
      path: '/formTable',
      path: 'formTable',
      name: 'FormTableDemo',
      component: () => import('/@/views/demo/table/FormTable.vue'),
      meta: {
@@ -64,7 +62,7 @@
      },
    },
    {
      path: '/useTable',
      path: 'useTable',
      name: 'UseTableDemo',
      component: () => import('/@/views/demo/table/UseTable.vue'),
      meta: {
@@ -72,7 +70,7 @@
      },
    },
    {
      path: '/refTable',
      path: 'refTable',
      name: 'RefTableDemo',
      component: () => import('/@/views/demo/table/RefTable.vue'),
      meta: {
@@ -80,7 +78,7 @@
      },
    },
    {
      path: '/multipleHeader',
      path: 'multipleHeader',
      name: 'MultipleHeaderDemo',
      component: () => import('/@/views/demo/table/MultipleHeader.vue'),
      meta: {
@@ -88,7 +86,7 @@
      },
    },
    {
      path: '/mergeHeader',
      path: 'mergeHeader',
      name: 'MergeHeaderDemo',
      component: () => import('/@/views/demo/table/MergeHeader.vue'),
      meta: {
@@ -96,7 +94,7 @@
      },
    },
    {
      path: '/expandTable',
      path: 'expandTable',
      name: 'ExpandTableDemo',
      component: () => import('/@/views/demo/table/ExpandTable.vue'),
      meta: {
@@ -104,7 +102,7 @@
      },
    },
    {
      path: '/fixedHeight',
      path: 'fixedHeight',
      name: 'FixedHeightDemo',
      component: () => import('/@/views/demo/table/FixedHeight.vue'),
      meta: {
@@ -112,7 +110,7 @@
      },
    },
    {
      path: '/footerTable',
      path: 'footerTable',
      name: 'FooterTableDemo',
      component: () => import('/@/views/demo/table/FooterTable.vue'),
      meta: {
@@ -120,7 +118,7 @@
      },
    },
    {
      path: '/editCellTable',
      path: 'editCellTable',
      name: 'EditCellTableDemo',
      component: () => import('/@/views/demo/table/EditCellTable.vue'),
      meta: {
@@ -128,7 +126,7 @@
      },
    },
    {
      path: '/editRowTable',
      path: 'editRowTable',
      name: 'EditRowTableDemo',
      component: () => import('/@/views/demo/table/EditRowTable.vue'),
      meta: {
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 @@
      },
    },
    {
      path: '/editTree',
      path: 'editTree',
      name: 'EditTreeDemo',
      component: () => import('/@/views/demo/tree/EditTree.vue'),
      meta: {
@@ -31,7 +29,7 @@
      },
    },
    {
      path: '/actionTree',
      path: 'actionTree',
      name: 'ActionTreeDemo',
      component: () => import('/@/views/demo/tree/ActionTree.vue'),
      meta: {
src/router/routes/modules/home.ts
New file
@@ -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;
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 @@
  // 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 @@
  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;
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,14 +14,12 @@
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';
@@ -87,6 +84,7 @@
  @Action
  async buildRoutesAction(id?: number | string): Promise<AppRouteRecordRaw[]> {
    const { t } = useI18n();
    let routes: AppRouteRecordRaw[] = [];
    const roleList = toRaw(userStore.getRoleListState);
@@ -95,17 +93,15 @@
    // 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 @@
      routeList = transformObjToRoute(routeList);
      //  后台路由转菜单结构
      const backMenuList = transformRouteToMenu(routeList);
      this.commitBackMenuListState(backMenuList);
      // 生成路由
      routes = genRouteModule(routeList) as AppRouteRecordRaw[];
      routes.push(REDIRECT_ROUTE);
      routes = routeList;
    }
    return routes;
  }
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 {
  cachedMapState = new Map<string, string[]>();
  // tab list
  tabsState: TabItem[] = [];
  // tab cache list
  keepAliveTabsState: CacheName[] = [];
  currentContextMenuIndexState = -1;
  currentContextMenuState: TabItem | null = null;
  tabsState: RouteLocationNormalized[] = [];
  // Last route change
  lastChangeRouteState: AppRouteRecordRaw | null = null;
  lastChangeRouteState: RouteLocationNormalized | null = null;
  lastDragEndIndexState = 0;
  get getTabsState() {
    return this.tabsState;
@@ -57,56 +47,93 @@
    return this.lastChangeRouteState;
  }
  get getCurrentContextMenuIndexState() {
    return this.currentContextMenuIndexState;
  }
  get getCurrentContextMenuState() {
    return this.currentContextMenuState;
  }
  get getKeepAliveTabsState() {
    return this.keepAliveTabsState;
  }
  get getCurrentTab(): TabItem {
  get getCurrentTab(): RouteLocationNormalized {
    const route = unref(router.currentRoute);
    return this.tabsState.find((item) => item.path === route.path)!;
  }
  get getCachedMapState(): Map<string, string[]> {
    return this.cachedMapState;
  }
  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);
  @Mutation
  commitCurrentContextMenuState(item: TabItem): void {
    this.currentContextMenuState = item;
  }
    let toPath: PageEnum | string = PageEnum.BASE_HOME;
  /**
   * @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;
    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
  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;
  }
  @Mutation
  commitTabRoutesState(route: RouteLocationNormalized) {
    const { path, fullPath, params, query } = route;
    let updateIndex = -1;
    // 已经存在的页面,不重复添加tab
@@ -123,39 +150,18 @@
      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 @@
    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 @@
    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
  closeLeftTabAction(route: AppRouteRecordRaw | TabItem): void {
  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
  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 });
      this.closeMultipleTab({ pathList });
    }
    this.commitCachedMapState();
    isGotoPage();
  }
  @Action
  addTabByPathAction(): void {
    const toRoute = getCurrentTo();
    if (!toRoute) return;
    const { meta } = toRoute;
    if (meta && meta.affix) {
      return;
    }
    this.commitAddTab((toRoute as unknown) as AppRouteRecordRaw);
  }
  @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 @@
        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);
src/utils/helper/dynamicImport.ts
File was deleted
src/utils/helper/routeHelper.ts
File was deleted
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('');
src/views/demo/level/Menu111.vue
New file
@@ -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>
src/views/demo/level/Menu12.vue
New file
@@ -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>
src/views/demo/level/Menu2.vue
New file
@@ -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>
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>
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"
@@ -1659,6 +1670,14 @@
  dependencies:
    "@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"
@@ -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"
@@ -1733,6 +1782,13 @@
  integrity sha512-t39Qmc42MX7wJtf8L6tHlu17eP9Rc5w4aRnxpLHNWoaRxddv/7FBhWqusJ2Bwkk8ixFHOQeejcLMt5G469WYJw==
  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"
@@ -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"
@@ -1757,6 +1821,15 @@
  dependencies:
    "@vue/runtime-core" "3.0.3"
    "@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":
@@ -1777,6 +1850,11 @@
  version "3.0.3"
  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"
@@ -3013,10 +3091,10 @@
    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 @@
  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==
@@ -3468,6 +3546,11 @@
  version "0.8.15"
  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"
@@ -3581,13 +3664,13 @@
  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/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"