feat: local Bootstrap Vue ECharts dashboards and universal genealogy write scope
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user