perf: 优化管理端生产加载体验

- 图标与重型组件按需加载,优化菜单预取和页面请求链路

- 开启 gzip 与静态缓存并修复子路由 KeepAlive 冲突
This commit is contained in:
2026-07-22 20:35:55 +08:00
parent 9f06c238d3
commit 19059fde96
27 changed files with 750 additions and 233 deletions

View File

@@ -11,6 +11,10 @@
"types": "./src/echarts/index.ts",
"default": "./src/echarts/index.ts"
},
"./echarts/workspace": {
"types": "./src/echarts/workspace.ts",
"default": "./src/echarts/workspace.ts"
},
"./vxe-table": {
"types": "./src/vxe-table/index.ts",
"default": "./src/vxe-table/index.ts"

View File

@@ -0,0 +1,149 @@
import type { EChartsOption } from 'echarts';
import type { EChartsType } from 'echarts/core';
import type { Ref } from 'vue';
import type { Nullable } from '@easyflow/types';
import type EchartsUI from './echarts-ui.vue';
import { computed, nextTick, watch } from 'vue';
import { usePreferences } from '@easyflow/preferences';
import {
tryOnUnmounted,
useDebounceFn,
useResizeObserver,
useTimeoutFn,
useWindowSize,
} from '@vueuse/core';
type EchartsUIType = typeof EchartsUI | undefined;
interface EchartsEngine {
init: (element: HTMLElement) => EChartsType;
}
function createUseEcharts(echarts: EchartsEngine) {
return function useEcharts(chartRef: Ref<EchartsUIType>) {
let chartInstance: EChartsType | null = null;
let cacheOptions: EChartsOption = {};
const { isDark } = usePreferences();
const { height, width } = useWindowSize();
const resizeHandler: () => void = useDebounceFn(resize, 200);
const getChartEl = (): HTMLElement | null => {
const refValue = chartRef?.value as unknown;
if (!refValue) return null;
if (refValue instanceof HTMLElement) {
return refValue;
}
const maybeComponent = refValue as { $el?: HTMLElement };
return maybeComponent.$el ?? null;
};
const isElHidden = (el: HTMLElement | null): boolean => {
if (!el) return true;
return el.offsetHeight === 0 || el.offsetWidth === 0;
};
const getOptions = computed((): EChartsOption => {
if (!isDark.value) {
return {};
}
return {
backgroundColor: 'transparent',
};
});
const initCharts = () => {
const el = chartRef?.value?.$el;
if (!el) {
return;
}
chartInstance = echarts.init(el);
return chartInstance;
};
const renderEcharts = (
options: EChartsOption,
clear = true,
): Promise<Nullable<EChartsType>> => {
cacheOptions = options;
const currentOptions = {
...options,
...getOptions.value,
};
return new Promise((resolve) => {
if (chartRef.value?.offsetHeight === 0) {
useTimeoutFn(async () => {
resolve(await renderEcharts(currentOptions));
}, 30);
return;
}
nextTick(() => {
const el = getChartEl();
if (isElHidden(el)) {
useTimeoutFn(async () => {
resolve(await renderEcharts(currentOptions));
}, 30);
return;
}
useTimeoutFn(() => {
if (!chartInstance) {
const instance = initCharts();
if (!instance) return;
}
clear && chartInstance?.clear();
chartInstance?.setOption(currentOptions);
resolve(chartInstance);
}, 30);
});
});
};
function resize() {
const el = getChartEl();
if (isElHidden(el)) {
return;
}
chartInstance?.resize({
animation: {
duration: 300,
easing: 'quadraticIn',
},
});
}
watch([width, height], () => {
resizeHandler?.();
});
useResizeObserver(chartRef as never, resizeHandler);
watch(isDark, () => {
if (chartInstance) {
chartInstance.dispose();
initCharts();
renderEcharts(cacheOptions);
resize();
}
});
tryOnUnmounted(() => {
chartInstance?.dispose();
});
return {
renderEcharts,
resize,
getChartInstance: () => chartInstance,
};
};
}
export { createUseEcharts };
export type { EchartsUIType };

View File

@@ -1,146 +1,7 @@
import type { EChartsOption } from 'echarts';
import type { Ref } from 'vue';
import type { Nullable } from '@easyflow/types';
import type EchartsUI from './echarts-ui.vue';
import { computed, nextTick, watch } from 'vue';
import { usePreferences } from '@easyflow/preferences';
import {
tryOnUnmounted,
useDebounceFn,
useResizeObserver,
useTimeoutFn,
useWindowSize,
} from '@vueuse/core';
import echarts from './echarts';
import { createUseEcharts } from './use-echarts-base';
type EchartsUIType = typeof EchartsUI | undefined;
function useEcharts(chartRef: Ref<EchartsUIType>) {
let chartInstance: echarts.ECharts | null = null;
let cacheOptions: EChartsOption = {};
const { isDark } = usePreferences();
const { height, width } = useWindowSize();
const resizeHandler: () => void = useDebounceFn(resize, 200);
const getChartEl = (): HTMLElement | null => {
const refValue = chartRef?.value as unknown;
if (!refValue) return null;
if (refValue instanceof HTMLElement) {
return refValue;
}
const maybeComponent = refValue as { $el?: HTMLElement };
return maybeComponent.$el ?? null;
};
const isElHidden = (el: HTMLElement | null): boolean => {
if (!el) return true;
return el.offsetHeight === 0 || el.offsetWidth === 0;
};
const getOptions = computed((): EChartsOption => {
if (!isDark.value) {
return {};
}
return {
backgroundColor: 'transparent',
};
});
const initCharts = () => {
const el = chartRef?.value?.$el;
if (!el) {
return;
}
chartInstance = echarts.init(el);
return chartInstance;
};
const renderEcharts = (
options: EChartsOption,
clear = true,
): Promise<Nullable<echarts.ECharts>> => {
cacheOptions = options;
const currentOptions = {
...options,
...getOptions.value,
};
return new Promise((resolve) => {
if (chartRef.value?.offsetHeight === 0) {
useTimeoutFn(async () => {
resolve(await renderEcharts(currentOptions));
}, 30);
return;
}
nextTick(() => {
const el = getChartEl();
if (isElHidden(el)) {
useTimeoutFn(async () => {
resolve(await renderEcharts(currentOptions));
}, 30);
return;
}
useTimeoutFn(() => {
if (!chartInstance) {
const instance = initCharts();
if (!instance) return;
}
clear && chartInstance?.clear();
chartInstance?.setOption(currentOptions);
resolve(chartInstance);
}, 30);
});
});
};
function resize() {
const el = getChartEl();
if (isElHidden(el)) {
return;
}
chartInstance?.resize({
animation: {
duration: 300,
easing: 'quadraticIn',
},
});
}
watch([width, height], () => {
resizeHandler?.();
});
useResizeObserver(chartRef as never, resizeHandler);
watch(isDark, () => {
if (chartInstance) {
chartInstance.dispose();
initCharts();
renderEcharts(cacheOptions);
resize();
}
});
tryOnUnmounted(() => {
// 销毁实例,释放资源
chartInstance?.dispose();
});
return {
renderEcharts,
resize,
getChartInstance: () => chartInstance,
};
}
const useEcharts = createUseEcharts(echarts);
export { useEcharts };
export type { EchartsUIType };
export type { EchartsUIType } from './use-echarts-base';

View File

@@ -0,0 +1,21 @@
import { LineChart } from 'echarts/charts';
import {
GridComponent,
LegendComponent,
TooltipComponent,
} from 'echarts/components';
import * as echarts from 'echarts/core';
import { LabelLayout, UniversalTransition } from 'echarts/features';
import { CanvasRenderer } from 'echarts/renderers';
echarts.use([
LineChart,
TooltipComponent,
GridComponent,
LegendComponent,
LabelLayout,
UniversalTransition,
CanvasRenderer,
]);
export default echarts;

View File

@@ -0,0 +1,8 @@
import { createUseEcharts } from './use-echarts-base';
import workspaceEcharts from './workspace-echarts';
const useEcharts = createUseEcharts(workspaceEcharts);
export { default as EchartsUI } from './echarts-ui.vue';
export { useEcharts };
export type { EchartsUIType } from './use-echarts-base';