<!-- 折线图,支持多组数据,支持阶梯式动画效果 -->
|
<template>
|
<div
|
ref="chartRef"
|
class="relative w-[calc(100%+10px)]"
|
:style="{ height: props.height }"
|
v-loading="props.loading"
|
>
|
</div>
|
</template>
|
|
<script setup>
|
import { graphic } from '@/plugins/echarts'
|
import { getCssVar, hexToRgba } from '@/utils/ui'
|
import { useChartOps, useChartComponent } from '@/hooks/core/useChart'
|
defineOptions({ name: 'ArtLineChart' })
|
const props = defineProps({
|
height: { required: false, default: useChartOps().chartHeight },
|
loading: { required: false, default: false },
|
isEmpty: { required: false, default: false },
|
colors: { required: false, default: () => useChartOps().colors },
|
data: { required: false, default: () => [0, 0, 0, 0, 0, 0, 0] },
|
xAxisData: { required: false, default: () => [] },
|
lineWidth: { required: false, default: 2.5 },
|
showAreaColor: { required: false, default: false },
|
smooth: { required: false, default: true },
|
symbol: { required: false, default: 'none' },
|
symbolSize: { required: false, default: 6 },
|
animationDelay: { required: false, default: 200 },
|
showAxisLabel: { required: false, default: true },
|
showAxisLine: { required: false, default: true },
|
showSplitLine: { required: false, default: true },
|
showTooltip: { required: false, default: true },
|
showLegend: { required: false, default: false },
|
legendPosition: { required: false, default: 'bottom' }
|
})
|
const isAnimating = ref(false)
|
const animationTimers = ref([])
|
const animatedData = ref([])
|
const clearAnimationTimers = () => {
|
animationTimers.value.forEach((timer) => clearTimeout(timer))
|
animationTimers.value = []
|
}
|
const isMultipleData = computed(() => {
|
return (
|
Array.isArray(props.data) &&
|
props.data.length > 0 &&
|
typeof props.data[0] === 'object' &&
|
'name' in props.data[0]
|
)
|
})
|
const maxValue = computed(() => {
|
if (isMultipleData.value) {
|
const multiData = props.data
|
return multiData.reduce((max, item) => {
|
if (item.data?.length) {
|
const itemMax = Math.max(...item.data)
|
return Math.max(max, itemMax)
|
}
|
return max
|
}, 0)
|
} else {
|
const singleData = props.data
|
return singleData?.length ? Math.max(...singleData) : 0
|
}
|
})
|
const initAnimationData = () => {
|
if (isMultipleData.value) {
|
const multiData = props.data
|
return multiData.map((item) => ({
|
...item,
|
data: Array(item.data.length).fill(0)
|
}))
|
}
|
const singleData = props.data
|
return Array(singleData.length).fill(0)
|
}
|
const copyRealData = () => {
|
if (isMultipleData.value) {
|
return props.data.map((item) => ({ ...item, data: [...item.data] }))
|
}
|
return [...props.data]
|
}
|
const primaryColor = computed(() => getCssVar('--el-color-primary'))
|
const getColor = (customColor, index) => {
|
if (customColor) return customColor
|
if (index !== void 0) return props.colors[index % props.colors.length]
|
return primaryColor.value
|
}
|
const generateAreaStyle = (item, color) => {
|
if (!item.areaStyle && !item.showAreaColor && !props.showAreaColor) return void 0
|
const areaConfig = item.areaStyle || {}
|
if (areaConfig.custom) return areaConfig.custom
|
return {
|
color: new graphic.LinearGradient(0, 0, 0, 1, [
|
{
|
offset: 0,
|
color: hexToRgba(color, areaConfig.startOpacity || 0.2).rgba
|
},
|
{
|
offset: 1,
|
color: hexToRgba(color, areaConfig.endOpacity || 0.02).rgba
|
}
|
])
|
}
|
}
|
const generateSingleAreaStyle = () => {
|
if (!props.showAreaColor) return void 0
|
const color = getColor(props.colors[0])
|
return {
|
color: new graphic.LinearGradient(0, 0, 0, 1, [
|
{
|
offset: 0,
|
color: hexToRgba(color, 0.2).rgba
|
},
|
{
|
offset: 1,
|
color: hexToRgba(color, 0.02).rgba
|
}
|
])
|
}
|
}
|
const createSeriesItem = (config) => {
|
return {
|
name: config.name,
|
data: config.data,
|
type: 'line',
|
color: config.color,
|
smooth: config.smooth ?? props.smooth,
|
symbol: config.symbol ?? props.symbol,
|
symbolSize: config.symbolSize ?? props.symbolSize,
|
lineStyle: {
|
width: config.lineWidth ?? props.lineWidth,
|
color: config.color
|
},
|
areaStyle: config.areaStyle,
|
emphasis: {
|
focus: 'series',
|
lineStyle: {
|
width: (config.lineWidth ?? props.lineWidth) + 1
|
}
|
}
|
}
|
}
|
const generateChartOptions = (isInitial = false) => {
|
const options = {
|
animation: true,
|
animationDuration: isInitial ? 0 : 1300,
|
animationDurationUpdate: isInitial ? 0 : 1300,
|
grid: getGridWithLegend(props.showLegend && isMultipleData.value, props.legendPosition, {
|
top: 15,
|
right: 15,
|
left: 0
|
}),
|
tooltip: props.showTooltip ? getTooltipStyle() : void 0,
|
xAxis: {
|
type: 'category',
|
boundaryGap: false,
|
data: props.xAxisData,
|
axisTick: getAxisTickStyle(),
|
axisLine: getAxisLineStyle(props.showAxisLine),
|
axisLabel: getAxisLabelStyle(props.showAxisLabel)
|
},
|
yAxis: {
|
type: 'value',
|
min: 0,
|
max: maxValue.value,
|
axisLabel: getAxisLabelStyle(props.showAxisLabel),
|
axisLine: getAxisLineStyle(props.showAxisLine),
|
splitLine: getSplitLineStyle(props.showSplitLine)
|
}
|
}
|
if (props.showLegend && isMultipleData.value) {
|
options.legend = getLegendStyle(props.legendPosition)
|
}
|
if (isMultipleData.value) {
|
const multiData = animatedData.value
|
options.series = multiData.map((item, index) => {
|
const itemColor = getColor(props.colors[index], index)
|
const areaStyle = generateAreaStyle(item, itemColor)
|
return createSeriesItem({
|
name: item.name,
|
data: item.data,
|
color: itemColor,
|
smooth: item.smooth,
|
symbol: item.symbol,
|
lineWidth: item.lineWidth,
|
areaStyle
|
})
|
})
|
} else {
|
const singleData = animatedData.value
|
const computedColor = getColor(props.colors[0])
|
const areaStyle = generateSingleAreaStyle()
|
options.series = [
|
createSeriesItem({
|
data: singleData,
|
color: computedColor,
|
areaStyle
|
})
|
]
|
}
|
return options
|
}
|
const updateChartOptions = (options) => {
|
initChart(options)
|
}
|
const initChartWithAnimation = () => {
|
clearAnimationTimers()
|
isAnimating.value = true
|
animatedData.value = initAnimationData()
|
updateChartOptions(generateChartOptions(true))
|
if (isMultipleData.value) {
|
const multiData = props.data
|
const currentAnimatedData = animatedData.value
|
multiData.forEach((item, index) => {
|
const timer = window.setTimeout(
|
() => {
|
currentAnimatedData[index] = { ...item, data: [...item.data] }
|
animatedData.value = [...currentAnimatedData]
|
updateChartOptions(generateChartOptions(false))
|
},
|
index * props.animationDelay + 100
|
)
|
animationTimers.value.push(timer)
|
})
|
const totalDelay = (multiData.length - 1) * props.animationDelay + 1500
|
const finishTimer = window.setTimeout(() => {
|
isAnimating.value = false
|
}, totalDelay)
|
animationTimers.value.push(finishTimer)
|
} else {
|
nextTick(() => {
|
animatedData.value = copyRealData()
|
updateChartOptions(generateChartOptions(false))
|
isAnimating.value = false
|
})
|
}
|
}
|
const checkIsEmpty = () => {
|
if (Array.isArray(props.data) && typeof props.data[0] === 'number') {
|
const singleData = props.data
|
return !singleData.length || singleData.every((val) => val === 0)
|
}
|
if (Array.isArray(props.data) && typeof props.data[0] === 'object') {
|
const multiData = props.data
|
return (
|
!multiData.length ||
|
multiData.every((item) => !item.data?.length || item.data.every((val) => val === 0))
|
)
|
}
|
return true
|
}
|
const {
|
chartRef,
|
initChart,
|
getAxisLineStyle,
|
getAxisLabelStyle,
|
getAxisTickStyle,
|
getSplitLineStyle,
|
getTooltipStyle,
|
getLegendStyle,
|
getGridWithLegend,
|
isEmpty
|
} = useChartComponent({
|
props,
|
checkEmpty: checkIsEmpty,
|
watchSources: [() => props.data, () => props.xAxisData, () => props.colors],
|
onVisible: () => {
|
if (!isEmpty.value) {
|
initChartWithAnimation()
|
}
|
},
|
generateOptions: () => generateChartOptions(false)
|
})
|
const renderChart = () => {
|
if (!isAnimating.value && !isEmpty.value) {
|
initChartWithAnimation()
|
}
|
}
|
watch([() => props.data, () => props.xAxisData, () => props.colors], renderChart, { deep: true })
|
onMounted(() => {
|
renderChart()
|
})
|
onBeforeUnmount(() => {
|
clearAnimationTimers()
|
})
|
</script>
|