334 lines
6.6 KiB
Vue
334 lines
6.6 KiB
Vue
<template>
|
|
<div class="echarts-wrapper">
|
|
<!-- 加载状态 -->
|
|
<div v-if="loading" class="loading-mask">
|
|
<div class="loading-spinner"></div>
|
|
</div>
|
|
|
|
<div
|
|
ref="chartRef"
|
|
:style="{
|
|
width: width,
|
|
height: height,
|
|
minHeight: '100px'
|
|
}"
|
|
class="echarts-container"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted, onBeforeUnmount, watch, computed } from 'vue'
|
|
import * as echarts from 'echarts'
|
|
import debounce from 'lodash/debounce'
|
|
|
|
// Props 定义
|
|
const props = defineProps({
|
|
// 图表数据
|
|
data: {
|
|
type: Array,
|
|
required: true
|
|
},
|
|
// 图表类型配置
|
|
config: {
|
|
type: Object,
|
|
default: () => ({})
|
|
},
|
|
// 标题
|
|
title: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
// 图表主题
|
|
theme: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
// 图表宽度
|
|
width: {
|
|
type: String,
|
|
default: '100%'
|
|
},
|
|
// 图表高度
|
|
height: {
|
|
type: String,
|
|
default: '400px'
|
|
},
|
|
// 是否自动调整大小
|
|
autoResize: {
|
|
type: Boolean,
|
|
default: true
|
|
},
|
|
// 加载状态
|
|
loading: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
// 动画时长
|
|
animationDuration: {
|
|
type: Number,
|
|
default: 1000
|
|
},
|
|
// 是否平滑曲线(用于折线图)
|
|
smooth: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
// 图表类型
|
|
chartType: {
|
|
type: String,
|
|
default: 'line',
|
|
validator: (value) => ['line', 'bar', 'pie', 'scatter'].includes(value)
|
|
},
|
|
// tooltip 显示的系列名称数组,为空则显示全部
|
|
tooltipSeries: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
// 自定义 tooltip 格式化函数
|
|
tooltipFormatter: {
|
|
type: Function,
|
|
default: null
|
|
}
|
|
})
|
|
|
|
// Emits 定义
|
|
const emit = defineEmits(['chartReady', 'click', 'legendselectchanged'])
|
|
|
|
const chartRef = ref(null)
|
|
let chartInstance = null
|
|
|
|
// 默认配置
|
|
const defaultConfig = {
|
|
xField: 'name',
|
|
series: [
|
|
{
|
|
name: '数值',
|
|
field: 'value',
|
|
type: 'bar'
|
|
}
|
|
],
|
|
yAxis: [
|
|
{
|
|
name: '',
|
|
min: 0
|
|
}
|
|
]
|
|
}
|
|
|
|
// 合并配置
|
|
const mergedConfig = computed(() => ({
|
|
...defaultConfig,
|
|
...props.config
|
|
}))
|
|
|
|
// 生成图表配置
|
|
const generateOptions = computed(() => {
|
|
if (!props.data?.length) return {}
|
|
|
|
const config = mergedConfig.value
|
|
const categories = props.data.map(item => item[config.xField])
|
|
|
|
// 处理系列数据
|
|
const series = config.series
|
|
.filter(s => s.show !== false) // 过滤掉 show: false 的系列
|
|
.map(s => ({
|
|
name: s.name,
|
|
type: s.type,
|
|
yAxisIndex: s.yAxisIndex || 0,
|
|
data: props.data.map(item => item[s.field]),
|
|
color: s.color,
|
|
barMaxWidth: 50,
|
|
barGap: '30%',
|
|
itemStyle: {
|
|
color: s.color,
|
|
borderRadius: s.type === 'bar' ? [4, 4, 0, 0] : 0
|
|
},
|
|
label: s.showLabel ? {
|
|
show: true,
|
|
position: s.type === 'line' ? 'top' : 'inside',
|
|
formatter: s.labelFormatter || '{c}',
|
|
fontSize: 12,
|
|
color: s.type === 'line' ? s.color : '#fff'
|
|
} : undefined,
|
|
emphasis: {
|
|
focus: 'series'
|
|
}
|
|
}))
|
|
|
|
return {
|
|
title: props.title ? {
|
|
text: props.title
|
|
} : null,
|
|
tooltip: {
|
|
trigger: 'axis',
|
|
axisPointer: {
|
|
type: 'cross'
|
|
},
|
|
formatter: (params) => {
|
|
// 找到原始配置中对应的系列配置
|
|
const validParams = params.filter(param => {
|
|
const seriesConfig = config.series.find(s => s.name === param.seriesName)
|
|
return seriesConfig && seriesConfig.tooltip?.show !== false
|
|
})
|
|
|
|
if (validParams.length === 0) return ''
|
|
|
|
let result = `${validParams[0].axisValue}<br/>`
|
|
validParams.forEach(param => {
|
|
// 找到对应的系列配置
|
|
const seriesConfig = config.series.find(s => s.name === param.seriesName)
|
|
// 提取 labelFormatter 中的单位
|
|
let unit = ''
|
|
if (seriesConfig?.labelFormatter) {
|
|
const match = seriesConfig.labelFormatter.match(/\{c\}(.+)/)
|
|
if (match) {
|
|
unit = match[1]
|
|
}
|
|
}
|
|
result += `${param.marker}${param.seriesName}: ${param.value}${unit}<br/>`
|
|
})
|
|
|
|
return result
|
|
}
|
|
},
|
|
legend: {
|
|
data: series.map(s => s.name)
|
|
},
|
|
xAxis: {
|
|
type: 'category',
|
|
data: categories,
|
|
axisLabel: {
|
|
interval: 0,
|
|
rotate: categories.length > 5 ? 30 : 0
|
|
}
|
|
},
|
|
yAxis: config.yAxis.map((axis, index) => ({
|
|
type: 'value',
|
|
name: axis.name,
|
|
min: axis.min,
|
|
max: axis.max,
|
|
position: index === 0 ? 'left' : 'right',
|
|
splitLine: {
|
|
show: index === 0
|
|
},
|
|
axisLabel: axis.axisLabel ? {
|
|
formatter: axis.axisLabel
|
|
} : undefined
|
|
})),
|
|
series,
|
|
grid: {
|
|
top: '15%',
|
|
bottom: '15%',
|
|
left: '10%',
|
|
right: '10%',
|
|
containLabel: true
|
|
}
|
|
}
|
|
})
|
|
|
|
// 初始化图表
|
|
const initChart = () => {
|
|
if (!chartRef.value) return
|
|
|
|
chartInstance = echarts.init(chartRef.value, props.theme)
|
|
chartInstance.setOption(generateOptions.value)
|
|
emit('chartReady', chartInstance)
|
|
}
|
|
|
|
// 更新图表
|
|
const updateChart = () => {
|
|
if (!chartInstance) return
|
|
chartInstance.setOption(generateOptions.value)
|
|
}
|
|
|
|
// 调整图表大小
|
|
const resizeChart = debounce(() => {
|
|
if (!chartInstance) return
|
|
chartInstance.resize()
|
|
}, 100)
|
|
|
|
// 监听配置变化
|
|
watch(
|
|
() => [props.data, props.config],
|
|
() => updateChart(),
|
|
{ deep: true }
|
|
)
|
|
|
|
// 监听主题变化
|
|
watch(
|
|
() => props.theme,
|
|
() => {
|
|
if (chartInstance) {
|
|
chartInstance.dispose()
|
|
}
|
|
initChart()
|
|
}
|
|
)
|
|
|
|
// 监听加载状态
|
|
watch(
|
|
() => props.loading,
|
|
(val) => {
|
|
if (chartInstance) {
|
|
val ? chartInstance.showLoading() : chartInstance.hideLoading()
|
|
}
|
|
}
|
|
)
|
|
|
|
onMounted(() => {
|
|
initChart()
|
|
if (props.autoResize) {
|
|
window.addEventListener('resize', resizeChart)
|
|
}
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
if (props.autoResize) {
|
|
window.removeEventListener('resize', resizeChart)
|
|
}
|
|
if (chartInstance) {
|
|
chartInstance.dispose()
|
|
chartInstance = null
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.echarts-wrapper {
|
|
position: relative;
|
|
width: 100%;
|
|
}
|
|
|
|
.echarts-container {
|
|
position: relative;
|
|
}
|
|
|
|
.loading-mask {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: rgba(255, 255, 255, 0.7);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 1;
|
|
}
|
|
|
|
.loading-spinner {
|
|
width: 40px;
|
|
height: 40px;
|
|
border: 3px solid #f3f3f3;
|
|
border-top: 3px solid #3498db;
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
|
|
@keyframes spin {
|
|
0% { transform: rotate(0deg); }
|
|
100% { transform: rotate(360deg); }
|
|
}
|
|
</style>
|