feat(components): 添加 Chart 组件支持 ECharts 渲染

main
zangch@mesnac.com 3 months ago
parent 966c25ba6e
commit 9550fdb5d9

@ -0,0 +1,202 @@
<template>
<div ref="chartRef" class="chart-host" />
</template>
<script setup lang="ts">
import { nextTick, onBeforeUnmount, onMounted, shallowRef, watch } from 'vue';
import * as echarts from 'echarts';
type ChartOption = echarts.EChartsOption & {
series?: Record<string, any> | Array<Record<string, any>>;
};
const props = withDefaults(
defineProps<{
chartOption?: ChartOption | null;
}>(),
{
chartOption: null
}
);
const chartRef = shallowRef<HTMLDivElement | null>(null);
const chart = shallowRef<echarts.ECharts | null>(null);
const resizeObserver = shallowRef<ResizeObserver | null>(null);
const pendingOption = shallowRef<ChartOption | null>(null);
const LIQUID_FILL_PLACEHOLDER_COLOR = 'rgba(19, 194, 194, 0.16)';
const clampPercent = (value: number) => {
if (Number.isNaN(value)) {
return 0;
}
return Math.min(100, Math.max(0, value));
};
// echarts-liquidfill
const buildLiquidFillFallbackSeries = (series: Record<string, any>) => {
const sourceData = Array.isArray(series?.data) ? series.data : [series?.data];
const rawValue = Number(sourceData[0] ?? 0);
const percentValue = clampPercent(rawValue * 100);
const colors = Array.isArray(series?.color) && series.color.length ? series.color : ['#36cfc9'];
return {
type: 'gauge',
min: 0,
max: 100,
startAngle: 90,
endAngle: -270,
radius: series?.radius || '74%',
center: series?.center || ['50%', '50%'],
pointer: {
show: false
},
anchor: {
show: false
},
progress: {
show: true,
roundCap: true,
overlap: false,
width: 18,
itemStyle: {
color: colors[0]
}
},
axisLine: {
lineStyle: {
width: 18,
color: [[1, LIQUID_FILL_PLACEHOLDER_COLOR]]
}
},
splitLine: {
show: false
},
axisTick: {
show: false
},
axisLabel: {
show: false
},
title: {
show: false
},
detail: {
valueAnimation: true,
offsetCenter: [0, '0%'],
color: series?.label?.color || colors[0],
fontSize: series?.label?.fontSize || 20,
formatter:
typeof series?.label?.formatter === 'string'
? series.label.formatter
: `${percentValue.toFixed(1)}%\n覆盖率`
},
data: [
{
value: percentValue
}
]
};
};
const normalizeOption = (option: ChartOption) => {
const sourceSeries = (Array.isArray(option?.series) ? option.series : option?.series ? [option.series] : []) as Array<Record<string, any>>;
if (!sourceSeries.some((item) => String(item?.type) === 'liquidFill')) {
return option;
}
return {
...option,
series: sourceSeries.map((item) => {
if (String(item?.type) !== 'liquidFill') {
return item;
}
return buildLiquidFillFallbackSeries(item);
})
} as ChartOption;
};
const initChart = () => {
if (chart.value || !chartRef.value) {
return chart.value;
}
//
chart.value = echarts.init(chartRef.value);
return chart.value;
};
const resize = () => {
chart.value?.resize();
};
const setData = async (option?: ChartOption | null) => {
pendingOption.value = option || null;
await nextTick();
if (!pendingOption.value) {
chart.value?.clear();
return;
}
const instance = initChart();
if (!instance) {
return;
}
instance.clear();
instance.setOption(normalizeOption(pendingOption.value), true);
resize();
};
const destroyChart = () => {
resizeObserver.value?.disconnect();
resizeObserver.value = null;
window.removeEventListener('resize', resize);
chart.value?.dispose();
chart.value = null;
};
const initResizeObserver = () => {
window.addEventListener('resize', resize);
if (!chartRef.value || typeof ResizeObserver === 'undefined') {
return;
}
// 使 window
resizeObserver.value = new ResizeObserver(() => {
resize();
});
resizeObserver.value.observe(chartRef.value);
};
watch(
() => props.chartOption,
(option) => {
void setData(option);
},
{
deep: true
}
);
onMounted(async () => {
initResizeObserver();
if (props.chartOption) {
await setData(props.chartOption);
}
});
onBeforeUnmount(() => {
destroyChart();
});
defineExpose({
chart,
resize,
setData
});
</script>
<style scoped>
.chart-host {
width: 100%;
height: 100%;
min-width: 0;
}
</style>
Loading…
Cancel
Save