- A+
所属分类:Web前端
项目中用到了很多echart图表,进行了简单的组件封装,主要包含以下功能:
- 创建图表实例,渲染图表
- 支持传入自定义函数,可拿到图表实例,实现个性化功能
- 支持配置更新后图表自动刷新,可配置是清空后再刷新
- loading状态控制
- resize时图表更新
- 支持饼图默认高亮功能
实现
资源引入
- echart资源按需引入
- 第三方组件引入(echarts-liquidfill,水波纹图表)
/* 即下文中的 @/modules/echartPlugin */ // https://echarts.apache.org/handbook/zh/basics/import#%E6%8C%89%E9%9C%80%E5%BC%95%E5%85%A5-echarts-%E5%9B%BE%E8%A1%A8%E5%92%8C%E7%BB%84%E4%BB%B6 import * as echarts from "echarts/core"; import { BarChart, // 系列类型的定义后缀都为 SeriesOption BarSeriesOption, PieChart, PieSeriesOption, LineChart, LineSeriesOption, LinesChart, LinesSeriesOption, EffectScatterChart, EffectScatterSeriesOption, } from "echarts/charts"; import { TitleComponent, // 组件类型的定义后缀都为 ComponentOption TitleComponentOption, TooltipComponent, TooltipComponentOption, DatasetComponent, DatasetComponentOption, GridComponent, GridComponentOption, DataZoomComponent, DataZoomComponentOption, LegendComponent, LegendComponentOption, GeoComponent, GeoComponentOption, } from "echarts/components"; import { CanvasRenderer } from "echarts/renderers"; import "echarts-liquidfill"; // 通过 ComposeOption 来组合出一个只有必须组件和图表的 Option 类型 export type ECOption = echarts.ComposeOption< | BarSeriesOption | TitleComponentOption | TooltipComponentOption | GridComponentOption | DatasetComponentOption | DataZoomComponentOption | PieSeriesOption | LegendComponentOption | GeoComponentOption | LinesSeriesOption | LineSeriesOption | EffectScatterSeriesOption >; // https://www.npmjs.com/package/echarts-liquidfill export interface LiquidFillOption { series: { type: "liquidFill"; data: number[]; color?: string[]; radius?: string; center?: [string, string]; label?: { color?: string; insideColor?: string; fontSize?: number; formatter?: (param: { borderColor: string; color: string; data: number; dataIndex: number; dataType: undefined; name: string; value: number; }) => string | number; }; shape?: | "circle" | "rect" | "roundRect" | "triangle" | "diamond" | "pin" | "arrow"; [name: string]: unknown; }[]; [name: string]: unknown; } // 注册必须的组件 echarts.use([ TitleComponent, TooltipComponent, GridComponent, BarChart, LinesChart, CanvasRenderer, DatasetComponent, DataZoomComponent, PieChart, LegendComponent, GeoComponent, LineChart, EffectScatterChart, ]); export default echarts;
组件封装
<template> <div class="h-echart-wrapper" ref="chartWrapperDom"> <div class="h-echart" ref="chartDom">loading</div> </div> </template> <script lang="ts" src="./index.ts"></script> <style lang="less" scoped> .h-echart-wrapper { height: 100%; } .h-echart { height: 100%; width: 100%; text-align: center; } </style>
import { defineComponent, onMounted, onUnmounted, PropType, ref, watch, toRaw, } from "vue"; import echarts, { ECOption, LiquidFillOption } from "@/modules/echartPlugin"; import ResizeObserver from "resize-observer-polyfill"; export default defineComponent({ name: "h-echart", props: { // echart配置 options: { type: Object as PropType<ECOption | LiquidFillOption>, required: true, }, // 饼图是否需要默认高亮 needDefaultHighLight: { type: Boolean, default: false, }, loading: Boolean, // 自定义函数,会暴露echart实例出去,可以实现个性化操作 customFn: Function as PropType< (echartInstance: null | echarts.ECharts) => void >, // 更新图表之前是否先清空 clearBeforeUpdate: Boolean, }, setup(props) { const chartWrapperDom = ref<null | HTMLElement>(null); const chartDom = ref<null | HTMLElement>(null); // WARN: echarts5 实例用响应式对象存放时会导致功能tooltip功能异常 let echartInstance: null | echarts.ECharts = null; let chartWrapperResize: null | ResizeObserver = null; let highlightName: string | null = null; let firstRender = true; const setOptions = (options?: ECOption | LiquidFillOption) => { echartInstance && options && echartInstance.setOption(toRaw(options), { notMerge: true, }); if (props.needDefaultHighLight && firstRender) { firstRender = false; const _options = props.options as ECOption; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore if (_options.series && _options.series[0] && _options.series[0].data) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const name = _options.series[0].data[0].name as string; setTimeout(() => { // 默认高亮 echartInstance && echartInstance.dispatchAction({ type: "highlight", seriesIndex: 0, name, }); highlightName = name; }, 600); } } }; watch( () => props.loading, (newLoading) => { if (newLoading !== undefined && echartInstance) { newLoading ? echartInstance.showLoading({ textColor: "rgb(255 255 255 / 0%)", showSpinner: false, zlevel: 0, }) : echartInstance.hideLoading(); } } ); const init = () => { chartDom.value && (echartInstance = echarts.init(chartDom.value)); props.customFn && props.customFn(echartInstance); if (props.needDefaultHighLight && echartInstance) { echartInstance.on("mouseover", function (e) { if (e.name !== highlightName) { echartInstance!.dispatchAction({ type: "downplay", seriesIndex: 0, name: highlightName, }); } }); echartInstance.on("mouseout", function (e) { highlightName = e.name; echartInstance!.dispatchAction({ type: "highlight", seriesIndex: 0, name: e.name, }); }); } setOptions(props.options); }; onMounted(() => { // 初始化图表实例 setTimeout(init, 300); // 观察包裹层变化,进行图表resize if (chartWrapperDom.value) { chartWrapperResize = new ResizeObserver(() => { echartInstance && echartInstance.resize(); }); chartWrapperResize.observe(chartWrapperDom.value); } }); // 观察者清理 onUnmounted(() => { chartWrapperResize?.disconnect(); }); watch( () => props, // 配置变化,重新设置 (newVal) => { if (newVal.clearBeforeUpdate) { echartInstance && echartInstance.clear(); } setOptions(toRaw(newVal.options)); }, { immediate: true, deep: true } ); return { chartDom, chartWrapperDom, }; }, });
组件注册及全局类型声明
/* ./components/index.ts */ import { App } from "vue"; import HEchart from "./h-echart"; import HIframeKeepAlive from "./h-iframe-keep-alive/index.vue"; export default function useCompoments(app: App<Element>) { app && app.component && [ HEchart, HIframeKeepAlive, ].forEach((_component) => { app.component(_component.name, _component); }); } // 声明全局组件类型 // https://github.com/johnsoncodehk/volar/blob/master/extensions/vscode-vue-language-features/README.md declare module "@vue/runtime-core" { export interface GlobalComponents { HEchart: typeof HEchart; HIframeKeepAlive: typeof HIframeKeepAlive; } }
import useCompoments from "./components"; const app = createApp(App).use(router); tempApp = app; // 注册所自定义组件 useCompoments(app);
使用
<div class="chart-wrapper"> <h-echart :options="boardPieOptions" needDefaultHighLight /> </div>
const boardPieOptions = computed(() => { return getBoardPieOptions(props.arrivalNodeStats.types); });