跳至主要內容

Vue echarts封装

fxss原创Vueecharts大约 8 分钟

做大屏的时候经常会遇到 echarts 展示,下面展示在 Vue2.7 / Vue3 中对 echarts (^5.4.0) 的简单封装。

样例查看open in new window

echarts 封装使用

props 说明

参数说明类型可选值默认值
opts初始化传入的 opts https://echarts.apache.org/zh/api.html#echarts.initopen in new windowObject-{renderer: 'svg'}
options配置项,对应 https://echarts.apache.org/zh/option.html#titleopen in new windowObject-{}
seriesDataseries 数据配置内容https://echarts.apache.org/zh/option.html#seriesopen in new window,数据变更自动更新Array-[]
intervalTime自动切换的时间跨度,指自动切换 高亮 + tooltip 展示,例子open in new windowNumber-1500
autoPlay是否自动播放,指的是是否自动添加切换 高亮 + tooltip 展示Boolean-true
isAddOn是否自动添加鼠标上移事件,切换 高亮 + tooltip 展示的时候,鼠标再移动到其他需要高亮显示上时,自动停止切换动画,鼠标移开自动继续播放Boolean-true

方法

方法名说明参数
echartsInstance返回 echarts 实例,如果功能不满足,自己定义-
echartsPlayecharts开启动画,对外开放,可手动调用clear = false, seriesIndex = 0, dataIndex = -1clear: 是否立即开始动画,并清除上个定时器,开启下个定时器,默认为 false;seriesIndex: series 中的第几项数据,默认为 0;dataIndex: series[seriesIndex].data 中的第几项,默认为 -1
echartsPauseecharts关闭动画,对外开放,可手动调用-

使用

如下演示 echarts 封装使用:

echarts封装使用

可以将如下代码拷贝到项目运行,更方便查看效果

