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