vue3 封装通用 ECharts 组件

我在项目中需要用到很多的图标,比如折线、饼图、柱状、关系等各种图标,我又比较懒,所以就封了一个基本的组件库,只需要传递options和canvasId,基本就可以了,代码如下:

javascript 复制代码
<template>
  <div class="wrapper-charts">
    <div class="text-title" v-if="isShowTitle">
      <div class="title-h3">{{ title || '' }}</div>
      <div v-if="isShowSelect">
        <!-- <template #select> xxx </template> -->
        <span class="title-tip" v-if="showInstitutionName">{{ institutionName || '' }}</span>
        <slot name="select">
          <a-select size="small" style="width: 113px" :value="modelValue" class="selectType" @select="onChange"
            :fieldNames="labelProps">
            <a-select-option value="全部">全部</a-select-option>
            <a-select-option :value="item" v-for="(item, index) in selectOptions" :key="index">
              {{ item }}
            </a-select-option>
          </a-select>
        </slot>
      </div>
    </div>
    <div class="wrapper-charts-content" :style="bodyStyle">
      <div class="charts-styles" :class="className" v-if="chartOptOneSort">
        <div v-size-direct="resizeChart" :ref="chartId" :id="chartId" :style="styles"></div>
      </div>
      <div class="no-data-available-chart" v-else>
        <svg-icon name="noDataAvailable" width="121" height="130"></svg-icon>
        <div>暂无数据</div>
      </div>
    </div>
  </div>
</template>

<script setup>
  import { ref, onMounted, onBeforeUnmount, watch, nextTick, defineSlots, markRaw } from 'vue'
  import * as echarts from 'echarts'

  // Props
  const props = defineProps({
    modelValue: {
      type: [String, Number, null, Array],
      default: '全部'
    },
    isShowTitle: {
      type: Boolean,
      default: false
    },
    isShowSelect: {
      type: Boolean,
      default: true
    },
    loading: {
      type: Boolean,
      default: false
    },
    className: {
      type: String,
      default: ""
    },
    selectOptions: {
      type: Array,
      default: []
    },
    labelProps: {
      type: Object,
      default: {
        label: 'name',
        value: 'value',
        options: 'options'
      }
    },
    showInstitutionName: {
      type: Boolean,
      default: true
    },
    institutionName: {
      type: String,
      default: ''
    },
    title: {
      type: String,
      default: '高原病就珍'
    },
    // id 需要不一样,不然会覆盖
    chartId: {
      type: String,
      required: true,
      default: 'chartContainer'
    },

    option: {
      type: Object,
      required: true // option 是必须的
    },

    bodyStyle: {
      type: Object,
      default: {
        width: '100%',
        height: '100%'
      }
    },

    styles: {
      type: Object,
      default: {
        width: '100%',
        height: '100%'
      }
    },
    clickChart: {
      type: Function,
      required: false
    }
  })

  let chartInstance = ref(null)
  const emit = defineEmits(['update:modelValue', 'change', 'clickChart', 'finished']) // 用来触发事件
  const $solt = defineSlots()
  const onChange = (value) => {
    emit('update:modelValue', value)
    emit('change', value)
  }

  const chartOptOneSort = ref(false)
  // 初始化图表
  const initChart = () => {
    if (chartInstance.value) {
      chartInstance.value?.dispose() // 销毁已有实例,避免重复渲染
    }
    const dom = document.getElementById(props.chartId)
    chartInstance.value = markRaw(echarts.init(dom))
    if (props.loading) {
      chartInstance.value?.showLoading()
    }
    chartInstance.value?.off('click') // 移除旧的点击事件
    chartInstance.value?.setOption(props.option)
    // 监听图表渲染完成事件
    chartInstance.value?.on('finished', () => {
      chartInstance.value?.hideLoading()
      emit('finished')
    })
    // 监听图表点击事件
    chartInstance.value?.on('click', (params) => {
      // 通过 emit 触发 'click' 事件,传递参数
      emit('clickChart', params)
    })
  }

  // 监听 option 的变化并更新图表
  watch(
    () => props.option,
    (newOption) => {
      chartOptOneSort.value = newOption?.series?.length > 0;
      let isShowView = newOption?.series?.map(item => item.data.some(value => value > 0)) || [];
      chartOptOneSort.value = isShowView.some(item => item);
      
      if (chartOptOneSort.value) {
        nextTick(() => {
          if (chartInstance.value) {
            // chartInstance.value.clear();
            chartInstance.value?.dispose() // 销毁已有实例,避免重复渲染
          }
          initChart()
        })
      }
    },
    { deep: true, immediate: true } // 深度监听
  )

  // 处理窗口大小变化时,重新调整图表尺寸
  const resizeChart = (contentRect) => {
    if (chartInstance.value) {
      chartInstance.value?.resize()
    }
  }

  // 在组件挂载时初始化图表,并添加 resize 事件监听
  onMounted(() => {
    // initChart();
    // window.addEventListener('resize', handleResize)
  })

  // 在组件卸载时销毁图表,并移除 resize 事件监听
  onBeforeUnmount(() => {
    if (chartInstance.value) {
      chartInstance.value?.dispose()
    }
    // window.removeEventListener('resize', handleResize)
  })

  defineExpose({
    chartInstance,
    resizeChart,
    initChart,
    onChange,
    chartOptOneSort
  })
