Commit 4baf90a5c87493939830129efaa146624faabbcc

Authored by vben
1 parent 43929174

perf: optimize tab switching speed

CHANGELOG.zh_CN.md
@@ -7,6 +7,7 @@ @@ -7,6 +7,7 @@
7 ### ⚡ Performance Improvements 7 ### ⚡ Performance Improvements
8 8
9 - 优化 settingDrawer 代码 9 - 优化 settingDrawer 代码
  10 +- 优化多标签页切换速度
10 11
11 ### 🐛 Bug Fixes 12 ### 🐛 Bug Fixes
12 13
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 +};