<template>
  <div class="overflow-y-auto bg-white text-black box-border" style="max-height: calc(100% - 80px)">
    <div class="flex items-start justify-between m-2">
      <div class="w-1/2 overflow-x-hidden border rounded-2">
        <h1 class="text-md my-2">1、默认展示,测试 seriesData 变更</h1>
        <button @click="changeDate1" class="border rounded-4 py-1 px-4 hover:border-blue-700">切换</button>
        <div class="h-80">
          <echarts-module :options="options1" :seriesData="seriesData1"></echarts-module>
        </div>
      </div>
      <div class="w-1/2 overflow-x-hidden border rounded-2 ml-2">
        <h1 class="text-md my-2">2、默认不展示,展示的时候,设置 seriesData</h1>
        <button @click="changeDate2" class="border rounded-4 py-1 px-4 hover:border-blue-700">切换</button>
        <div class="h-80">
          <echarts-module v-if="isShow" :options="options2" :seriesData="seriesData2"></echarts-module>
        </div>
      </div>
    </div>
    <div class="flex items-start justify-between m-2">
      <div class="w-1/2 overflow-x-hidden border rounded-2">
        <h1 class="text-md my-2">3、前后两次赋值的 seriesData[0].data.length 不一样,动画从头开始</h1>
        <button @click="changeDate3" class="border rounded-4 py-1 px-4 hover:border-blue-700">切换</button>
        <div class="h-80">
          <echarts-module :options="options3" :seriesData="seriesData3"></echarts-module>
        </div>
      </div>
      <div class="w-1/2 overflow-x-hidden border rounded-2 ml-2">
        <h1 class="text-md my-2">4、切换宽高,自动 resize</h1>
        <button @click="changeDate4" class="border rounded-4 py-1 px-4 hover:border-blue-700">切换宽度</button>
        <button @click="changeDate41" class="border rounded-4 py-1 px-4 ml-3 hover:border-blue-700">切换高度</button>
        <div class="h-80">
          <div :style="{ width: boxWidth4 ? '100%' : '50%', height: boxHeight4 ? '100%' : '75%' }">
            <echarts-module :options="options4" :seriesData="seriesData4"></echarts-module>
          </div>
        </div>
      </div>
    </div>
    <div class="flex items-start justify-between m-2">
      <div class="w-1/2 overflow-x-hidden border rounded-2">
        <h1 class="text-md my-2">5、通过 ref 调用开始结束动画,使用 ref 调用的好处是可以指定在第几项开始动画</h1>
        <button @click="changeDate5" class="border rounded-4 py-1 px-4 hover:border-blue-700">开始动画</button>
        <button @click="changeDate51" class="border rounded-4 py-1 px-4 ml-3 hover:border-blue-700">关闭动画</button>
        <div class="h-80">
          <echarts-module ref="echartsModule5" :options="options5" :seriesData="seriesData5" :autoPlay="false"></echarts-module>
        </div>
      </div>
      <div class="w-1/2 overflow-x-hidden border rounded-2 ml-2">
        <h1 class="text-md my-2">6、改变 autoPlay 开始结束动画,使用 autoPlay 改变的好处是可以从上次暂停动画项处继续动画</h1>
        <button @click="changeDate6" class="border rounded-4 py-1 px-4 hover:border-blue-700">{{ autoPlay6 ? '结束' : '开始' }}动画</button>
        <div class="h-80">
          <echarts-module ref="echartsModule6" :options="options6" :seriesData="seriesData6" :autoPlay="autoPlay6"></echarts-module>
        </div>
      </div>
    </div>
    <div class="flex items-start justify-between m-2">
      <div class="w-1/2 overflow-x-hidden border rounded-2">
        <h1 class="text-md my-2">7、切换动画跳动时间 5s -> 1s</h1>
        <button @click="changeDate7" class="border rounded-4 py-1 px-4 hover:border-blue-700">切换</button>
        <div class="h-80">
          <echarts-module :options="options7" :seriesData="seriesData7" :intervalTime="timeout"></echarts-module>
        </div>
      </div>
      <div class="w-1/2 overflow-x-hidden border rounded-2 ml-2">
        <h1 class="text-md my-2">8、通过 isAddOn 添加移除鼠标事件</h1>
        <button @click="changeDate8" class="border rounded-4 py-1 px-4 hover:border-blue-700">{{ isAddOn8 ? '移除' : '添加' }}</button>
        <div class="h-80">
          <echarts-module :options="options8" :seriesData="seriesData8" :isAddOn="isAddOn8"></echarts-module>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
  import { ref, reactive, nextTick } from 'vue'
  import EchartsModule from './../components/EchartsModule.vue'

  // 图表1
  const options1 = reactive({
    legend: {},
    xAxis: {
      type: 'category',
      data: ['Matcha Latte', 'Milk Tea', 'Cheese Cocoa', 'Walnut Brownie', 'Brownie', 'Cheese']
    },
    yAxis: {},
    tooltip: {
      trigger: 'axis'
    }
  })
  const seriesData1 = ref([
    {
      type: 'bar',
      name: '2015',
      data: [89.3, 92.1, 94.4, 85.4, 88, 90]
    },
    {
      type: 'bar',
      name: '2016',
      data: [95.8, 89.4, 91.2, 76.9, 75, 68]
    },
    {
      type: 'bar',
      name: '2017',
      data: [97.7, 83.1, 92.5, 78.1, 95, 93]
    }
  ])
  function changeDate1() {
    seriesData1.value = [
      {
        type: 'bar',
        name: '2015',
        data: [89.3, 92.1, 94.4, 85.4, 50, 69]
      },
      {
        type: 'bar',
        name: '2016',
        data: [95.8, 89.4, 91.2, 76.9, 75, 68]
      }
    ]
  }

  // 图表2
  const isShow = ref(false)
  const options2 = reactive({
    legend: {},
    xAxis: {},
    yAxis: {
      type: 'category',
      data: ['Latte', 'Tea', 'Cocoa', 'Brownie']
    },
    tooltip: {
      trigger: 'axis'
    }
  })
  const seriesData2 = ref([])
  function changeDate2() {
    isShow.value = true
    seriesData2.value = [
      {
        type: 'bar',
        name: '2015',
        data: [89.3, 92.1, 94.4, 85.4]
      },
      {
        type: 'bar',
        name: '2016',
        data: [95.8, 89.4, 91.2, 76.9]
      }
    ]
  }

  // 图表3
  const options3 = reactive({
    legend: {},
    xAxis: {
      type: 'category',
      data: ['Matcha Latte', 'Milk Tea', 'Cheese Cocoa', 'Walnut Brownie', 'Brownie', 'Cheese']
    },
    yAxis: {},
    tooltip: {
      trigger: 'axis'
    }
  })
  const seriesData3 = ref([
    {
      type: 'bar',
      name: '2015',
      data: [89.3, 92.1, 94.4, 85.4, 88, 90]
    },
    {
      type: 'bar',
      name: '2016',
      data: [95.8, 89.4, 91.2, 76.9, 75, 68]
    },
    {
      type: 'bar',
      name: '2017',
      data: [97.7, 83.1, 92.5, 78.1, 95, 93]
    }
  ])
  function changeDate3() {
    seriesData3.value = [
      {
        type: 'bar',
        name: '2015',
        data: [89.3, 92.1]
      }
    ]
  }

  // 图表4
  const boxWidth4 = ref(true)
  const boxHeight4 = ref(true)
  const options4 = reactive({
    legend: {},
    xAxis: {
      type: 'category',
      data: ['Matcha Latte', 'Milk Tea', 'Cheese Cocoa', 'Walnut Brownie', 'Brownie', 'Cheese']
    },
    yAxis: {},
    tooltip: {
      trigger: 'axis'
    }
  })
  const seriesData4 = ref([
    {
      type: 'line',
      name: '2015',
      data: [89.3, 92.1, 94.4, 85.4, 88, 90]
    },
    {
      type: 'line',
      name: '2016',
      data: [95.8, 89.4, 91.2, 76.9, 75, 68]
    },
    {
      type: 'line',
      name: '2017',
      data: [97.7, 83.1, 92.5, 78.1, 95, 93]
    }
  ])
  function changeDate4() {
    boxWidth4.value = !boxWidth4.value
  }
  function changeDate41() {
    boxHeight4.value = !boxHeight4.value
  }

  // 图表5
  const options5 = reactive({
    title: {
      text: '饼图程序调用高亮示例',
      left: 'center'
    },
    tooltip: {
      trigger: 'item',
      formatter: '{a} <br/>{b} : {c} ({d}%)',
      confine: true
    },
    legend: {
      orient: 'vertical',
      left: 'left',
      data: ['直接访问', '邮件营销', '联盟广告', '视频广告', '搜索引擎']
    }
  })
  const seriesData5 = ref([
    {
      name: '访问来源',
      type: 'pie',
      radius: '55%',
      center: ['50%', '60%'],
      data: [
        { value: 335, name: '直接访问' },
        { value: 310, name: '邮件营销' },
        { value: 234, name: '联盟广告' },
        { value: 135, name: '视频广告' },
        { value: 1548, name: '搜索引擎' }
      ],
      emphasis: {
        itemStyle: {
          shadowBlur: 10,
          shadowOffsetX: 0,
          shadowColor: 'rgba(0, 0, 0, 0.5)'
        }
      }
    }
  ])
  const echartsModule5 = ref()
  function changeDate5() {
    echartsModule5.value.echartsPlay(true, 0, -1)
  }
  function changeDate51() {
    echartsModule5.value.echartsPause()
  }

  // 图表6
  const autoPlay6 = ref(false)
  const options6 = reactive({
    title: {
      text: '饼图程序调用高亮示例',
      left: 'center'
    },
    tooltip: {
      trigger: 'item',
      formatter: '{a} <br/>{b} : {c} ({d}%)',
      confine: true
    },
    legend: {
      orient: 'vertical',
      left: 'left',
      data: ['直接访问', '邮件营销', '联盟广告', '视频广告', '搜索引擎']
    }
  })
  const seriesData6 = ref([
    {
      name: '访问来源',
      type: 'pie',
      radius: '55%',
      center: ['50%', '60%'],
      data: [
        { value: 335, name: '直接访问' },
        { value: 310, name: '邮件营销' },
        { value: 234, name: '联盟广告' },
        { value: 135, name: '视频广告' },
        { value: 1548, name: '搜索引擎' }
      ],
      emphasis: {
        itemStyle: {
          shadowBlur: 10,
          shadowOffsetX: 0,
          shadowColor: 'rgba(0, 0, 0, 0.5)'
        }
      }
    }
  ])
  const echartsModule6 = ref()
  function changeDate6() {
    autoPlay6.value = !autoPlay6.value
  }

  // 图表7
  const timeout = ref(5000)
  const options7 = reactive({
    legend: {},
    xAxis: {
      type: 'category',
      data: ['Matcha Latte', 'Milk Tea', 'Cheese Cocoa', 'Walnut Brownie', 'Brownie', 'Cheese']
    },
    yAxis: {},
    tooltip: {
      trigger: 'axis'
    }
  })
  const seriesData7 = ref([
    {
      type: 'line',
      name: '2015',
      data: [89.3, 92.1, 94.4, 85.4, 88, 90]
    },
    {
      type: 'line',
      name: '2016',
      data: [95.8, 89.4, 91.2, 76.9, 75, 68]
    },
    {
      type: 'line',
      name: '2017',
      data: [97.7, 83.1, 92.5, 78.1, 95, 93]
    }
  ])
  function changeDate7() {
    timeout.value = 1000
  }

  // 图表8
  const isAddOn8 = ref(false)
  const options8 = reactive({
    legend: {},
    xAxis: {
      type: 'category',
      data: ['Matcha Latte', 'Milk Tea', 'Cheese Cocoa', 'Walnut Brownie', 'Brownie', 'Cheese']
    },
    yAxis: {},
    tooltip: {
      trigger: 'axis'
    }
  })
  const seriesData8 = ref([
    {
      type: 'line',
      name: '2015',
      data: [89.3, 92.1, 94.4, 85.4, 88, 90]
    },
    {
      type: 'line',
      name: '2016',
      data: [95.8, 89.4, 91.2, 76.9, 75, 68]
    },
    {
      type: 'line',
      name: '2017',
      data: [97.7, 83.1, 92.5, 78.1, 95, 93]
    }
  ])
  function changeDate8() {
    isAddOn8.value = !isAddOn8.value
  }
