vue3 echart组件封装

项目中用到了很多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);
  });

热门相关:重开地狱   神医嫁到   与校花同居:高手风流   龙组使命   公子风流