封装 ECharts Hook 适配多种图表容器

我的项目中有一个图表组件,通过父组件传递数据到图表组件,此时会出现一个问题:当我第二天再点进这个页面时,图表数据并没有更新(没有当天的数据,最新的日期是昨天),需要刷新页面后,才会展示最新的时间以及数据。封装hook后,尽管我没有关闭当面标签页,第二天再打开这个标签页,图表数据依旧能自动更新到最新。

在hooks目录下新建useEchart.ts

TypeScript 复制代码
//导入Echart
import * as echarts from 'echarts'
import { onBeforeUnmount, onMounted, Ref, watch } from 'vue'
export interface RefObject {
  current?: HTMLElement | null
}

export interface CallbackRef {
  (el: HTMLElement | null): void
}

export type EchartsOption = echarts.EChartsOption

export type container = Ref<HTMLElement | null> | HTMLElement | string | string[]

/**
 * 适配多种容器选择方式的 ECharts 封装
 * @param container - 容器选择器(支持 Ref, DOM 元素, ID 选择器, 类选择器)
 * @param option - 初始配置
 * @returns {chart: echarts.ECharts | null, update: (newOption: EChartsOption) => void}
 */
export function useEchart(
  container: container,
  option: EchartsOption = {},
): {
  chart: echarts.ECharts | null
  onChartEvent: (event: string, handler: (params: any) => void) => void
  offChartEvent: (event: string, handler: (params: any) => void) => void
  update: (newOption: EchartsOption) => void
  handleResize: () => void
} {
  let chart: echarts.ECharts | null = null
  let containerElement: HTMLElement | null = null
  let resizeObserver: ResizeObserver | null = null //ResizeObserver 实例

  //   辅助函数处理单个选择器
  const getContainerElementForSingle = (selector: string): HTMLElement | null => {
    if (selector.startsWith('#')) {
      return document.getElementById(selector.slice(1)) || null
    } else if (selector.startsWith('.')) {
      return (document.querySelector(selector) as HTMLElement) || null
    }
    // 直接ID 无#
    return document.getElementById(selector) || null
  }

  //获取容器元素
  const getContainerElement = (): HTMLElement | null => {
    if (container instanceof HTMLElement) {
      return container
    } else if (typeof container === 'string') {
      return getContainerElementForSingle(container)
    } else if ('value' in container) {
      // Ref 类型
      return container.value
    } else if (Array.isArray(container)) {
      // 多个选择器(返回第一个匹配)
      for (const selector of container) {
        const element = getContainerElementForSingle(selector)
        if (element) {
          return element
        }
      }
    }
    return null
  }

  //   初始化图表
  const initChart = (): void => {
    containerElement = getContainerElement()

    if (!containerElement) {
      console.error('无法获取容器元素')
      return
    }

    if (!chart) {
      chart = echarts.init(containerElement, 'infographic')
      resizeObserver = new ResizeObserver(() => {
        chart?.resize()
      })
      resizeObserver.observe(containerElement)
    }

    if (option) {
      chart.setOption(option)
    }
  }

  //   处理窗口大小变化
  const handleResize = () => {
    chart?.resize()
  }

  //   更新图表配置
  const update = (newOption: EchartsOption): void => {
    if (chart) {
      chart.setOption(newOption)
    }
  }
  // 新增:事件绑定方法
  const onChartEvent = (event: string, handler: (params: any) => void) => {
    chart?.on(event, handler)
  }

  const offChartEvent = (event: string, handler: (params: any) => void) => {
    chart?.off(event, handler)
  }
  //   响应式更新图表配置
  watch(
    () => option,
    newOption => update(newOption),
    {
      deep: true,
    },
  )

  onMounted(() => {
    initChart()
  })

  onBeforeUnmount(() => {
    if (chart) {
      // 清理 ResizeObserver 实例
      if (resizeObserver) {
        resizeObserver.disconnect()
        resizeObserver = null
      }
      chart.dispose()
      chart = null
    }
  })
  return {
    get chart() {
      return chart
    },
    onChartEvent,
    offChartEvent,
    update,
    handleResize,
  }
}

在页面中调用hook:

javascript 复制代码
<template>
  <div w-full>
    <div ref="chartRef" class="chart-container" w-full h-400></div>
  </div>
</template>

<script setup lang="ts">
import { ref,  watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useEchart } from '@eam/ui-components'
const { t } = useI18n()
const props = defineProps({
  visitTrend: {
    type: Object,
    default: () => ({
      dateType: 'day',
      visitData: { dates: [], visitCounts: [], userCounts: [] },
    }),
  },
})


// 图表实例
const chartRef = ref(null)

// 生成图表配置
const getChartOption = data => ({
  tooltip: { trigger: 'axis', padding: 10 },
  xAxis: [
    {
      type: 'category',
      data: data.dates,
    },
  ],
  yAxis: [
    {
      type: 'value',
      min: 0,
      splitLine: { lineStyle: { color: '#f5f5f5' } },
    },
    // { type: 'value', show: false },
  ],
  series: [
    {
      name: t('statistics.visitor.visitors'),
      type: 'bar',
      itemStyle: { color: '#C893FD', borderRadius: [12, 12, 0, 0] },
      barWidth: 20,
      data: data.userCounts,
    },
    {
      name: t('statistics.visitor.visits'),
      type: 'line',
      // yAxisIndex: 1,
      lineStyle: { color: '#4A3AFF', width: 2 },
      symbol: 'none',
      data: data.visitCounts,
    },
  ],
  grid: { top: '5%', bottom: '10%', left: '5%', right: '5%' },
})

// 初始化图表
const { update } = useEchart(chartRef, getChartOption(props.visitTrend.visitData))

// 监听数据变化更新图表
watch(
  () => props.visitTrend.visitData,
  newData => {
    update(getChartOption(newData))
  },
  { deep: true, immediate: false }, // 非必要不立即执行,避免挂载时重复调用
)
</script>
相关推荐
忆往wu前7 分钟前
从0到1一步步拆解搭建,梳理一个 Vue3 简易图书后台全开发流程
前端·javascript·vue.js
木斯佳14 分钟前
前端八股文面经大全:字节抖音前端三面(2026-04-27)·面经深度解析
前端·面试·笔试·八股·面经
光影少年38 分钟前
大屏页面,一次多个请求,请求加密导致 点击 全局时间选择器 时出现卡顿咋解决(面板收起会延迟1~2秒)
前端·javascript·vue.js·学习·前端框架·echarts·reactjs
Mr.mjw1 小时前
vue中封装一个环形进度条组件,根据外部盒子大小自适应变化
前端·javascript·vue.js
无心使然1 小时前
Openlayers调用ArcGis影像服务之一动态地图、地图切片(/exportImage)
前端·javascript·数据可视化
唯火锅不可辜负1 小时前
uniapp开发公众号订阅功能踩坑小记
前端·vue.js
opteOG1 小时前
游览器跨域问题详解
前端
SameX1 小时前
后台 GPS 记录从半天掉电 30% 到全天 8%,我的三版方案演进
前端
Cder1 小时前
用 React + Ink 在终端里「优雅搜索」:开源 CLI 设计与非交互模式实践
前端·agent
像我这样帅的人丶你还1 小时前
前端监控体系与实践(二):全局监控
前端·javascript·vue.js