</script>

代码封装

<template>
  <div ref="echartsRef" style="width: 100%; height: 100%"></div>
</template>

<script setup>
  import { defineProps, defineExpose, watch, nextTick, ref, onMounted, onBeforeUnmount } from 'vue'
  import * as echarts from 'echarts'
  import { useElementSize } from '@vueuse/core'

  const props = defineProps({
    // https://echarts.apache.org/zh/api.html#echarts.init
    // 初始化传入的 opts
    opts: {
      type: Object,
      default() {
        return {
          renderer: 'svg'
        }
      }
    },
    // 配置项
    options: {
      type: Object,
      default() {
        return {}
      }
    },
    // 数据集合
    seriesData: {
      type: Array,
      default() {
        return []
      }
    },
    // 自动切换的时间跨度
    intervalTime: {
      type: Number,
      default: 1500
    },
    // 是否自动播放
    autoPlay: {
      type: Boolean,
      default: true
    },
    // 是否自动添加鼠标上移事件
    isAddOn: {
      type: Boolean,
      default: true
    }
  })

  const echartsRef = ref()
  let myChart = null
  let echartsOptions = {}
  let myChartEventTime = null
  let currentSeriesIndex = 0
  let currentDataIndex = -1

  // 是否调用过 echartsPlay
  let isEchartsPlay = false
  // echarts初始化
  function init() {
    destroyEchart() //判断是否有echart实例,如果有,先销毁
    myChart = echarts.init(echartsRef.value, null, props.opts)
    update()

    if (props.isAddOn) {
      addEventFn()
    }
  }

  // 绑定事件
  function addEventFn() {
    // 鼠标移上查看的时候,暂停动画
    myChart.on('mouseover', 'series', event => {
      // 取消之前高亮的图形
      myChart.dispatchAction({
        type: 'downplay',
        seriesIndex: currentSeriesIndex,
        dataIndex: currentDataIndex
      })
      echartsPause()
    })
    // 鼠标移出的时候打开动画
    myChart.on('mouseout', 'series', event => {
      // 自动播放 或者 调用过 echartsPlay
      if (props.autoPlay || isEchartsPlay) echartsPlay(true, event.seriesIndex, event.dataIndex - 1)
    })
  }

  // 移除事件
  function removeEventFn() {
    myChart.off('mouseover')
    myChart.off('mouseout')
  }

  // 数据更新
  function update(isRefresh = false) {
    // 逻辑处理组件options参数
    const options = {
      ...props.options,
      series: props.seriesData
      // other options here ...
    }
    echartsOptions = options
    // 调用ECharts组件setOption更新
    nextTick(() => {
      myChart.setOption(options, true)
      if (options.series.length && props.autoPlay) {
        if (isRefresh) {
          echartsPlay(false, currentSeriesIndex, -1)
        } else {
          echartsPlay(false, currentSeriesIndex, currentDataIndex)
        }
      }
    })
  }

  // 销毁echarts
  function destroyEchart() {
    if (myChart) {
      if (props.isAddOn) {
        removeEventFn()
      }
      if (typeof myChart.clear === 'function') myChart.clear()
      if (typeof myChart.dispose === 'function') myChart.dispose()
      myChart = null
    }
  }

  /**
   * echarts开启动画,对外开放,可手动调用
   * clear: 是否立即开始动画,并清除上个定时器,开启下个定时器,默认为 false
   * seriesIndex: series 中的第几项数据,默认为 0
   * dataIndex: series[seriesIndex].data 中的第几项,默认为 -1
   */
  function echartsPlay(clear = false, seriesIndex = 0, dataIndex = -1) {
    if (clear) {
      echartsPause()
    }
    isEchartsPlay = true
    currentSeriesIndex = seriesIndex
    currentDataIndex = dataIndex
    if (!myChartEventTime) {
      echartsEventPlay(seriesIndex)
    }
  }

  function echartsTimeout(seriesIndex = 0) {
    myChartEventTime = setTimeout(() => {
      echartsEventPlay(seriesIndex)
    }, props.intervalTime)
  }

  function echartsEventPlay(seriesIndex = 0) {
    const dataLen = echartsOptions.series[seriesIndex].data.length
    if (myChart && myChart.dispatchAction) {
      // 取消之前高亮的图形
      myChart.dispatchAction({
        type: 'downplay',
        seriesIndex,
        dataIndex: currentDataIndex
      })
      currentDataIndex = (currentDataIndex + 1) % dataLen
      // 高亮当前图形
      myChart.dispatchAction({
        type: 'highlight',
        seriesIndex,
        dataIndex: currentDataIndex
      })
      // 显示 tooltip
      myChart.dispatchAction({
        type: 'showTip',
        seriesIndex,
        dataIndex: currentDataIndex
      })
    }
    echartsTimeout(seriesIndex)
  }

  // echarts关闭动画,对外开放,可手动调用
  function echartsPause() {
    if (myChart && myChart.dispatchAction) {
      // 取消之前高亮的图形
      myChart.dispatchAction({
        type: 'downplay',
        seriesIndex: currentSeriesIndex,
        dataIndex: currentDataIndex
      })
      // 取消显示 tooltip
      myChart.dispatchAction({
        type: 'hideTip'
      })
    }
    if (myChartEventTime) {
      clearTimeout(myChartEventTime)
      myChartEventTime = null
    }
  }

  // 重置大小
  function echartsResize() {
    if (myChart) myChart.resize()
  }

  onMounted(() => {
    init()
  })
  onBeforeUnmount(() => {
    echartsPause()
    destroyEchart()
  })

  watch(
    () => props.seriesData,
    (val, oldVal) => {
      let isRefresh = false
      // 前后两次赋值的 seriesData[0].data.length 不一样,动画从头开始
      if (val.length > 0 && val[currentSeriesIndex].data && oldVal.length > 0 && oldVal[currentSeriesIndex].data && val[currentSeriesIndex].data.length !== oldVal[currentSeriesIndex].data.length) {
        isRefresh = true
      }
      update(isRefresh)
    },
    { deep: true }
  )

  // 监听 props.autoPlay ,自动关闭、继续动画
  watch(
    () => props.autoPlay,
    val => {
      if (val) {
        echartsPlay(false, currentSeriesIndex, currentDataIndex)
      } else {
        echartsPause()
      }
    }
  )

  // 监听 props.isAddOn ,自动添加、移除事件
  watch(
    () => props.isAddOn,
    val => {
      if (val) {
        addEventFn()
      } else {
        removeEventFn()
      }
    }
  )

  // 父元素大小改变的时候,自动 resieze
  const { width, height } = useElementSize(echartsRef)
  watch(
    () => width.value,
    val => {
      nextTick(() => {
        echartsResize()
      })
    }
  )
  watch(
    () => height.value,
    val => {
      nextTick(() => {
        echartsResize()
      })
    }
  )

  defineExpose({
    echartsInstance: () => myChart,
    echartsPlay,
    echartsPause
  })
</script>