src/components/Echarts/echarts.vue
@@ -1,12 +1,15 @@
<template>
  <div>
  <div style="position: relative;">
    <div ref="chartRef" :style="chartStyle"></div>
    <slot></slot>
  </div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, watchEffect } from 'vue'
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
import * as echarts from 'echarts'
const emit = defineEmits(['finished'])
// Props
const props = defineProps({
@@ -51,15 +54,15 @@
  },
  lineColors: {
    type: Array,
    default: () => ['#A4EEDA', '#86C1F4', '#91A0FC', '#F6C18B', '#F09595']
    default: () => []
  },
  barColors: {
    type: Array,
    default: () => ['#A4EEDA', '#86C1F4', '#91A0FC', '#F6C18B', '#F09595']
    default: () => []
  },
  pieColors: {
    type: Array,
    default: () => ['#A4EEDA', '#86C1F4', '#91A0FC', '#F6C18B', '#F09595']
    default: () => []
  },
  loadingOption: {
    type: Object,
@@ -70,45 +73,102 @@
      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)
  renderChart()
  // setOption 后补一次 resize,确保首屏尺寸正确
  nextTick(() => {
    if (chartInstance) chartInstance.resize()
  })
}
// Methods
function generateChart(option) {
  if (option.series && option.series.length > 0) {
    option.series.forEach((s, index) => {
      if (s.type === 'line') {
        s.itemStyle = {
          color: props.lineColors[index] || props.lineColors[0]
        }
        s.lineStyle = {
          color: props.lineColors[index] || props.lineColors[0]
        }
      } else if (s.type === 'bar') {
        s.itemStyle = {
          color: props.barColors[index] || props.barColors[0]
        }
  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(option)
  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
    tooltip: props.tooltip,
    visualMap: Object.keys(props.visualMap).length ? props.visualMap : undefined,
  }
  
  chartInstance.clear()
@@ -122,26 +182,39 @@
// Lifecycle hooks
onMounted(() => {
  chartInstance = echarts.init(chartRef.value)
  renderChart()
  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
watchEffect(() => {
  // 避免在 mounted 前触发
  if (chartInstance) {
    renderChart()
  }
}, {
  flush: 'post'
})
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>