gaoluyang
2026-06-02 7c2cdcbc7f5585b96fba76a07b0e4417a09c4d7e
src/components/Echarts/echarts.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,223 @@
<template>
  <div style="position: relative;">
    <div ref="chartRef" :style="chartStyle"></div>
    <slot></slot>
  </div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
import * as echarts from 'echarts'
const emit = defineEmits(['finished', 'click'])
// Props
const props = defineProps({
  options: {
    type: Object,
    default: () => ({})
  },
  chartStyle: {
    type: Object,
    default: () => ({
      height: '80%',
      width: '100%'
    })
  },
  dataset: {
    type: Object,
    default: () => { }
  },
  xAxis: {
    type: Array,
    default: () => []
  },
  yAxis: {
    type: Array,
    default: () => []
  },
  series: {
    type: Array,
    default: () => []
  },
  grid: {
    type: Object,
    default: () => ({})
  },
  legend: {
    type: Object,
    default: () => ({})
  },
  tooltip: {
    type: Object,
    default: () => ({})
  },
  lineColors: {
    type: Array,
    default: () => []
  },
  barColors: {
    type: Array,
    default: () => []
  },
  pieColors: {
    type: Array,
    default: () => []
  },
  loadingOption: {
    type: Object,
    default: () => ({
      text: '数据加载中...',
      color: '#00BAFF',
      textColor: '#000',
      maskColor: 'rgba(255, 255, 255, 0.8)',
      zlevel: 0
    })
  },
  color: {
    type: Array,
    default: () => []
  },
  visualMap: {
    type: Object,
    default: () => ({})
  },
  option: {
    type: Object,
    default: () => ({})
  },
})
import { watch } from 'vue'
// Refs
const chartRef = ref(null)
let chartInstance = null
let finishedHandler = null
let initTimer = null
let initAttempts = 0
function clearInitTimer() {
  if (initTimer) {
    clearTimeout(initTimer)
    initTimer = null
  }
}
function isContainerReady() {
  const el = chartRef.value
  if (!el) return false
  // offsetWidth/offsetHeight æ›´è´´è¿‘真实布局(为 0 å¾€å¾€ä»£è¡¨è¿˜æ²¡å¸ƒå±€/不可见)
  return el.offsetWidth > 0 && el.offsetHeight > 0
}
function initChartWhenReady() {
  clearInitTimer()
  initAttempts += 1
  if (!isContainerReady()) {
    // ç­‰å®¹å™¨çœŸæ­£æœ‰å°ºå¯¸ï¼ˆé¿å…é¦–屏初始化偏移/空白,热更新后才正常的情况)
    // æœ€å¤šé‡è¯•约 3 ç§’,避免无限循环
    if (initAttempts < 60) {
      initTimer = setTimeout(initChartWhenReady, 50)
    }
    return
  }
  if (chartInstance) return
  chartInstance = echarts.init(chartRef.value)
  finishedHandler = () => emit('finished')
  chartInstance.on('finished', finishedHandler)
  chartInstance.on('click', (params) => {
    emit('click', params)
  })
  renderChart()
  // setOption åŽè¡¥ä¸€æ¬¡ resize,确保首屏尺寸正确
  nextTick(() => {
    if (chartInstance) chartInstance.resize()
  })
}
// Methods
function generateChart(option) {
  const copiedOption = option
  if (copiedOption.series && copiedOption.series.length > 0) {
    copiedOption.series.forEach((s, index) => {
      if (s.type === 'line' && props.lineColors.length) {
        s.itemStyle = s.itemStyle || {}
        s.lineStyle = s.lineStyle || {}
        s.itemStyle.color = props.lineColors[index] || props.lineColors[0]
        s.lineStyle.color = props.lineColors[index] || props.lineColors[0]
      } else if (s.type === 'bar' && props.barColors.length) {
        s.itemStyle = s.itemStyle || {}
        s.itemStyle.color = props.barColors[index] || props.barColors[0]
      }
    })
  }
  chartInstance.setOption(copiedOption)
}
function renderChart() {
  const option = {
    color: props.color.length ? props.color : undefined,
    backgroundColor: props.options.backgroundColor || '#fff',
    textStyle: props.options.textStyle || { color: '#333' },
    xAxis: props.xAxis,
    yAxis: props.yAxis,
    dataset: props.dataset,
    series: props.series,
    grid: props.grid,
    legend: props.legend,
    tooltip: props.tooltip,
    visualMap: Object.keys(props.visualMap).length ? props.visualMap : undefined,
  }
  chartInstance.clear()
  generateChart(option)
}
function windowResizeListener() {
  if (!chartInstance) return
  chartInstance.resize()
}
// Lifecycle hooks
onMounted(() => {
  initAttempts = 0
  initChartWhenReady()
  window.addEventListener('resize', windowResizeListener)
})
onBeforeUnmount(() => {
  if (chartInstance) {
    window.removeEventListener('resize', windowResizeListener)
    if (finishedHandler) {
      chartInstance.off('finished', finishedHandler)
      finishedHandler = null
    }
    chartInstance.dispose()
    chartInstance = null
  }
  clearInitTimer()
})
// Watch all reactive props that affect the chart
watch(
  () => [props.xAxis, props.yAxis, props.series, props.legend, props.tooltip, props.visualMap],
  () => {
    // å¦‚果首屏还没初始化成功,等待容器 ready åŽå†æ¸²æŸ“
    if (!chartInstance) {
      initChartWhenReady()
      return
    }
    renderChart()
    // æ•°æ®å˜åŒ–后补一次 resize,避免布局变化导致的偏移
    nextTick(() => {
      if (chartInstance) chartInstance.resize()
    })
  },
  { deep: true, immediate: true }
)
</script>