摘要
在企业级应用中,数据可视化是一个非常重要的需求。如何设计一个既灵活又易用的图表组件,能够满足不同业务场景的需求,同时又能保持良好的可维护性?本文将分享一个基于Vue + ECharts的企业级图表组件设计方案,通过该方案,我们可以轻松实现数据大屏、统计报表等多种业务场景的图表展示需求。
一、组件设计思路
1. 核心特点
-
高度复用:通过配置化方式,支持折线图、柱状图、饼图等多种图表类型
-
灵活扩展:支持顶部卡片、图表组合等多种布局方式
-
数据联动:支持同比、环比等业务数据的联动展示
-
交互友好:内置搜索、重置、加载等交互体验
2. 组件结构AskCopyApply to index.vue├── personnelChart/│ ├── common/ # 公共图表组件│ │ ├── index.vue # 组件主文件│ │ └── index.scss # 样式文件│ └── overview/ # 业务场景实现│ └── index.vue # 概览页面
overview页面里面只用做参数配置和引用当前组件就可以
这就是这个页面的全部代码了,是不是很简洁?
<!-- 使用后:只需要配置数据 -->
<template>
<Common
:propTopList="topList"
:propChartList="chartList"
type="overview"
@changeProp="changeProp"
/>
</template>
<script>
export default {
data() {
return {
topList: [],
chartList: [
{
id: 'statusEmployees',
title: '在职员工情况',
type: 'line',
// ... 更多配置
}
]
}
}
}
</script>
为什么要封装这个组件
需求高度相似的图表展示页面:
-
都需要收索选择
-
都有顶部数据卡片
-
都有图表联动
仔细分析后让我觉得有偷懒的可乘之机。
通过配置化的方式,将图表定义转换为数据结构:
2. 配置灵活性,可扩展的数据结构,能定义图表,嵌套图表,支持搜索。
- 支持多种图表类型
{
type: 'line', // 折线图
type: 'bar', // 柱状图
type: 'pie', // 饼图
// 还可以扩展,支持更多图表类型
}
支持数据卡片
topList: [{
type: 'num', // 数字展示
type: 'pie', // 迷你图表
// 支持更多展示类型
}]
支持图表嵌套
{
id: 'parentChart',
// 顶部嵌套图表
topList: [{
type: 'pie',
// 子图表配置
}],
// 主图表配置
type: 'line'
}
二、实现要点
1. 配置驱动设计
// 图表配置结构
{
// 基础信息
id: String, // 图表唯一标识
title: String, // 图表标题
type: String, // 图表类型
height: Number, // 图表高度
// 数据处理
nonStandard: String, // 特殊数据处理标识
// 顶部卡片
topList: Array, // 顶部指标配置
// 图表配置
legend: Array, // 图例配置
xAxis: Array, // X轴配置
yAxis: Array, // Y轴配置
series: Array, // 数据系列
// 交互配置
dataZoom: Array, // 数据缩放
select: Object // 选择器配置
}
2. 组件接口设计
// Props定义
props: {
propChartList: Array, // 图表配置列表
propTopList: Array, // 顶部卡片配置
type: String, // 业务类型
},
// 事件定义
events: {
changeProp: Function, // 配置变更
search: Function, // 搜索触发
}
3. 性能优化
// 1. 图表实例缓存
const chartInstances = new Map()
// 2. 按需渲染
<el-card v-if="item.visible">
<div :id="item.id" class="chart-box"></div>
</el-card>
// 3. 数据更新优化
updateChart(chart, data) {
const options = this.getOptions(data)
chart.setOption(options, true) // 只更新数据
三、核心实现逻辑
xport default {
name: 'personnelMatters-personnelChart-common',
props: {
propChartList: Array,
propTopList: Array,
type: String
},
data() {
return {
loading: false,
searchForm: {
organizationId: undefined,
startTime: '',
endTime: '',
},
organizeIdTree: undefined,
timeList: [],
time: '',
chartInstances: new Map(), // 图表实例缓存
}
},
mounted() {
this.initCharts()
window.addEventListener('resize', this.resizeCharts)
},
beforeDestroy() {
window.removeEventListener('resize', this.resizeCharts)
this.destroyCharts()
},
methods: {
// 1. 图表初始化
initCharts() {
this.$nextTick(() => {
// 初始化顶部卡片图表
this.propTopList.forEach(item => {
if (item.type === 'pie') {
const chart = this.createChart(item)
this.chartInstances.set(item.id, chart)
}
})
// 初始化主图表
this.propChartList.forEach(item => {
const chart = this.createChart(item)
this.chartInstances.set(item.id, chart)
})
})
},
// 2. 创建图表实例
createChart(config) {
const el = document.getElementById(config.id)
if (!el) return null
const chart = echarts.init(el)
const options = this.generateOptions(config)
chart.setOption(options)
return chart
},
// 3. 生成图表配置
generateOptions(config) {
const options = {
tooltip: {
trigger: 'axis'
},
legend: {
data: config.legend || []
},
xAxis: config.xAxis,
yAxis: config.yAxis,
series: config.series,
}
// 处理数据缩放
if (config.dataZoom) {
options.dataZoom = config.dataZoom
}
// 处理饼图特殊配置
if (config.type === 'pie') {
options.series = [{
type: 'pie',
radius: config.fullDraw ? '100%' : '50%',
data: config.data
}]
}
return options
},
// 4. 更新图表数据
updateChartData(id, data) {
const chart = this.chartInstances.get(id)
if (!chart) return
const config = this.propChartList.find(item => item.id === id)
if (!config) return
// 处理数据
const processedData = this.processData(data, config)
// 更新配置
const options = this.generateOptions({
...config,
series: processedData
})
chart.setOption(options, true)
},
单个图表的渲染流程:请求数据 --> 数据校对 --> 将数据导入图表校对 --> 生成图表渲染数据 --> 渲染图表 ,循环。 其中需要注意的是在一次渲染循环完成前不能重新点击搜索,防止重复请求。
if (index == this.chartList.length - 1) {
this.$nextTick(() => {
//把新设置的值传回父组件,保持单项数据流。
this.$emit('changeProp', this.chartList, this.topList)
//用loading控制提交按钮。
this.loading = false;
})
resolve()
}
四、关键实现细节
1. 数据处理机制
因为对接的后端开发习惯不同,对返回值需要做特殊处理,以方便套用现在的模板。
// 标准数据处理
processStandardData(data, config) {
return config.series.map(serie => {
return {
...serie,
data: data[serie.keyName] || []
}
})
}
// 非标准数据处理
processNonStandardData(data, config) {
switch(config.nonStandard) {
case 'wageCostSituation':
return this.processWageCostData(data, config)
case 'attendance':
return this.processAttendanceData(data, config)
default:
return this.processStandardData(data, config)
}
}
2. 图表缓存机制
// 图表实例管理
class ChartInstanceManager {
constructor() {
this.instances = new Map()
}
get(id) {
return this.instances.get(id)
}
set(id, chart) {
if (this.instances.has(id)) {
this.instances.get(id).dispose()
}
this.instances.set(id, chart)
}
clear() {
this.instances.forEach(chart => chart.dispose())
this.instances.clear()
}
}
五、收益总结
-
开发效率
-
新页面开发时间:从3天缩短到0.5天
-
代码量:减少70%以上
-
维护成本:显著降低
-
统一的代码规范
-
更少的Bug
-
更好的可维护性
希望这次分享能像一杯浓咖啡一样,给你的代码实现带来灵感的提神效果!如果你觉得有用,别忘了给个赞哦,毕竟赞就是我编程的动力,像代码中的注释一样重要!😄👍