关于数据大屏中复用图表组件公共逻辑方式的案例

124 阅读3分钟

背景介绍

在做数据大屏时,每一个数据区域都需要写很多相同的逻辑,例如监听用户选择的时间范围、监听页面大小resize等,所以我们需要提取这些公共的部分作为公共组件,即对Echarts进行二次封装。在前期初步形成了三个方案如下:

  1. 封装一个公共组件,使用slot给父组件提供一个插槽入口,但是使用这种方法时,因为Echarts图表写在插槽里,相关的处理逻辑仍然在父组件代码里,代码复用率低且组件间通信复杂,故不考虑。❌
  2. 将Echarts进行封装,父组件通过props向子组件传递相关数据,而例如时间范围变化、页面大小改变事件则由子组件监听到后通过emit传递给父组件。
  3. 使用Vue3的组合式函数,将公共逻辑提取出来进行复用。

cn.vuejs.org/guide/reusa…

封装步骤

经过个人的尝试与研究,二次封装Echarts需要结合使用上面的2和3,以下是具体的封装步骤:

0. 文件目录结构(无关部分已省略)

-src # 根目录
---components
-----MyChart
-------MyChart.vue # 二次封装组件
---composables
-----charts
-------useCharts.ts # 组合式函数声明
---views
-----components
-------UseCase.vue # 数据区域组件
-----index.vue # 主页面文件
  1. useEchart.ts

该组合式函数主要功能为创建echarts实例并开启监听屏幕大小变化,触发echarts实例的resize。

import * as Echarts from "echarts";

// 封装Echarts的顶级hooks
export default function (el: HTMLElement) {
  // 创建Echarts实例
  const echartInstance = Echarts.init(el);

  /**
* 创建option
* @param option echarts配置项
*/
  const setOption = (option: Echarts.EChartsOption) => {
    echartInstance.setOption(option);
  };

  // 调用Echarts实例来适配echarts大小,这里默认函数为仅调用resize,如果需要其他操作可以使用updataSize
  window.addEventListener("resize", () => {
    echartInstance.resize();
  });

  /**
* 导出更新配置项的函数,以供需要更新的场景使用
* @param optione charts配置项
*/
  const updataSize = (option: Echarts.EChartsOption) => {
    setOption(option);
    echartInstance.resize();
  };

  return { setOption, updataSize };
}
  1. MyChart.vue

该组件主要是调用了useEchart进行实例创建,并且监听时间变化后传递给父组件。

另外,该组件通过判断是否传入option来选择是创建echarts实例还是向外提供slot。

<template>
  <div class="base-echart" v-if="option">
    <div ref="echartDivRef" :style="{ width: width, height: height }"></div>
  </div>
  <div class="base-echart" v-else>
    <slot :style="{ width: width, height: height }"></slot>
  </div>
</template>

<script lang="ts" setup>
import { EChartsOption } from "echarts";
import useCharts from "@/hooks/charts/useCharts.ts";
import useBoardStore from "@/store/modules/dataBoard.ts";

const boardStore = useBoardStore();
const timeRange = computed(() => boardStore.timeRange);
const emits = defineEmits(["timeChange"]);
// 监听时间变化
watch(timeRange, () => {
  emits("timeChange", timeRange);
});

// 定义prpos
const props = withDefaults(
  defineProps<{
    option?: EChartsOption;
    width?: string;
    height?: string;
  }>(),
  {
    width: "100%",
    height: "100%",
  }
);

const echartDivRef = ref<HTMLElement>();

onMounted(() => {
  if (props.option) {
    const { setOption } = useCharts(echartDivRef.value!);
    watchEffect(() => {
      props.option && setOption(props.option);
    });
  }
});
</script>

<style lang="scss" scoped>
.base-echart {
  display: flex;
  justify-content: center;
  width: 100%;
  height: 100%;
}
</style>
  1. UseCase.vue

使用MyChart.vue组件,传递option,并在子组件触发timeChange时更新option。

以下代码option中的内容只展示部分,根据实际需要进行配置。

<template>
  <my-chart :option="option" @time-change="getData"></my-chart>
</template>
<script lang="ts" setup>
import { getUseCase } from "@/api/dataBoard";
import MyChart from "@/components/MyChart/MyChart.vue";

// 获取根字体大小
let fontsize = ref(parseInt(document.documentElement.style.fontSize.replace("px", "")));

const option = ref({});

const initEcharts = (data: { data: [] }) => {
  option.value = {
    grid: {
      // 设置边距
      x: fontsize.value * 4,
      y: fontsize.value * 3.5,
      x2: 20,
      y2: 50,
    },
    xAxis: {
      type: "time", // x轴为时间轴
    },
    yAxis: {
      min: 0, //取0为最小刻度
      max: 100, //取100为最大刻度
      type: "value", // y轴为数值轴
    },
    series: [
      {
        name: "平台使用率",
        type: "line",
        data: data.data,
        color: "#F8B62B",
      }
    ],
  };
};

/**
* 获取列表数据
*/
const getData = async () => {
  try {
    let res = await getUseCase();
    initEcharts(res);
  } catch (err: any) {
    ElMessage.error(err.toString().replace("Error: ", ""));
  }
};

onMounted(() => {
  getData();
  // 监听屏幕尺寸变化
  window.addEventListener("resize", function () {
    fontsize.value = parseInt(document.documentElement.style.fontSize.replace("px", ""));
    Object.assign(option.value, {
      grid: {
        x: fontsize.value * 4,
        y: fontsize.value * 3.5,
      },
    });
  });
});
</script>
  1. index.vue

页面使用数据区域组件

<div class="container-item">
  <use-case></use-case>
</div>