Files
medical-mall/uni_modules/charts/EChartsView.vue
2026-02-05 17:11:41 +08:00

435 lines
13 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.
<template>
<view class="ec-wrap">
<!-- 通过 props option 喂给 renderjs -->
<view
class="ec-canvas"
:prop="option"
:change:prop="ec.setOption"
:data-theme="theme"
/>
</view>
</template>
<script>
export default {
name: "EChartsView",
props: {
option: { type: Object, default: () => ({}) },
theme: { type: String, default: "light" },
},
};
</script>
<script module="ec" lang="renderjs">
import * as echarts from "echarts";
// 使用 Map 存储多个图表实例(支持多个 EChartsView 组件)
const charts = new Map();
const resizeObservers = new Map();
// 地图数据加载状态
let chinaMapLoaded = false;
let chinaMapLoading = false;
// 加载并注册中国地图
async function loadChinaMap() {
if (chinaMapLoaded) {
return Promise.resolve();
}
if (chinaMapLoading) {
// 如果正在加载,等待加载完成
return new Promise((resolve) => {
const checkInterval = setInterval(() => {
if (chinaMapLoaded) {
clearInterval(checkInterval);
resolve();
}
}, 100);
// 最多等待 10 秒
setTimeout(() => {
clearInterval(checkInterval);
resolve();
}, 10000);
});
}
chinaMapLoading = true;
try {
// 从在线 CDN 加载中国地图 GeoJSON 数据
// 使用 ECharts 官方示例数据源
const response = await fetch('https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json');
if (!response.ok) {
// 如果第一个源失败,尝试备用源
const backupResponse = await fetch('https://echarts.apache.org/examples/data/map/china.json');
if (!backupResponse.ok) {
throw new Error('Failed to load China map data');
}
const geoJson = await backupResponse.json();
echarts.registerMap('china', geoJson);
} else {
const geoJson = await response.json();
echarts.registerMap('china', geoJson);
}
chinaMapLoaded = true;
console.log('[EChartsView] 中国地图数据已加载并注册');
} catch (error) {
console.error('[EChartsView] 加载中国地图数据失败:', error);
// 即使加载失败,也标记为已尝试,避免重复请求
chinaMapLoaded = false;
} finally {
chinaMapLoading = false;
}
}
function getChartKey(el) {
// 使用元素的唯一标识作为 key
if (!el._echartsKey) {
el._echartsKey = 'echarts_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
}
return el._echartsKey;
}
function ensureChart(el, retryCount = 0) {
if (!el) return null;
const key = getChartKey(el);
let chart = charts.get(key);
// 如果图表已存在且有效,直接返回
if (chart && !chart.isDisposed()) {
return chart;
}
// 如果图表已销毁,从 Map 中移除
if (chart && chart.isDisposed()) {
charts.delete(key);
const ro = resizeObservers.get(key);
if (ro) {
ro.disconnect();
resizeObservers.delete(key);
}
chart = null;
}
// 确保元素有尺寸
const rect = el.getBoundingClientRect();
const computedStyle = window.getComputedStyle(el);
const width = parseFloat(computedStyle.width) || rect.width;
const height = parseFloat(computedStyle.height) || rect.height;
// 如果尺寸为 0尝试延迟初始化最多重试 10 次)
if ((width === 0 || height === 0) && retryCount < 10) {
if (retryCount === 0) {
console.warn('[EChartsView] 容器尺寸为 0延迟初始化', { width, height, rect });
}
// 使用指数退避策略,避免无限循环
const delay = Math.min(100 * Math.pow(1.5, retryCount), 1000);
setTimeout(() => {
ensureChart(el, retryCount + 1);
}, delay);
return null;
}
// 如果重试次数过多,使用默认尺寸
if (width === 0 || height === 0) {
console.warn('[EChartsView] 容器尺寸仍为 0使用默认尺寸', { width, height });
// 使用父元素尺寸或默认值
const parentRect = el.parentElement ? el.parentElement.getBoundingClientRect() : { width: 800, height: 400 };
const finalWidth = width || parentRect.width || 800;
const finalHeight = height || parentRect.height || 400;
if (finalWidth > 0 && finalHeight > 0) {
// 设置元素尺寸
el.style.width = finalWidth + 'px';
el.style.height = finalHeight + 'px';
} else {
console.error('[EChartsView] 无法确定容器尺寸,跳过初始化');
return null;
}
}
try {
// 注意:地图数据加载在 setOption 中处理,这里不处理
// 因为 ensureChart 是同步函数,不能使用 await
chart = echarts.init(el, null, {
renderer: "canvas",
width: rect.width,
height: rect.height
});
charts.set(key, chart);
// 自适应:监听容器尺寸变化
if (typeof ResizeObserver !== "undefined") {
const ro = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width, height } = entry.contentRect;
const c = charts.get(key);
if (c && !c.isDisposed() && width > 0 && height > 0) {
try {
c.resize({ width, height });
} catch (e) {
console.warn('[EChartsView] resize 失败', e);
}
}
}
});
ro.observe(el);
resizeObservers.set(key, ro);
} else {
// 兜底
const resizeHandler = () => {
const c = charts.get(key);
if (c && !c.isDisposed()) {
const rect = el.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
try {
c.resize();
} catch (e) {
console.warn('[EChartsView] resize 失败', e);
}
}
}
};
window.addEventListener("resize", resizeHandler);
// 存储 handler 以便后续清理
el._resizeHandler = resizeHandler;
}
return chart;
} catch (e) {
console.error('[EChartsView] 初始化失败', e);
return null;
}
}
function disposeChart(el) {
if (!el) return;
const key = getChartKey(el);
const chart = charts.get(key);
if (chart && !chart.isDisposed()) {
try {
chart.dispose();
} catch (e) {
console.warn('[EChartsView] dispose 失败', e);
}
}
charts.delete(key);
const ro = resizeObservers.get(key);
if (ro) {
ro.disconnect();
resizeObservers.delete(key);
}
if (el._resizeHandler) {
window.removeEventListener("resize", el._resizeHandler);
delete el._resizeHandler;
}
}
export default {
mounted() {
const el = this.$el.querySelector(".ec-canvas") || this.$el;
if (!el) {
console.error('[EChartsView] 找不到容器元素');
return;
}
// 延迟初始化,确保 DOM 已渲染
setTimeout(() => {
ensureChart(el);
}, 50);
},
beforeDestroy() {
const el = this.$el.querySelector(".ec-canvas") || this.$el;
if (el) {
disposeChart(el);
}
},
methods: {
async setOption(option, oldOption) {
const el = this.$el.querySelector(".ec-canvas") || this.$el;
if (!el) {
console.error('[EChartsView] setOption: 找不到容器元素');
return;
}
// 检查 option 是否有效
if (!option || typeof option !== 'object') {
console.warn('[EChartsView] setOption: option 无效', option);
return;
}
// 检查是否使用了地图,如果是,先加载地图数据
const needsMap = option.geo || (option.series && Array.isArray(option.series) && option.series.some(s => s.type === 'map' && s.map === 'china'));
if (needsMap) {
await loadChinaMap();
}
// 保存 option 供 ensureChart 使用
el._pendingOption = option;
// 确保图表已初始化
let c = ensureChart(el);
if (!c) {
// 如果容器尺寸为 0使用 ResizeObserver 等待容器尺寸可用
if (typeof ResizeObserver !== "undefined") {
let roTimeout = null;
const ro = new ResizeObserver(async (entries) => {
for (const entry of entries) {
const { width, height } = entry.contentRect;
if (width > 0 && height > 0) {
ro.disconnect();
if (roTimeout) clearTimeout(roTimeout);
c = ensureChart(el);
if (c && !c.isDisposed()) {
try {
// 如果使用地图,确保地图已加载
if (needsMap) {
await loadChinaMap();
}
const plainOption = JSON.parse(JSON.stringify(option));
c.setOption(plainOption, true);
requestAnimationFrame(() => {
const c2 = charts.get(getChartKey(el));
if (c2 && !c2.isDisposed()) {
try {
c2.resize({ width, height });
} catch (e) {
// 忽略已销毁的错误
}
}
});
} catch (e) {
console.error('[EChartsView] setOption 失败', e);
}
}
break;
}
}
});
ro.observe(el);
// 设置超时避免无限等待5秒后强制使用默认尺寸
roTimeout = setTimeout(() => {
ro.disconnect();
console.warn('[EChartsView] ResizeObserver 超时,尝试使用默认尺寸初始化');
// 尝试使用父元素尺寸或默认值
const parentRect = el.parentElement ? el.parentElement.getBoundingClientRect() : null;
const defaultWidth = parentRect ? parentRect.width : 800;
const defaultHeight = parentRect ? parentRect.height : 400;
if (defaultWidth > 0 && defaultHeight > 0) {
el.style.width = defaultWidth + 'px';
el.style.height = defaultHeight + 'px';
c = ensureChart(el);
if (c && !c.isDisposed()) {
try {
const plainOption = JSON.parse(JSON.stringify(option));
c.setOption(plainOption, true);
} catch (e) {
console.error('[EChartsView] setOption 失败', e);
}
}
}
}, 5000);
} else {
// 兜底延迟重试最多3次
let retryCount = 0;
const maxRetries = 3;
const retry = () => {
if (retryCount >= maxRetries) {
console.warn('[EChartsView] 重试次数过多,跳过初始化');
return;
}
retryCount++;
setTimeout(async () => {
if (needsMap) {
await loadChinaMap();
}
c = ensureChart(el);
if (c && !c.isDisposed()) {
try {
const plainOption = JSON.parse(JSON.stringify(option));
c.setOption(plainOption, true);
} catch (e) {
console.error('[EChartsView] setOption 失败', e);
}
} else if (retryCount < maxRetries) {
retry();
}
}, 200 * retryCount);
};
retry();
}
return;
}
// 检查图表是否已销毁
if (c.isDisposed()) {
console.warn('[EChartsView] setOption: 图表已销毁,重新初始化');
charts.delete(getChartKey(el));
if (needsMap) {
await loadChinaMap();
}
c = ensureChart(el);
if (!c) return;
}
try {
// 如果使用地图,确保地图已加载
if (needsMap) {
await loadChinaMap();
}
// 深拷贝 option 确保是纯 JS 对象
const plainOption = JSON.parse(JSON.stringify(option));
c.setOption(plainOption, true);
// 使用 requestAnimationFrame 避免 resize 警告
requestAnimationFrame(() => {
const key = getChartKey(el);
const c2 = charts.get(key);
if (c2 && !c2.isDisposed()) {
const rect = el.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
try {
c2.resize({ width: rect.width, height: rect.height });
} catch (e) {
// 忽略已销毁的错误(可能组件已卸载)
}
}
}
});
} catch (e) {
console.error('[EChartsView] setOption 失败', e, option);
}
},
},
};
</script>
<style>
.ec-wrap {
position: relative !important;
width: 100%;
height: 100%;
overflow: hidden; /* 防止 canvas 越界 */
}
.ec-canvas {
position: absolute !important;
left: 0;
right: 0;
top: 0;
bottom: 0;
width: 100% !important;
height: 100% !important;
display: block;
}
</style>