Files
ucvl-home-vision/frontend/src/components/LocalChart.vue
T

51 lines
3.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>