项目中经常用到echarts,不做封装直接拿来使用也行,但不可避免要写很多重复的配置代码,封装稍不注意又会过度封装,丢失了扩展性和可读性。始终没有找到一个好的实践,偶然看到一篇文章,给了灵感。找到了一个目前认为用起来很舒服的封装。
思路结合项目需求,针对不同类型的图表,配置基础的默认通用配置,例如x/y,label,图例等的样式创建图表组件实例(不要使用id,容易重复,还需要操作dom,直接用ref获取当前组件的el来创建图表),提供type(图表类型),和options(图表配置)两个必要属性根据传入type,加载默认的图表配置深度监听传入的options,变化时更新覆盖默认配置,更新图表提供事件支持,支持echart事件按需绑定交互注意要确保所有传入图表组件的options数组都是shallowreactive类型,避免数组量过大,深度响应式导致性能问题
目录结构├─v-charts│ │ index.ts // 导出类型定义以及图表组件方便使用│ │ type.d.ts // 各种图表的类型定义│ │ usecharts.ts // 图表hooks│ │ v-charts.vue // echarts图表组件│ ││ └─options // 图表配置文件│ bar.ts│ gauge.ts│ pie.ts
组件代码v-charts.vue<template> <div ref="chartref" /></template><script setup>import { proptype } from vue;import * as echarts from echarts/core;import { usecharts, charttype, chartsevents } from ./usecharts;/** * echarts事件类型 * 截至目前,vue3类型声明参数必须是以下内容之一,暂不支持外部引入类型参数 * 1. 类型字面量 * 2. 在同一文件中的接口或类型字面量的引用 * // 文档中有说明:https://cn.vuejs.org/api/sfc-script-setup.html#typescript-only-features */interface eventemitstype { <t extends chartsevents.eventtype>(e: `${t}`, event: chartsevents.events[uncapitalize<t>]): void;}defineoptions({ name: vcharts});const props = defineprops({ type: { type: string as proptype<charttype>, default: bar }, options: { type: object as proptype<echarts.echartscoreoption>, default: () => ({}) }});// 定义事件,提供ts支持,在组件使用时可获得友好提示defineemits<eventemitstype>();const { type, options } = torefs(props);const chartref = shallowref();const { charts, setoptions, initchart } = usecharts({ type, el: chartref });onmounted(async () => { await initchart(); setoptions(options.value);});watch( options, () => { setoptions(options.value); }, { deep: true });defineexpose({ $charts: charts});</script><style scoped>.v-charts { width: 100%; height: 100%; min-height: 200px;}</style>
usecharts.tsimport { charttype } from ./type;import * as echarts from echarts/core;import { shallowref, ref } from vue;import { titlecomponent, legendcomponent, tooltipcomponent, gridcomponent, datasetcomponent, transformcomponent} from echarts/components;import { barchart, linechart, piechart, gaugechart } from echarts/charts;import { labellayout, universaltransition } from echarts/features;import { canvasrenderer } from echarts/renderers;const optionsmodules = import.meta.glob<{ default: echarts.echartscoreoption }>(./options/**.ts);interface charthookoption { type?: ref<charttype>; el: shallowref<htmlelement>;}/** * 视口变化时echart图表自适应调整 */class chartsresize { #charts = new set<echarts.echarts>(); // 缓存已经创建的图表实例 #timeid = null; constructor() { window.addeventlistener(resize, this.handleresize.bind(this)); // 视口变化时调整图表 } getcharts() { return [...this.#charts]; } handleresize() { cleartimeout(this.#timeid); this.#timeid = settimeout(() => { this.#charts.foreach(chart => { chart.resize(); }); }, 500); } add(chart: echarts.echarts) { this.#charts.add(chart); } remove(chart: echarts.echarts) { this.#charts.delete(chart); } removelistener() { window.removeeventlistener(resize, this.handleresize); }}export const chartsresize = new chartsresize();export const usecharts = ({ type, el }: charthookoption) => { echarts.use([ barchart, linechart, barchart, piechart, gaugechart, titlecomponent, legendcomponent, tooltipcomponent, gridcomponent, datasetcomponent, transformcomponent, labellayout, universaltransition, canvasrenderer ]); const charts = shallowref<echarts.echarts>(); let options!: echarts.echartscoreoption; const getoptions = async () => { const modulekey = `./options/${type.value}.ts`; const { default: defaultoption } = await optionsmodules[modulekey](); return defaultoption; }; const setoptions = (opt: echarts.echartscoreoption) => { charts.value.setoption(opt); }; const initchart = async () => { charts.value = echarts.init(el.value); options = await getoptions(); charts.value.setoption(options); chartsresize.add(charts.value); // 将图表实例添加到缓存中 initevent(); // 添加事件支持 }; /** * 初始化事件,按需绑定事件 */ const attrs = useattrs(); const initevent = () => { object.keys(attrs).foreach(attrkey => { if (/^on/.test(attrkey)) { const cb = attrs[attrkey]; attrkey = attrkey.replace(/^on(chart)?/, ); attrkey = `${attrkey[0]}${attrkey.substring(1)}`; typeof cb === function && charts.value?.on(attrkey, cb as () => void); } }); }; onbeforeunmount(() => { chartsresize.remove(charts.value); // 移除缓存 }); return { charts, setoptions, initchart, initevent };};export const chartsoptions = <t extends echarts.echartscoreoption>(option: t) => shallowreactive<t>(option);export * from ./type.d;
type.d.ts/* * @description: * @version: 2.0 * @autor: gc * @date: 2022-03-02 10:21:33 * @lasteditors: gc * @lastedittime: 2022-06-02 17:45:48 */// import * as echarts from 'echarts/core';import * as echarts from 'echarts'import { xaxiscomponentoption, yaxiscomponentoption } from 'echarts';import { ecelementevent, selectchangedpayload, highlightpayload, } from 'echarts/types/src/util/types'import { titlecomponentoption, tooltipcomponentoption, gridcomponentoption, datasetcomponentoption, ariacomponentoption, axispointercomponentoption, legendcomponentoption,} from 'echarts/components';// 组件import { // 系列类型的定义后缀都为 seriesoption barseriesoption, lineseriesoption, pieseriesoption, funnelseriesoption, gaugeseriesoption} from 'echarts/charts';type options = lineecoption | barecoption | pieecoption | funneloptiontype baseoptiontype = xaxiscomponentoption | yaxiscomponentoption | titlecomponentoption | tooltipcomponentoption | legendcomponentoption | gridcomponentoptiontype baseoption = echarts.composeoption<baseoptiontype>type lineecoption = echarts.composeoption<lineseriesoption | baseoptiontype>type barecoption = echarts.composeoption<barseriesoption | baseoptiontype>type pieecoption = echarts.composeoption<pieseriesoption | baseoptiontype>type funneloption = echarts.composeoption<funnelseriesoption | baseoptiontype>type gaugeecoption = echarts.composeoption<gaugeseriesoption | gridcomponentoption>type echartsoption = echarts.echartsoption;type charttype = 'bar' | 'line' | 'pie' | 'gauge'// echarts事件namespace chartsevents { // 鼠标事件类型 type mouseeventtype = 'click' | 'dblclick' | 'mousedown' | 'mousemove' | 'mouseup' | 'mouseover' | 'mouseout' | 'globalout' | 'contextmenu' // 鼠标事件类型 type mouseevents = { [key in exclude<mouseeventtype,'globalout'|'contextmenu'> as `chart${capitalize<key>}`] :ecelementevent } // 其他的事件类型极参数 interface events extends mouseevents { globalout:ecelementevent, contextmenu:ecelementevent, selectchanged: selectchangedpayload; highlight: highlightpayload; legendselected: { // 图例选中后的事件 type: 'legendselected', // 选中的图例名称 name: string // 所有图例的选中状态表 selected: { [name: string]: boolean } }; // ... 其他类型的事件在这里定义 } // echarts所有的事件类型 type eventtype = keyof events}export { baseoption, charttype, lineecoption, barecoption, options, pieecoption, funneloption, gaugeecoption, echartsoption, chartsevents}
options/bar.tsimport { barecoption } from ../type;const options: barecoption = { legend: {}, tooltip: {}, xaxis: { type: category, axisline: { linestyle: { // type: dashed, color: #c8d0d7 } }, axistick: { show: false }, axislabel: { color: #7d8292 } }, yaxis: { type: value, alignticks: true, splitline: { show: true, linestyle: { color: #c8d0d7, type: dashed } }, axisline: { linestyle: { color: #7d8292 } } }, grid: { left: 60, bottom: 8%, top: 20% }, series: [ { type: bar, barwidth: 20, itemstyle: { color: { type: linear, x: 0, x2: 0, y: 0, y2: 1, colorstops: [ { offset: 0, color: #62a5ff // 0% 处的颜色 }, { offset: 1, color: #3365ff // 100% 处的颜色 } ] } } // label: { // show: true, // position: top // } } ]};export default options;
项目中使用index.vue<template> <div> <section> <div class="device-statistics chart-box"> <div>累计设备接入统计</div> <v-charts type="bar" :options="statisdevicebyuserobjectopts" @selectchanged="selectchanged" @chart-click="handlechartclick" /> </div> <div class="coordinate-statistics chart-box"> <div>坐标数据接入统计</div> <v-charts type="bar" :options="statiscoordaccess" /> </div> </section> </div></template><script setup>import { usestatisdevicebyuserobject,} from ./hooks;// 设备分类统计const { options: statisdevicebyuserobjectopts,selectchanged,handlechartclick } = usestatisdevicebyuserobject();</script>
/hooks/usestatisdevicebyuserobject.tsexport const usestatisdevicebyuserobject = () => { // 使用chartsoptions确保所有传入v-charts组件的options数据都是## shallowreactive浅层作用形式,避免大量数据导致性能问题 const options = chartsoptions<barecoption>({ yaxis: {}, xaxis: {}, series: [] }); const init = async () => { const xdata = []; const sdata = []; const dicts = usehashmapdics<["dev_user_object"]>([dev_user_object]); const data = await statisdevicebyuserobject(); dicts.dictionarymap.dev_user_object.foreach(({ label, value }) => { if (value === 6) return; // 排除其他 xdata.push(label); const temp = data.find(({ name }) => name === value); sdata.push(temp?.qty || 0); // 给options赋值时要注意options是浅层响应式 options.xaxis = { data: xdata }; options.series = [{ ...options.series[0], data: sdata }]; }); }; // 事件 const selectchanged = (params: chartsevents.events[selectchanged]) => { console.log(params, 选中图例了); }; const handlechartclick = (params: chartsevents.events[chartclick]) => { console.log(params, 点击了图表); }; onmounted(() => { init(); }); return { options, selectchanged, handlechartclick };};
使用时输入@可以看到组件支持的所有事件:
推荐学习:《vue.js视频教程》
以上就是聊聊vue3中echarts用什么形式封装最好?(代码详解)的详细内容。