<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'])
|
|
// 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)
|
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>
|