51 lines
3.4 KiB
Vue
51 lines
3.4 KiB
Vue
<script setup>
|
||
import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
|
||
const props = defineProps({ option: { type: Object, required: true }, label: String, active: { type: Boolean, default: true }, height: { type: Number, default: 260 } });
|
||
const emit = defineEmits(['select']);
|
||
const host = ref(null), failed = ref(false);
|
||
let chart, resizeObserver, themeObserver, frame, alive = true, loading = false, last = '', size = '';
|
||
function dispose() { chart?.dispose(); chart = null; last = ''; size = ''; }
|
||
function queue() { if (!frame && alive) frame = requestAnimationFrame(() => { frame = null; render(); }); }
|
||
async function render() {
|
||
if (!alive || !host.value || !props.active) { dispose(); return; }
|
||
const width = Math.floor(host.value.clientWidth), height = Math.floor(host.value.clientHeight);
|
||
if (!width || !height) return;
|
||
if (!chart) {
|
||
if (loading) return;
|
||
loading = true;
|
||
try {
|
||
const runtime = await import('../composables/charts-runtime.js');
|
||
if (!alive || !props.active || !host.value) return;
|
||
chart = runtime.init(host.value, null, { renderer: 'canvas', devicePixelRatio: Math.min(window.devicePixelRatio || 1, 2) });
|
||
chart.on('click', value => { if (value.data?.targetId) emit('select', value.data.targetId); });
|
||
failed.value = false;
|
||
} catch (error) { failed.value = true; console.error('Local chart unavailable', error); return; }
|
||
finally { loading = false; }
|
||
}
|
||
const css = getComputedStyle(document.documentElement), ink = css.getPropertyValue('--ink').trim() || '#253b46', muted = css.getPropertyValue('--ui-muted').trim() || '#627485', line = css.getPropertyValue('--ui-line').trim() || '#e3e9ef';
|
||
const fingerprint = JSON.stringify(props.option) + ink + muted + line;
|
||
// Periodic status refreshes must not restart chart layout/animation.
|
||
if (fingerprint !== last) {
|
||
const option = { ...props.option, animation: false, backgroundColor: 'transparent', textStyle: { color: ink, fontFamily: 'system-ui, Microsoft YaHei, sans-serif' }, aria: { enabled: true, description: props.label }, tooltip: { trigger: 'item', renderMode: 'richText', confine: true, ...props.option.tooltip } };
|
||
for (const axis of ['xAxis', 'yAxis']) if (option[axis]) option[axis] = { ...option[axis], axisLabel: { color: muted, ...option[axis].axisLabel }, splitLine: { lineStyle: { color: line } } };
|
||
option.series = (option.series || []).map(series => ({ ...series, label: { color: ink, ...series.label } }));
|
||
chart.setOption(option, { notMerge: true, lazyUpdate: false }); last = fingerprint;
|
||
}
|
||
const nextSize = width + ':' + height;
|
||
if (nextSize !== size) { chart.resize({ width, height }); size = nextSize; }
|
||
}
|
||
watch(() => [props.option, props.active], queue, { deep: true, flush: 'post' });
|
||
onMounted(() => {
|
||
resizeObserver = new ResizeObserver(queue); resizeObserver.observe(host.value);
|
||
themeObserver = new MutationObserver(queue); themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme', 'data-density'] });
|
||
queue();
|
||
});
|
||
onBeforeUnmount(() => { alive = false; cancelAnimationFrame(frame); resizeObserver?.disconnect(); themeObserver?.disconnect(); dispose(); });
|
||
</script>
|
||
<template>
|
||
<div class="local-chart-wrap">
|
||
<div ref="host" class="local-chart" role="img" :aria-label="label" :style="{ height: height + 'px' }"></div>
|
||
<p v-if="failed" class="small text-secondary px-3" role="status">图表暂未加载,可查看下方数据。</p>
|
||
</div>
|
||
</template>
|