</script>

<style lang="less" scoped>
  .wrapper-charts {
    width: 100%;
    height: 100%;
  }

  /* 可以根据需要设置图表的样式 */

  .text-title {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 10px;

    .title-h3 {
      font-size: 16px;
      font-style: normal;
      font-weight: 600;
      color: #333;
    }

    .title-tip {
      font-size: 14px;
      font-weight: 400;
      color: #5e6580;
      margin-right: 10px;
    }
  }

  .wrapper-charts-content {
    width: 100%;
    height: 100%;

    .charts-styles {
      width: 100%;
      height: calc(100% - 35px);
    }
  }

  .no-data-available-chart {
    width: 100%;
    height: 100%;
    display: flex;
    align-items: center;
    flex-direction: column;
    justify-content: center;
    color: #5e6580;
    font-family: 'PingFang SC';
    font-size: 14px;
    font-style: normal;
    font-weight: 400;
  }
</style>

在使用的页面中引入或者在全局配置都是可以的,我是才页面中引入的,

javascript 复制代码
import StatisticsCharts from '@/components/ECharts/lineTrend.vue'

配置文件 config.ts

javascript 复制代码
import { ref } from 'vue'
import * as echarts from 'echarts'
// charts-config

// 折线图柱状数据配置
// https://echarts.apache.org/zh/option.html#tooltip.confine
export const colors = ['#405BC8', '#00CFBE', '#A96BFF']
export function convertToPercentage(decimal: number) {
  return (decimal * 100).toFixed(2) + ' %'
}
export const chartOptions = {
  color: colors,
  tooltip: {
    trigger: 'axis',
    triggerOn: 'mousemove',
    confine: true,
    height: 'auto',
    backgroundColor: 'rgba(40, 49, 67, 0.85);',
    borderColor: 'rgba(40, 49, 67, 0.85);',
    enterable: true,
    appendToBody: true,
    axisPointer: {
      type: 'cross',
      label: {
        backgroundColor: '#6a7985'
      }
      // crossStyle: {
      //   color: '#fff'
      // }
    },
    // 自定义提示框内容
    formatter: function (data) {
      let tooltipHtml = `
      <div style="color:#ffffff;font-size: 14px;font-weight: 400;margin-bottom: 8px;max-height: 200px; overflow-y: auto;">
        <div style="color:#ffffff;font-size: 14px;font-weight: 400;margin-bottom: 8px;">
          ${data[0].axisValue}
          </br>
        </div>
      `
      data.forEach((item) => {
        tooltipHtml += `
        <div style="font-size: 12px;padding: 4px 0;width: auto;max-height: 200px; overflow-y: auto;">
          <div style="display:inline-block;margin-right:4px;width:10px;height:10px;background-color: ${item.color};"></div>
          <span>${item.seriesName} :</span>
          
          <span style="color:#ffffff">${item.seriesType == 'line' ? convertToPercentage(Number(item.data)) : item.data + '元'}</span>
        </div>
       `
      })

      tooltipHtml += '</div>'
      return tooltipHtml
    },
    textStyle: {
      fontSize: 14
    }
  },
  toolbox: {
    top: '2%',
    right: '3%',
    show: false,
    feature: {
      // dataView: { show: true, readOnly: false },
      magicType: { show: true, type: ['line', 'bar'] },
      restore: { show: true }
      // saveAsImage: { show: true }
    }
  },
  legend: {
    width: '80%',
    itemHeight: 10,
    itemWidth: 10,
    top: '2%',
    left: '3%',
    itemGap: 15,
    type: 'scroll', // 数据过多时,分页显示
    icon: 'rect', // 设置图例的形状
    selected: [] //这里默认显示数组中前十个,如果不设置,则所有的数据都会显示在图表上
  },
  xAxis: [
    {
      type: 'category',
      boundaryGap: true, //坐标轴两边留白
      axisTick: {
        show: false
      },
      data: [],
      axisPointer: {
        type: 'shadow'
      },
      axisLabel: {
        show: true, //下方日期显示与否
        interval: 0, // 设置数据间隔
        rotate: 28, // 标题倾斜
        margin: 5, //刻度标签与轴线之间的距离
        textStyle: {
          fontSize: 12, //横轴字体大小
          color: '#8F94A7' //颜色
        }
      }
    }
  ],
  yAxis: [
    {
      name: '',
      // interval: 5,
      position: 'left',
      alignTicks: true,
      type: 'value',
      splitLine: {
        show: false
      },
      axisLine: {
        show: false,
        //  min: 0,
        //  max: 10,
        lineStyle: { color: '#8F94A7' }
      },
      axisLabel: {
        show: true,
        fontSize: 14,
        color: '#8F94A7',
        formatter: function (value: string) {
          return `${Number(value)} 元`
        }
      }
    },
    {
      // name: '温度',
      // interval: 5,
      type: 'value',
      position: 'right',
      alignTicks: true,
      axisLine: {
        show: false
        // lineStyle: {
        //   color: colors[2]
        // }
      },
      axisLabel: {
        show: true,
        fontSize: 14,
        formatter: function (value: string) {
          return `${(Number(value) * 100).toFixed(0)} %`
        },
        color: '#8F94A7'
      }
    }
  ],
  // X轴可滚动
  dataZoom: [
    {
      type: 'slider', // 设置为滑动条型式
      show: true, // 显示dataZoom组件
      bottom: 0,
      height: 10,
      showDetail: false,
      startValue: 0, //滚动条的起始位置
      endValue: 9 //滚动条的截止位置(按比例分割你的柱状图x轴长度)
      // xAxisIndex: [0] // 表示控制第一个x轴
      // start: 0, // 默认显示的起始位置为0
      // end: 20, // 默认显示的结束位置为100
      // handleSize: 8, // 滑动条的手柄大小
      // handleStyle: {
      //   color: '#DCE2E8' // 滑动条的手柄颜色
      // },
      // filterMode: 'filter' // 设置为filter模式,即数据超过范围时会被过滤掉
    },
    {
      type: 'inside', //设置鼠标滚轮缩放
      show: true,
      xAxisIndex: [0],
      startValue: 0,
      endValue: 9
    }
  ],
  grid: {
    left: '3%',
    right: '4%',
    bottom: '3%',
    containLabel: true
  },
  series: [
    {
      name: '药品费用总额',
      type: 'bar',
      barWidth: '20',
      tooltip: {
        valueFormatter: function (value: string) {
          return `${Number(value)} 元`
        }
      },
      itemStyle: {
        normal: {
          barBorderRadius: [2, 2, 0, 0]
        }
      },
      data: []
    },
    {
      name: '医疗总费用',
      type: 'bar',
      barWidth: '20',
      itemStyle: {
        //柱形图圆角,鼠标移上去效果,如果只是一个数字则说明四个参数全部设置为那么多
        normal: {
          //柱形图圆角,初始化效果
          barBorderRadius: [2, 2, 0, 0]
        }
      },
      tooltip: {
        valueFormatter: function (value: string) {
          return `${Number(value)} 元`
        }
      },
      data: []
    },
    {
      name: '药占比',
      type: 'line',
      yAxisIndex: 1,
      smooth: true, //true 为平滑曲线,false为直线
      symbol: 'circle', //将小圆点改成实心 不写symbol默认空心 none 不显示
      symbolSize: 8, //小圆点的大小
      label: {
        show: true,
        position: 'top',
        formatter: function (params: any) {
          return convertToPercentage(Number(params.value))
        }
      },
      tooltip: {
        valueFormatter: function (value: string) {
          return convertToPercentage(Number(value))
        }
      },
      data: []
    }
  ]
}
// 折线配置
export const chartLineOptions = {
  color: ['#209E85'],
  // title: {
  //   text: 'Stacked Line'
  // },
  tooltip: {
    trigger: 'axis',
    triggerOn: 'mousemove',
    confine: true,
    height: 'auto',
    backgroundColor: 'rgba(40, 49, 67, 0.85);',
    borderColor: 'rgba(40, 49, 67, 0.85);',
    enterable: true,
    appendToBody: true,
    axisPointer: {
      type: 'cross',
      label: {
        backgroundColor: '#6a7985'
      }
      // crossStyle: {
      //   color: '#fff'
      // }
    },
    // 自定义提示框内容
    formatter: function (data) {
      let tooltipHtml = `
      <div style="color:#ffffff;font-size: 14px;font-weight: 400;margin-bottom: 8px;max-height: 200px; overflow-y: auto;">
        <div style="color:#ffffff;font-size: 14px;font-weight: 400;margin-bottom: 8px;">
          ${data[0].axisValue}
          </br>
        </div>
      `
      data.forEach((item) => {
        tooltipHtml += `
        <div style="font-size: 12px;padding: 4px 0;width: auto;max-height: 200px; overflow-y: auto;">
          <div style="display:inline-block;margin-right:4px;width:10px;height:10px;background-color: ${item.color};"></div>
          <span>${item.seriesName} :</span>
          
          <span style="color:#ffffff">${item.seriesType == 'line' ? convertToPercentage(Number(item.data)) : item.data + '元'}</span>
        </div>
       `
      })

      tooltipHtml += '</div>'
      return tooltipHtml
    },
    textStyle: {
      fontSize: 14
    }
  },
  legend: {
    data: []
  },
  grid: {
    left: '3%',
    right: '4%',
    bottom: '3%',
    containLabel: true
  },
  xAxis: {
    type: 'category',
    boundaryGap: true,
    data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
  },
  yAxis: {
    name: '',
    // interval: 5,
    position: 'left',
    alignTicks: true,
    type: 'value',
    splitLine: {
      show: true // X 轴线
    },
    axisLine: {
      // Y 轴线
      show: false,
      //  min: 0,
      //  max: 10,
      lineStyle: { color: '#939495' }
    },
    axisLabel: {
      show: true,
      fontSize: 14,
      color: '#939495',
      formatter: '{value} 元'
    }
  },
  series: [
    {
      type: 'line',
      // symbol: 'none', //去掉折线图中的节点
      smooth: true, //true 为平滑曲线,false为直线
      symbol: 'circle', //将小圆点改成实心 不写symbol默认空心
      symbolSize: 8, //小圆点的大小
      itemStyle: {
        color: '#209E85' //小圆点和线的颜色
      },
      // areaStyle: {
      //   color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
      //     {
      //       offset: 0,
      //       color: 'rgba(213,72,120,0.8)' //靠上方的透明颜色
      //     },
      //     {
      //       offset: 1,
      //       color: 'rgba(213,72,120,0.3)' //靠下方的透明颜色
      //     }
      //   ])
      // },
      label: {
        show: true,
        position: 'top',
        formatter: function (params: any) {
          return convertToPercentage(Number(params.value))
        }
      },
      tooltip: {
        valueFormatter: function (value: string) {
          return convertToPercentage(Number(value))
        }
      },
      stack: 'Total',
      data: [120, 132, 101, 134, 90, 230, 210]
    }
  ]
}
// 柱状图配置
export const barChartOptions = {
  title: {
    textStyle: {
      //文字颜色
      color: '#07123C',
      fontWeight: 'bold',
      //字体系列
      fontFamily: 'sans-serif',
      //字体大小
      fontSize: 18
    }
  },
  toolbox: {
    top: '2%',
    right: '3%',
    show: false,
    feature: {
      // dataView: { show: true, readOnly: false },
      magicType: { show: true, type: ['line', 'bar'] },
      restore: { show: true }
      // saveAsImage: { show: true }
    }
  },
  tooltip: {
    trigger: 'axis',
    backgroundColor: 'rgba(40, 49, 67, 0.85);',
    borderColor: 'rgba(40, 49, 67, 0.85);',
    enterable: true,
    appendToBody: true,
    textStyle: {
      color: '#fff'
    }
  },
  legend: {
    // top: '2%',
    left: '3%',
    itemHeight: 10,
    itemWidth: 10,
    itemGap: 15,
    type: 'scroll', // 数据过多时,分页显示
    selected: [], //这里默认显示数组中前十个,如果不设置,则所有的数据都会显示在图表上
    icon: 'rect', // 设置图例的形状
    data: []
  },
  color: ['#13A89B'],
  barWidth: 20,
  calculable: true,
  grid: {
    left: '3%',
    right: '4%',
    bottom: '3%',
    containLabel: true
  },
  xAxis: {
    type: 'category',
    data: ['急诊科', '门诊科', '儿科', '妇科', '神经外科', '神经内科', '皮肤科', '放射科'],
    axisTick: {
      show: false
    },

    axisLabel: {
      // 轴文字
      show: true,
      color: '#A6AAB2',
      fontSize: 12,
      interval: 0,
      rotate: 20
    },
    axisLine: {
      lineStyle: {
        type: 'solid',
        color: '#E6E6E8',
        width: '1'
      }
    }
  },
  yAxis: {
    type: 'value',
    axisLabel: {
      // 轴文字
      show: true,
      color: '#A6AAB2',
      fontSize: 12
    }
    // max:99999//给y轴设置最大值
  },
  series: [
    {
      type: 'bar',
      data: [120, 200, 150, 80, 70, 110, 130, 50],
      // barGap:'80%',/*多个并排柱子设置柱子之间的间距*/
      // barCategoryGap:'50%',/*多个并排柱子设置柱子之间的间距*/
      itemStyle: {
        //柱状图上方显示数值
        normal: {
          //柱形图圆角,初始化效果
          barBorderRadius: [2, 2, 0, 0],
          color: '#13A89B',
          label: {
            show: true, //开启显示
            position: 'top', //在上方显示
            textStyle: {
              //数值样式
              color: '#5E6580',
              fontSize: 12
            }
          }
        }
      },
      showBackground: true,
      backgroundStyle: {
        color: '#f2f8ff'
      },
      label: {
        show: true,
        position: 'top',
        formatter: '{c}'
      }
    }
  ],
  dataZoom: [
    {
      type: 'slider', //给x轴设置滚动条
      show: true, //flase直接隐藏图形
      xAxisIndex: [0],
      bottom: 0,
      height: 10,
      showDetail: false,
      startValue: 0, //滚动条的起始位置
      endValue: 9 //滚动条的截止位置(按比例分割你的柱状图x轴长度)
    },
    {
      type: 'inside', //设置鼠标滚轮缩放
      show: true,
      xAxisIndex: [0],
      startValue: 0,
      endValue: 9
    }
  ]
}
相关推荐
机构师6 分钟前
<tauri><rust><GUI>基于tauri,实现websocket通讯程序(右键菜单、websocket)
开发语言·javascript·websocket·rust·gui·tauri
小钟H呀24 分钟前
Vue3 Hooks:从原理到实战封装指南
前端·javascript·vue.js
三月七(爱看动漫的程序员)32 分钟前
Prompt Engineering for Large Language Models
前端·javascript·人工智能·语言模型·自然语言处理·pdf·prompt
我爱学习_zwj43 分钟前
1. HTTP 数据请求
前端
pink大呲花1 小时前
ES6笔记总结
前端·笔记·es6
m0_748255261 小时前
Vue项目中 安装及使用Sass(scss)
vue.js·sass·scss
我的div丢了肿么办1 小时前
试试使用 Vitest 进行测试,确实可以减少bug
前端·vue.js·vite
LaughingZhu1 小时前
PH热榜 | 2025-02-28
前端·人工智能·经验分享·搜索引擎·产品运营
Ink1 小时前
从源码角度看 React 的批量更新
前端·javascript
IT、木易1 小时前
防流、节抖、重绘、回流原理,以及实现方法和区别
前端·性能优化