|
|
|
|
@ -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>
|