Commit 4baf90a5c87493939830129efaa146624faabbcc
1 parent
43929174
perf: optimize tab switching speed
Showing
9 changed files
with
192 additions
and
47 deletions
CHANGELOG.zh_CN.md
src/components/Menu/src/index.less
@@ -206,7 +206,7 @@ | @@ -206,7 +206,7 @@ | ||
206 | // 层级样式 | 206 | // 层级样式 |
207 | &.ant-menu-dark:not(.basic-menu__sidebar-hor) { | 207 | &.ant-menu-dark:not(.basic-menu__sidebar-hor) { |
208 | overflow-x: hidden; | 208 | overflow-x: hidden; |
209 | - background: @first-menu-item-dark-bg-color; | 209 | + background: @menu-item-dark-bg-color; |
210 | .active-menu-style(); | 210 | .active-menu-style(); |
211 | 211 | ||
212 | .ant-menu-item.ant-menu-item-selected.basic-menu-menu-item__level1, | 212 | .ant-menu-item.ant-menu-item-selected.basic-menu-menu-item__level1, |
@@ -215,20 +215,21 @@ | @@ -215,20 +215,21 @@ | ||
215 | } | 215 | } |
216 | 216 | ||
217 | .basic-menu-item__level1 { | 217 | .basic-menu-item__level1 { |
218 | - background-color: @first-menu-item-dark-bg-color; | 218 | + background-color: @menu-item-dark-bg-color; |
219 | 219 | ||
220 | > .ant-menu-sub > li { | 220 | > .ant-menu-sub > li { |
221 | - background-color: @sub-menu-item-dark-bg-color; | 221 | + background-color: lighten(@menu-item-dark-bg-color, 6%); |
222 | } | 222 | } |
223 | } | 223 | } |
224 | 224 | ||
225 | .basic-menu-item__level2:not(.ant-menu-item-selected), | 225 | .basic-menu-item__level2:not(.ant-menu-item-selected), |
226 | .ant-menu-sub { | 226 | .ant-menu-sub { |
227 | - background-color: @sub-menu-item-dark-bg-color; | 227 | + background-color: lighten(@menu-item-dark-bg-color, 6%); |
228 | + // background-color: @sub-menu-item-dark-bg-color; | ||
228 | } | 229 | } |
229 | 230 | ||
230 | .basic-menu-item__level3:not(.ant-menu-item-selected) { | 231 | .basic-menu-item__level3:not(.ant-menu-item-selected) { |
231 | - background-color: @children-menu-item-dark-bg-color; | 232 | + background-color: lighten(@menu-item-dark-bg-color, 10%); |
232 | } | 233 | } |
233 | 234 | ||
234 | .ant-menu-submenu-title { | 235 | .ant-menu-submenu-title { |
@@ -241,7 +242,7 @@ | @@ -241,7 +242,7 @@ | ||
241 | &.ant-menu-inline-collapsed { | 242 | &.ant-menu-inline-collapsed { |
242 | .ant-menu-submenu-selected, | 243 | .ant-menu-submenu-selected, |
243 | .ant-menu-item-selected { | 244 | .ant-menu-item-selected { |
244 | - background: darken(@first-menu-item-dark-bg-color, 6%) !important; | 245 | + background: darken(@menu-item-dark-bg-color, 6%) !important; |
245 | } | 246 | } |
246 | } | 247 | } |
247 | } | 248 | } |
@@ -310,7 +311,7 @@ | @@ -310,7 +311,7 @@ | ||
310 | .ant-menu-dark { | 311 | .ant-menu-dark { |
311 | &.ant-menu-submenu-popup { | 312 | &.ant-menu-submenu-popup { |
312 | > ul { | 313 | > ul { |
313 | - background: @first-menu-item-dark-bg-color; | 314 | + background: @menu-item-dark-bg-color; |
314 | } | 315 | } |
315 | 316 | ||
316 | .active-menu-style(); | 317 | .active-menu-style(); |
src/design/color.less
@@ -64,12 +64,7 @@ | @@ -64,12 +64,7 @@ | ||
64 | // ================================= | 64 | // ================================= |
65 | 65 | ||
66 | // let -menu | 66 | // let -menu |
67 | -@first-menu-item-dark-bg-color: #273352; | ||
68 | - | ||
69 | -// Level 2 menu dark background color | ||
70 | -@sub-menu-item-dark-bg-color: #314268; | ||
71 | -// Level 3 menu dark background color | ||
72 | -@children-menu-item-dark-bg-color: #4f6088; | 67 | +@menu-item-dark-bg-color: #273352; |
73 | 68 | ||
74 | // top-menu | 69 | // top-menu |
75 | @top-menu-active-bg-color: #273352; | 70 | @top-menu-active-bg-color: #273352; |
src/layouts/default/index.less
@@ -41,7 +41,7 @@ | @@ -41,7 +41,7 @@ | ||
41 | background-size: 100% 100%; | 41 | background-size: 100% 100%; |
42 | 42 | ||
43 | &.ant-layout-sider-dark { | 43 | &.ant-layout-sider-dark { |
44 | - background: @first-menu-item-dark-bg-color; | 44 | + background: @menu-item-dark-bg-color; |
45 | } | 45 | } |
46 | 46 | ||
47 | &:not(.ant-layout-sider-dark) { | 47 | &:not(.ant-layout-sider-dark) { |
src/layouts/default/multitabs/index.tsx
@@ -2,15 +2,10 @@ import type { TabContentProps } from './tab.data'; | @@ -2,15 +2,10 @@ import type { TabContentProps } from './tab.data'; | ||
2 | import type { TabItem } from '/@/store/modules/tab'; | 2 | import type { TabItem } from '/@/store/modules/tab'; |
3 | import type { AppRouteRecordRaw } from '/@/router/types'; | 3 | import type { AppRouteRecordRaw } from '/@/router/types'; |
4 | 4 | ||
5 | -import { | ||
6 | - defineComponent, | ||
7 | - watch, | ||
8 | - computed, | ||
9 | - // ref, | ||
10 | - unref, | ||
11 | - // onMounted, | ||
12 | - toRaw, | ||
13 | -} from 'vue'; | 5 | +import { defineComponent, watch, computed, unref, toRaw } from 'vue'; |
6 | +import { useRouter } from 'vue-router'; | ||
7 | +import router from '/@/router'; | ||
8 | + | ||
14 | import { Tabs } from 'ant-design-vue'; | 9 | import { Tabs } from 'ant-design-vue'; |
15 | import TabContent from './TabContent'; | 10 | import TabContent from './TabContent'; |
16 | 11 | ||
@@ -18,16 +13,13 @@ import { useGo } from '/@/hooks/web/usePage'; | @@ -18,16 +13,13 @@ import { useGo } from '/@/hooks/web/usePage'; | ||
18 | 13 | ||
19 | import { TabContentEnum } from './tab.data'; | 14 | import { TabContentEnum } from './tab.data'; |
20 | 15 | ||
21 | -import { useRouter } from 'vue-router'; | ||
22 | - | ||
23 | import { tabStore } from '/@/store/modules/tab'; | 16 | import { tabStore } from '/@/store/modules/tab'; |
17 | +import { userStore } from '/@/store/modules/user'; | ||
18 | + | ||
24 | import { closeTab } from './useTabDropdown'; | 19 | import { closeTab } from './useTabDropdown'; |
25 | -import router from '/@/router'; | ||
26 | import { useTabs } from '/@/hooks/web/useTabs'; | 20 | import { useTabs } from '/@/hooks/web/useTabs'; |
27 | -// import { PageEnum } from '/@/enums/pageEnum'; | ||
28 | 21 | ||
29 | import './index.less'; | 22 | import './index.less'; |
30 | -import { userStore } from '/@/store/modules/user'; | ||
31 | export default defineComponent({ | 23 | export default defineComponent({ |
32 | name: 'MultiTabs', | 24 | name: 'MultiTabs', |
33 | setup() { | 25 | setup() { |
@@ -41,20 +33,24 @@ export default defineComponent({ | @@ -41,20 +33,24 @@ export default defineComponent({ | ||
41 | return tabStore.getTabsState; | 33 | return tabStore.getTabsState; |
42 | }); | 34 | }); |
43 | 35 | ||
44 | - if (!isAddAffix) { | ||
45 | - addAffixTabs(); | ||
46 | - isAddAffix = true; | ||
47 | - } | ||
48 | - | 36 | + // If you monitor routing changes, tab switching will be stuck. So use this method |
49 | watch( | 37 | watch( |
50 | - () => unref(currentRoute).path, | 38 | + () => tabStore.getLastChangeRouteState, |
51 | () => { | 39 | () => { |
52 | - if (!userStore.getTokenState) return; | ||
53 | - const { path: rPath, fullPath } = unref(currentRoute); | ||
54 | - if (activeKeyRef.value !== (fullPath || rPath)) { | ||
55 | - activeKeyRef.value = fullPath || rPath; | 40 | + if (!isAddAffix) { |
41 | + addAffixTabs(); | ||
42 | + isAddAffix = true; | ||
43 | + } | ||
44 | + | ||
45 | + const lastChangeRoute = unref(tabStore.getLastChangeRouteState); | ||
46 | + | ||
47 | + if (!lastChangeRoute || !userStore.getTokenState) return; | ||
48 | + | ||
49 | + const { path, fullPath } = lastChangeRoute; | ||
50 | + if (activeKeyRef.value !== (fullPath || path)) { | ||
51 | + activeKeyRef.value = fullPath || path; | ||
56 | } | 52 | } |
57 | - tabStore.commitAddTab((unref(currentRoute) as unknown) as AppRouteRecordRaw); | 53 | + tabStore.commitAddTab((lastChangeRoute as unknown) as AppRouteRecordRaw); |
58 | }, | 54 | }, |
59 | { | 55 | { |
60 | immediate: true, | 56 | immediate: true, |
src/router/guard/index.ts
@@ -12,6 +12,8 @@ import { getIsOpenTab, setCurrentTo } from '/@/utils/helper/routeHelper'; | @@ -12,6 +12,8 @@ import { getIsOpenTab, setCurrentTo } from '/@/utils/helper/routeHelper'; | ||
12 | import { setTitle } from '/@/utils/browser'; | 12 | import { setTitle } from '/@/utils/browser'; |
13 | import { AxiosCanceler } from '/@/utils/http/axios/axiosCancel'; | 13 | import { AxiosCanceler } from '/@/utils/http/axios/axiosCancel'; |
14 | 14 | ||
15 | +import { tabStore } from '/@/store/modules/tab'; | ||
16 | + | ||
15 | const { projectSetting, globSetting } = useSetting(); | 17 | const { projectSetting, globSetting } = useSetting(); |
16 | export function createGuard(router: Router) { | 18 | export function createGuard(router: Router) { |
17 | const { openNProgress, closeMessageOnSwitch, removeAllHttpPending } = projectSetting; | 19 | const { openNProgress, closeMessageOnSwitch, removeAllHttpPending } = projectSetting; |
@@ -20,8 +22,21 @@ export function createGuard(router: Router) { | @@ -20,8 +22,21 @@ export function createGuard(router: Router) { | ||
20 | axiosCanceler = new AxiosCanceler(); | 22 | axiosCanceler = new AxiosCanceler(); |
21 | } | 23 | } |
22 | router.beforeEach(async (to) => { | 24 | router.beforeEach(async (to) => { |
25 | + // Determine whether the tab has been opened | ||
23 | const isOpen = getIsOpenTab(to.fullPath); | 26 | const isOpen = getIsOpenTab(to.fullPath); |
24 | to.meta.inTab = isOpen; | 27 | to.meta.inTab = isOpen; |
28 | + | ||
29 | + // Notify routing changes | ||
30 | + const { fullPath, path, query, params, name, meta } = to; | ||
31 | + tabStore.commitLastChangeRouteState({ | ||
32 | + fullPath, | ||
33 | + path, | ||
34 | + query, | ||
35 | + params, | ||
36 | + name, | ||
37 | + meta, | ||
38 | + } as any); | ||
39 | + | ||
25 | try { | 40 | try { |
26 | if (closeMessageOnSwitch) { | 41 | if (closeMessageOnSwitch) { |
27 | Modal.destroyAll(); | 42 | Modal.destroyAll(); |
src/store/modules/menu.ts
@@ -8,12 +8,6 @@ const NAME = 'menu'; | @@ -8,12 +8,6 @@ const NAME = 'menu'; | ||
8 | hotModuleUnregisterModule(NAME); | 8 | hotModuleUnregisterModule(NAME); |
9 | @Module({ namespaced: true, name: NAME, dynamic: true, store }) | 9 | @Module({ namespaced: true, name: NAME, dynamic: true, store }) |
10 | class Menu extends VuexModule { | 10 | class Menu extends VuexModule { |
11 | - // // 默认展开 | ||
12 | - // private collapsedState: boolean = appStore.getProjectConfig.menuSetting.collapsed; | ||
13 | - | ||
14 | - // // 菜单宽度 | ||
15 | - // private menuWidthState: number = appStore.getProjectConfig.menuSetting.menuWidth; | ||
16 | - | ||
17 | // 是否开始拖拽 | 11 | // 是否开始拖拽 |
18 | private dragStartState = false; | 12 | private dragStartState = false; |
19 | 13 |
src/store/modules/tab.ts
@@ -7,6 +7,7 @@ import { hotModuleUnregisterModule } from '/@/utils/helper/vuexHelper'; | @@ -7,6 +7,7 @@ import { hotModuleUnregisterModule } from '/@/utils/helper/vuexHelper'; | ||
7 | 7 | ||
8 | import { PageEnum } from '/@/enums/pageEnum'; | 8 | import { PageEnum } from '/@/enums/pageEnum'; |
9 | import { appStore } from '/@/store/modules/app'; | 9 | import { appStore } from '/@/store/modules/app'; |
10 | +import { userStore } from './user'; | ||
10 | 11 | ||
11 | import store from '/@/store'; | 12 | import store from '/@/store'; |
12 | import router from '/@/router'; | 13 | import router from '/@/router'; |
@@ -43,10 +44,17 @@ class Tab extends VuexModule { | @@ -43,10 +44,17 @@ class Tab extends VuexModule { | ||
43 | 44 | ||
44 | currentContextMenuState: TabItem | null = null; | 45 | currentContextMenuState: TabItem | null = null; |
45 | 46 | ||
47 | + // Last route change | ||
48 | + lastChangeRouteState: AppRouteRecordRaw | null = null; | ||
49 | + | ||
46 | get getTabsState() { | 50 | get getTabsState() { |
47 | return this.tabsState; | 51 | return this.tabsState; |
48 | } | 52 | } |
49 | 53 | ||
54 | + get getLastChangeRouteState() { | ||
55 | + return this.lastChangeRouteState; | ||
56 | + } | ||
57 | + | ||
50 | get getCurrentContextMenuIndexState() { | 58 | get getCurrentContextMenuIndexState() { |
51 | return this.currentContextMenuIndexState; | 59 | return this.currentContextMenuIndexState; |
52 | } | 60 | } |
@@ -65,6 +73,12 @@ class Tab extends VuexModule { | @@ -65,6 +73,12 @@ class Tab extends VuexModule { | ||
65 | } | 73 | } |
66 | 74 | ||
67 | @Mutation | 75 | @Mutation |
76 | + commitLastChangeRouteState(route: AppRouteRecordRaw): void { | ||
77 | + if (!userStore.getTokenState) return; | ||
78 | + this.lastChangeRouteState = route; | ||
79 | + } | ||
80 | + | ||
81 | + @Mutation | ||
68 | commitClearCache(): void { | 82 | commitClearCache(): void { |
69 | this.keepAliveTabsState = []; | 83 | this.keepAliveTabsState = []; |
70 | } | 84 | } |
@@ -86,7 +100,7 @@ class Tab extends VuexModule { | @@ -86,7 +100,7 @@ class Tab extends VuexModule { | ||
86 | commitAddTab(route: AppRouteRecordRaw | TabItem): void { | 100 | commitAddTab(route: AppRouteRecordRaw | TabItem): void { |
87 | const { path, name, meta, fullPath, params, query } = route as TabItem; | 101 | const { path, name, meta, fullPath, params, query } = route as TabItem; |
88 | // 404 页面不需要添加tab | 102 | // 404 页面不需要添加tab |
89 | - if (path === PageEnum.ERROR_PAGE) { | 103 | + if (path === PageEnum.ERROR_PAGE || !name) { |
90 | return; | 104 | return; |
91 | } else if ([REDIRECT_ROUTE.name, PAGE_NOT_FOUND_ROUTE.name].includes(name as string)) { | 105 | } else if ([REDIRECT_ROUTE.name, PAGE_NOT_FOUND_ROUTE.name].includes(name as string)) { |
92 | return; | 106 | return; |
@@ -107,7 +121,6 @@ class Tab extends VuexModule { | @@ -107,7 +121,6 @@ class Tab extends VuexModule { | ||
107 | this.tabsState.splice(updateIndex, 1, curTab); | 121 | this.tabsState.splice(updateIndex, 1, curTab); |
108 | return; | 122 | return; |
109 | } | 123 | } |
110 | - | ||
111 | this.tabsState.push({ path, fullPath, name, meta, params, query }); | 124 | this.tabsState.push({ path, fullPath, name, meta, params, query }); |
112 | if (unref(getOpenKeepAliveRef) && name) { | 125 | if (unref(getOpenKeepAliveRef) && name) { |
113 | const noKeepAlive = meta && meta.ignoreKeepAlive; | 126 | const noKeepAlive = meta && meta.ignoreKeepAlive; |
src/utils/color.ts
0 → 100644
1 | +/** | ||
2 | + * 判断是否 十六进制颜色值. | ||
3 | + * 输入形式可为 #fff000 #f00 | ||
4 | + * | ||
5 | + * @param String color 十六进制颜色值 | ||
6 | + * @return Boolean | ||
7 | + */ | ||
8 | +export const isHexColor = function (color: string) { | ||
9 | + const reg = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/; | ||
10 | + return reg.test(color); | ||
11 | +}; | ||
12 | + | ||
13 | +/** | ||
14 | + * RGB 颜色值转换为 十六进制颜色值. | ||
15 | + * r, g, 和 b 需要在 [0, 255] 范围内 | ||
16 | + * | ||
17 | + * @param Number r 红色色值 | ||
18 | + * @param Number g 绿色色值 | ||
19 | + * @param Number b 蓝色色值 | ||
20 | + * @return String 类似#ff00ff | ||
21 | + */ | ||
22 | +export const rgbToHex = function (r: number, g: number, b: number) { | ||
23 | + // tslint:disable-next-line:no-bitwise | ||
24 | + const hex = ((r << 16) | (g << 8) | b).toString(16); | ||
25 | + return '#' + new Array(Math.abs(hex.length - 7)).join('0') + hex; | ||
26 | +}; | ||
27 | + | ||
28 | +/** | ||
29 | + * Transform a HEX color to its RGB representation | ||
30 | + * @param {string} hex The color to transform | ||
31 | + * @returns The RGB representation of the passed color | ||
32 | + */ | ||
33 | +export const hexToRGB = function (hex: string) { | ||
34 | + return ( | ||
35 | + parseInt(hex.substring(0, 2), 16) + | ||
36 | + ',' + | ||
37 | + parseInt(hex.substring(2, 4), 16) + | ||
38 | + ',' + | ||
39 | + parseInt(hex.substring(4, 6), 16) | ||
40 | + ); | ||
41 | +}; | ||
42 | + | ||
43 | +/** | ||
44 | + * Darkens a HEX color given the passed percentage | ||
45 | + * @param {string} color The color to process | ||
46 | + * @param {number} amount The amount to change the color by | ||
47 | + * @returns {string} The HEX representation of the processed color | ||
48 | + */ | ||
49 | +export const darken = (color: string, amount: number) => { | ||
50 | + color = color.indexOf('#') >= 0 ? color.substring(1, color.length) : color; | ||
51 | + amount = Math.trunc((255 * amount) / 100); | ||
52 | + return `#${subtractLight(color.substring(0, 2), amount)}${subtractLight( | ||
53 | + color.substring(2, 4), | ||
54 | + amount | ||
55 | + )}${subtractLight(color.substring(4, 6), amount)}`; | ||
56 | +}; | ||
57 | + | ||
58 | +/** | ||
59 | + * Lightens a 6 char HEX color according to the passed percentage | ||
60 | + * @param {string} color The color to change | ||
61 | + * @param {number} amount The amount to change the color by | ||
62 | + * @returns {string} The processed color represented as HEX | ||
63 | + */ | ||
64 | +export const lighten = (color: string, amount: number) => { | ||
65 | + color = color.indexOf('#') >= 0 ? color.substring(1, color.length) : color; | ||
66 | + amount = Math.trunc((255 * amount) / 100); | ||
67 | + return `#${addLight(color.substring(0, 2), amount)}${addLight( | ||
68 | + color.substring(2, 4), | ||
69 | + amount | ||
70 | + )}${addLight(color.substring(4, 6), amount)}`; | ||
71 | +}; | ||
72 | + | ||
73 | +/* Suma el porcentaje indicado a un color (RR, GG o BB) hexadecimal para aclararlo */ | ||
74 | +/** | ||
75 | + * Sums the passed percentage to the R, G or B of a HEX color | ||
76 | + * @param {string} color The color to change | ||
77 | + * @param {number} amount The amount to change the color by | ||
78 | + * @returns {string} The processed part of the color | ||
79 | + */ | ||
80 | +const addLight = (color: string, amount: number) => { | ||
81 | + const cc = parseInt(color, 16) + amount; | ||
82 | + const c = cc > 255 ? 255 : cc; | ||
83 | + return c.toString(16).length > 1 ? c.toString(16) : `0${c.toString(16)}`; | ||
84 | +}; | ||
85 | + | ||
86 | +/** | ||
87 | + * Calculates luminance of an rgb color | ||
88 | + * @param {number} r red | ||
89 | + * @param {number} g green | ||
90 | + * @param {number} b blue | ||
91 | + */ | ||
92 | +const luminanace = (r: stri, g: number, b: number) => { | ||
93 | + const a = [r, g, b].map((v) => { | ||
94 | + v /= 255; | ||
95 | + return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); | ||
96 | + }); | ||
97 | + return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722; | ||
98 | +}; | ||
99 | + | ||
100 | +/** | ||
101 | + * Calculates contrast between two rgb colors | ||
102 | + * @param {string} rgb1 rgb color 1 | ||
103 | + * @param {string} rgb2 rgb color 2 | ||
104 | + */ | ||
105 | +const contrast = (rgb1: string[], rgb2: number[]) => | ||
106 | + (luminanace(rgb1[0], ~~rgb1[1], ~~rgb1[2]) + 0.05) / | ||
107 | + (luminanace(rgb2[0], rgb2[1], rgb2[2]) + 0.05); | ||
108 | + | ||
109 | +/** | ||
110 | + * Determines what the best text color is (black or white) based con the contrast with the background | ||
111 | + * @param hexColor - Last selected color by the user | ||
112 | + */ | ||
113 | +export const calculateBestTextColor = (hexColor: string) => { | ||
114 | + const rgbColor = hexToRGB(hexColor.substring(1)); | ||
115 | + const contrastWithBlack = contrast(rgbColor.split(','), [0, 0, 0]); | ||
116 | + | ||
117 | + return contrastWithBlack >= 12 ? '#000000' : '#FFFFFF'; | ||
118 | +}; | ||
119 | + | ||
120 | +/** | ||
121 | + * Subtracts the indicated percentage to the R, G or B of a HEX color | ||
122 | + * @param {string} color The color to change | ||
123 | + * @param {number} amount The amount to change the color by | ||
124 | + * @returns {string} The processed part of the color | ||
125 | + */ | ||
126 | +const subtractLight = (color: string, amount: number) => { | ||
127 | + const cc = parseInt(color, 16) - amount; | ||
128 | + const c = cc < 0 ? 0 : cc; | ||
129 | + return c.toString(16).length > 1 ? c.toString(16) : `0${c.toString(16)}`; | ||
130 | +}; |