基于Echarts进行图表组件的封装
什么是Echarts
是一个使用js实现的开源可视库,提供了多种图表,但是当我们在项目中进行使用的时候可能就是需要进行一系列的相关配置如: 标题,类型,x轴,y轴等,当我们使用较为频繁的时候就容易导致代码的冗余,并且整体将echarts进行安装引入也是比较大的,我们可以按照自己的需要进行对应组件的引入
Echarts的使用
安装Echarts
Echarts官网-安装echarst并实现按需引入Echarts图表和组件
基于Echarts相关配置进行组件的封装
// components/BarChart.vue
/* @/components/BarChart.vue */<template><div ref="chartDom" :style="{ height: getHeight }"></div>
</template><script setup lang="ts">
import { echarts, type ECOption } from '@/utils/echarts'
import { ref, shallowRef, watch, computed, onMounted, onBeforeUnmount, type ShallowRef, type Ref } from 'vue'
import type { EChartsType } from 'echarts/types/dist/core'
import type {XAXisOption, YAXisOption, LegendComponentOption, BarSeriesOption, DataZoomComponentOption
} from 'echarts/types/dist/shared'
import resize from '@/utils/resize'
import type { ChartSetting } from '@/types/ChartData'//定义组件属性
const props = withDefaults(defineProps<{//数据data?: Array<string | number>//x轴数据xAxisData?: Array<string>//图表标题title?: string//系列配置series?: Array<BarSeriesOption>//x轴配置xAxis?: Array<XAXisOption>//y轴配置yAxis?: Array<YAXisOption>//图例配置legend?: LegendComponentOption//区域缩放配置dataZoom?: Array<DataZoomComponentOption>//图形高度height?: number | string//数据集datasetSource?: Array<any>//综合配置options: ChartSetting}>(),{data: () => [],xAxisData: () => [],title: 'ECharts柱状图',}
)
//要渲染的Dom元素
const chartDom: Ref<HTMLDivElement | null> = ref(null)
//渲染的chart对象要用shallowRef
const chart: ShallowRef<EChartsType | null | undefined> = shallowRef(null)
//高度同时支持string和number
const getHeight = computed(() => {return typeof props.height === 'number' ? props.height + 'px' : props.height
})
//监听数据变化,重新绘制
watch(() => props,() => {drawChart()},{ deep: true }
)//绘制
async function drawChart() {let datasetSource: Array<any> | undefined = props.datasetSource,series: Array<BarSeriesOption> = [],xAxisData: Array<string> = props.xAxisDatalet chartType = props.options.chartType || 'bar'; // 默认为柱状图,如果需要折线图则设置为 'line'if (props.options) {if (props.options.apiMethod) {//获取接口数据作为数据集let allx = await props.options.apiMethod()datasetSource = allx.dataif (props.options.xProp) {//根据配置的x轴属性名生成x轴数据xAxisData = []datasetSource?.forEach(data => {xAxisData.push(data[props.options.xProp])})}}if (props.options.seriesOption) {props.options.seriesOption.forEach((opt: any) => {series.push({name: '车牌',barMaxWidth: 30,emphasis: { focus: 'series' },label: { show: true, position: 'top', color: 'inherit' },...opt,type: chartType, // 设置图表类型})})}}// else {// series = props.series ? props.series : [{// name: '车牌',// type: 'bar',// barMaxWidth: 30,// emphasis: { focus: 'self' },// label: { show: true, position: 'inside', color: '#fff' },// data: props.data// }]// }let xAxis: Array<XAXisOption> = props.xAxis ? props.xAxis : [{type: 'category',axisTick: { show: false },data: xAxisData}]let yAxis: Array<YAXisOption> = props.yAxis ? props.yAxis : [{ type: 'value', minInterval: 1 }]let legend: LegendComponentOption = props.legend ? props.legend : {show: true,type: 'scroll',orient: 'horizontal',top: 25,left: 'center'}let dataZoom: Array<DataZoomComponentOption> = props.dataZoom ? props.dataZoom : []const options: ECOption = {backgroundColor: '',title: {text: props.title},tooltip: {trigger: 'axis',axisPointer: {type: 'shadow'},// appendToBody:true},legend: legend,grid: {left: 10,right: 10,bottom: props.dataZoom ? 40 : 10,containLabel: true},toolbox: {show: true,feature: {magicType: { type: ['line', 'bar'] },dataView: { readOnly: false },saveAsImage: {}}},xAxis: xAxis,yAxis: yAxis,dataZoom: dataZoom,dataset: {source: datasetSource},series: series}//开启notMerge保证配置数据不会叠加chart.value?.setOption(options, { notMerge: true });
}const { chartObject, addResize, removeResize } = resize()
onMounted(() => {chart.value = echarts.init(chartDom.value);drawChart()//添加窗口自适应chartObject.value = chart.valueaddResize()
})onBeforeUnmount(() => {removeResize()chart.value?.dispose()
})
</script>
在父组件中使用封装好的组件
<template><el-row :gutter="16"><el-col v-for="(item, index) in chartOptionList" :key="index" :lg="12" style="margin-bottom: 10px;"><el-card><BarChart :title="item.title" :height="item.height || 300" :options="item.chartOption" /></el-card></el-col></el-row>
</template><script setup lang="ts">
import BarChart from '@/components/BarChart.vue'
import type { ChartSetting } from '@/types/ChartData'
import { ref } from 'vue'
//axios api请求方法
import { getPayRecord, delicacies } from '@/services/http'interface ChartCard {//标题title?: string,//高度height?: number,//图表y轴配置yAxis?: Array<any>chartOption: ChartSetting
}
const chartOptionList = ref<ChartCard[]>([{title: '图1',chartOption: {apiMethod: () => getPayRecord(),xProp: 'name',chartType: 'bar', // 设置为 'line' 以生成折线图seriesOption: [{ name: '数量', encode: { x: 'name', y: 'count' } }]}},{title: '图2',chartOption: {apiMethod: () => delicacies(),xProp: 'name',chartType: 'line', // 设置为 'line' 以生成折线图seriesOption: [{ name: '销量', encode: { x: 'name', y: 'saleNum' } },{ name: '好评', encode: { x: 'name', y: 'positiveReviews' } },]}},
])
</script>
效果展示
其他:
// types.ChartData.ts
export interface SeriesData {name?: stringdata?: number[]color?: stringyAxisIndex?: numberradius?: string | string[]itemStyle?: anyencode?: {x?: stringy?: stringitemName?: stringvalue?: string}
}export interface ChartSetting {//api接口方法apiMethod: Function// x轴属性名xProp: stringchartType: string|undefined // 设置为 'line' 以生成折线图//图例配置seriesOption: SeriesData[]
}
// reqTypes.ts
import axiosInstance from '../utils/request'export interface ApiResult<T> {code: numbermessage: stringdata: T
}
export async function get<T>(url: string, params?: any): Promise<ApiResult<T>> {const response = await axiosInstance.get<ApiResult<T>>(url, { params })return response.data
}
export async function post<T>(url: string, data?: any): Promise<ApiResult<T>> {const response = await axiosInstance.post<ApiResult<T>>(url, data)return response.data
}
export async function put<T>(url: string, data?: any): Promise<ApiResult<T>> {const response = await axiosInstance.put<ApiResult<T>>(url, data)return response.data
}
export async function del<T>(url: string, params?: any): Promise<ApiResult<T>> {const response = await axiosInstance.delete<ApiResult<T>>(url, { params })return response.data
}// utils.echarts.ts
/* @/utils/echarts.ts */
/**在ts中实现按需引入echarts 图表和组件*/
import * as Echarts from 'echarts/core'
import { BarChart, PieChart, LineChart } from 'echarts/charts'
import {// 标题组件TitleComponent,// 图例组件LegendComponent,// 提示框组件TooltipComponent,// 坐标系网格组件GridComponent,// 数据集组件DatasetComponent,// 内置数据转换器组件 (filter, sort)TransformComponent,// 工具栏组件ToolboxComponent,// 区域缩放组件DataZoomComponent,// 原生图形元素组件
} from 'echarts/components'
// 标签自动布局、全局过渡动画等特性
import { LabelLayout, UniversalTransition } from 'echarts/features'
// 引入 Canvas 渲染器,注意引入 CanvasRenderer 或者 SVGRenderer 是必须的一步
// 使用canvas进行渲染 也可以使用 SVGRenderer 进行渲染
import { CanvasRenderer } from 'echarts/renderers'
import type {// 系列类型的定义后缀都为 SeriesOptionBarSeriesOption, // 柱状图PieSeriesOption, //饼图LineSeriesOption, // 折线/面积图
} from 'echarts/charts'
import type {// 组件类型的定义后缀都为 ComponentOptionTitleComponentOption,TooltipComponentOption,GridComponentOption,DatasetComponentOption,ToolboxComponentOption,DataZoomComponentOption,GraphicComponentOption,
} from 'echarts/components'
import type { ComposeOption } from 'echarts/core'// 注册必须的组件
Echarts.use([TitleComponent,LegendComponent,TooltipComponent,GridComponent,DatasetComponent,TransformComponent,ToolboxComponent,DataZoomComponent,GridComponent,LabelLayout,UniversalTransition,CanvasRenderer,BarChart,PieChart,LineChart,
])// 通过 ComposeOption 来组合出一个只有必须组件和图表的 Option 类型
export type ECOption = ComposeOption<| BarSeriesOption| PieSeriesOption| LineSeriesOption| TitleComponentOption| TooltipComponentOption| GridComponentOption| DatasetComponentOption| ToolboxComponentOption| DataZoomComponentOption| GraphicComponentOption
>export const echarts = Echarts
// request.ts 这里的baseUrl 是基于EasyMock进行模拟的数据
/*** 使用 axios.create() 创建了一个 axios 实例,并设置了基本 URL 和请求超时时间。我们还添加了请求和响应拦截器**/
import axios, {AxiosInstance,AxiosResponse,InternalAxiosRequestConfig,
} from 'axios'
const axiosInstance: AxiosInstance = axios.create({baseURL:'https://mock.presstime.cn/mock/6686990ecb2f4f1158f2a7b8/screen-big-sys',timeout: 5000,
})
// 添加请求拦截器
// 自定义请求头
axiosInstance.interceptors.request.use((config: InternalAxiosRequestConfig) => {// 在发送请求之前做些什么const token = localStorage.getItem('ACCESS_TOKEN')if (token) {// 配置请求头config.headers.Authorization = 'Bearer ' + token}return config},(error: any) => {// 处理请求错误return Promise.reject(error)}
)
// 添加响应拦截器
axiosInstance.interceptors.response.use((response: AxiosResponse) => {// 对响应数据做点什么return response},(error: any) => {// 处理响应错误return Promise.reject(error)}
)
export default axiosInstance
// resize.ts
/*** 实现页面大小的自适应* ECharts提供的API会发现,它提供了一个resize 方法 重新渲染图表结合window.addEventListener* 功能:echarts图表自适应窗口变化封装方法*/
//echarts图表自适应窗口变化封装方法
import { ref } from 'vue'
import { debounce } from 'lodash'export default function () {//echarts图的实例const chartObject = ref()//使用防抖debounce函数,减少resize的次数const chartResizeHandler = debounce(() => {if (chartObject.value) {chartObject.value.resize()}}, 100)const initResizeEvent = () => {//添加窗口大小变化监听window.addEventListener('resize', chartResizeHandler)}const destroyResizeEvent = () => {//移除窗口大小变化监听window.removeEventListener('resize', chartResizeHandler)}const addResize = () => {initResizeEvent()}const removeResize = () => {destroyResizeEvent()}return {chartObject,addResize,removeResize,}
}
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import ElementPlus from 'element-plus'
import { createPinia } from 'pinia'
import 'element-plus/dist/index.css'
// 实现持久化标记
import { createPersistedState } from 'pinia-plugin-persistedstate'const app = createApp(App)
const pinia = createPinia()
// 使用pinia-plugin-persistedstate插件
pinia.use(createPersistedState())app.use(ElementPlus)
app.mount('#app')