数据大屏:封装多类型 ECharts 图表组件(折线图、柱状图、地图等)

在大屏数据可视化项目中,我们要用到折线图、柱状图、饼图、地图等十几种 ECharts 图表,但一开始直接写原生 ECharts 会遇到三个致命问题:

  1. 重复代码多:每个图表都要写「创建实例、绑定事件、监听窗口 resize、销毁实例」的代码,一个项目里重复写几十次,既费时间又容易出错;
  2. 样式不统一:不同开发人员写的图表,坐标轴颜色、tooltip 样式、边距都不一样,大屏视觉效果乱糟糟;
  3. 大数据卡顿:大屏经常加载上千条数据,原生 ECharts 默认的动画、高亮效果会让页面帧率暴跌,用户体验差。

所以我封装了统一的多类型 ECharts 组件,核心目标就三个:减少重复代码、统一图表样式、优化大屏性能。整体思路是 “做一个通用的 ECharts 基础壳子,再给不同图表(折线 / 柱状 / 地图)做预设样式和数据适配”,业务开发时只需要传数据,不用关心底层逻辑。

第一部分:先做通用基础组件(BaseEchart)—— 解决所有图表的共性问题

这是封装的核心,相当于给所有图表做一个 “通用模板”,把实例创建、销毁、自适应、事件绑定这些所有图表都需要的逻辑,一次性写好,后续所有图表都基于这个模板来扩展。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<template>
<!-- 图表容器:必须有宽高,支持自定义样式 -->
<div ref="chartDom" class="base-echart" :style="{ width, height, ...containerStyle }"></div>
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue';
import * as echarts from 'echarts';
import { useDebounceFn } from '@vueuse/core';
import type { ECharts, EChartsOption } from 'echarts';

// 定义通用Props:所有图表都需要的配置,比如尺寸、自定义配置、事件
const props = defineProps({
// 容器尺寸:支持100%、400px这种格式
width: { type: [String, Number], default: '100%' },
height: { type: [String, Number], default: '400px' },
// 自定义ECharts配置:业务层可以覆盖预设样式
customOption: { type: Object as () => EChartsOption, default: () => ({}) },
// 图表事件:比如click、mouseover,格式是[{type: 'click', handler: fn}]
events: { type: Array, default: () => [] },
// 容器样式:自定义背景、边框等
containerStyle: { type: Object, default: () => ({}) },
// 是否开启窗口resize自适应
autoResize: { type: Boolean, default: true },
});

// 存储ECharts实例和DOM节点
const chartDom = ref<HTMLDivElement | null>(null);
const chartInstance = ref<ECharts | null>(null);
</script>

<style scoped>
.base-echart {
box-sizing: border-box;
}
</style>

第二部分:封装具体图表组件(折线图、柱状图、地图)—— 做个性化适配

基于基础组件,给不同图表做预设样式、数据处理、专属配置。这里以 ** 折线图(LineChart)地图(MapChart)** 为例,讲清楚 “怎么把基础组件变成具体图表”。

示例 1:折线图组件(LineChart.vue)—— 最常用的图表类型

核心是:预设大屏常用的折线图样式,把业务数据转换成 ECharts 能识别的格式,支持少量个性化配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
<template>
<!-- 直接复用基础组件,把所有属性传过去 -->
<BaseEchart
v-bind="$attrs"
ref="lineChartRef"
:customOption="customOption"
/>
</template>

<script setup lang="ts">
import { ref, computed, onMounted, defineProps } from 'vue';
import BaseEchart from './BaseEchart.vue';
import type { EChartsOption } from 'echarts';

// 定义折线图专属Props:业务数据+个性化配置
const props = defineProps({
// 业务数据:格式是[{x: '00:00', y: 80}, ...],简单易懂
data: { type: Array, default: () => [] },
// 自定义配置:覆盖预设样式
customOption: { type: Object as () => EChartsOption, default: () => ({}) },
// 折线图专属:是否显示面积、是否平滑、主题色
showArea: { type: Boolean, default: false },
smooth: { type: Boolean, default: true },
themeColor: { type: String, default: '#409eff' }, // 大屏主色调
});

// 获取基础组件的实例,用来重写updateChartOption方法
const lineChartRef = ref<InstanceType<typeof BaseEchart> | null>(null);

// 计算折线图的预设配置(大屏统一样式)
const baseOption = computed(() => {
return {
// 大屏常用的边距:避免图表挤在角落
grid: { top: 20, right: 20, bottom: 30, left: 40 },
// tooltip:防抖+惰性更新,解决高频触发卡顿
tooltip: {
trigger: 'axis',
triggerDelay: 100,
lazyUpdate: true,
},
// X轴:用业务数据的x值,统一灰色样式
xAxis: {
type: 'category',
data: props.data.map((item: any) => item.x),
axisLine: { lineStyle: { color: '#ccc' } },
},
// Y轴:统一灰色样式
yAxis: {
type: 'value',
axisLine: { lineStyle: { color: '#ccc' } },
},
// 系列数据:把业务数据的y值转成ECharts格式
series: [
{
type: 'line',
data: props.data.map((item: any) => item.y),
smooth: props.smooth,
// 面积样式:半透明主题色
areaStyle: props.showArea ? { color: props.themeColor + '20' } : undefined,
lineStyle: { color: props.themeColor, width: 2 },
itemStyle: { color: props.themeColor },
// 大数据量时关闭高亮:解决1000+数据时hover卡顿
emphasis: { disabled: props.data.length > 1000 },
},
],
// 大数据量时关闭动画:避免渲染卡顿
animation: props.data.length < 1000,
// 空数据时显示“暂无数据”:提升用户体验
graphic: props.data.length === 0
? {
type: 'text',
left: 'center',
top: 'center',
style: { text: '暂无数据', fontSize: 14, color: '#909399' },
}
: undefined,
} as EChartsOption;
});

// 组件挂载后,重写基础组件的updateChartOption方法
onMounted(() => {
if (lineChartRef.value) {
// 核心:合并预设配置和自定义配置,传给ECharts
lineChartRef.value.updateChartOption = () => {
const finalOption = { ...baseOption.value, ...props.customOption };
lineChartRef.value?.chartInstance?.setOption(finalOption);
};
}
});
</script>

第三部分:业务层怎么用?—— 极简调用,开发效率拉满

封装完之后,业务开发人员只用写几行代码,就能生成一个样式统一、性能优化的图表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<template>
<div class="chart-container" style="width: 100%; height: 400px;">
<!-- 折线图:只传数据和少量配置 -->
<LineChart
:data="lineData"
:showArea="true"
themeColor="#67c23a"
:events="[{ type: 'click', handler: handleChartClick }]"
/>

<!-- 地图:只传省份数据 -->
<MapChart
:data="mapData"
mapType="china"
style="width: 100%; height: 600px;"
/>
</div>
</template>

<script setup>
// 业务数据:后端返回的格式直接用,不用转换
const lineData = ref([
{ x: '00:00', y: 80 },
{ x: '01:00', y: 85 },
{ x: '02:00', y: 78 },
]);

const mapData = ref([
{ name: '北京', value: 120 },
{ name: '上海', value: 150 },
{ name: '广东', value: 180 },
]);

// 图表点击事件
const handleChartClick = (params: any) => {
console.log('点击了折线图:', params);
};
</script>