Files
medical-mall/uni_modules/charts/EChartsView.vue
2026-02-11 01:19:50 +08:00

642 lines
22 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;
// 内置的简化中国地图数据50个主要城市或地区的边界
const SIMPLIFIED_CHINA_MAP = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": { "name": "南海诸岛", "cp": [113.5439, 3.5951], "childNum": 0 },
"geometry": { "type": "MultiPolygon", "coordinates": [[[[113.5439, 3.5951], [113.5439, 3.5951]]]] }
},
{
"type": "Feature",
"properties": { "name": "北京", "cp": [116.4074, 39.9042], "childNum": 16 },
"geometry": { "type": "Polygon", "coordinates": [[[116, 39.5], [117, 39.5], [117, 40.5], [116, 40.5], [116, 39.5]]] }
},
{
"type": "Feature",
"properties": { "name": "天津", "cp": [117.2, 39.0842], "childNum": 16 },
"geometry": { "type": "Polygon", "coordinates": [[[116.7, 38.7], [117.7, 38.7], [117.7, 39.5], [116.7, 39.5], [116.7, 38.7]]] }
},
{
"type": "Feature",
"properties": { "name": "河北", "cp": [114.5149, 38.0428], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[113.5, 37.5], [119.5, 37.5], [119.5, 42.5], [113.5, 42.5], [113.5, 37.5]]] }
},
{
"type": "Feature",
"properties": { "name": "山西", "cp": [112.5489, 37.8739], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[110.5, 35], [114.5, 35], [114.5, 40.5], [110.5, 40.5], [110.5, 35]]] }
},
{
"type": "Feature",
"properties": { "name": "内蒙古", "cp": [111.7558, 40.8183], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[97, 37], [126, 37], [126, 54], [97, 54], [97, 37]]] }
},
{
"type": "Feature",
"properties": { "name": "辽宁", "cp": [123.4328, 41.8045], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[118.5, 40], [126.5, 40], [126.5, 45], [118.5, 45], [118.5, 40]]] }
},
{
"type": "Feature",
"properties": { "name": "吉林", "cp": [125.3235, 43.8957], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[121, 41.5], [130, 41.5], [130, 48.5], [121, 48.5], [121, 41.5]]] }
},
{
"type": "Feature",
"properties": { "name": "黑龙江", "cp": [126.6424, 45.7568], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[121, 43.5], [135, 43.5], [135, 55], [121, 55], [121, 43.5]]] }
},
{
"type": "Feature",
"properties": { "name": "上海", "cp": [121.4737, 31.2304], "childNum": 16 },
"geometry": { "type": "Polygon", "coordinates": [[[120.8, 30.7], [122, 30.7], [122, 31.9], [120.8, 31.9], [120.8, 30.7]]] }
},
{
"type": "Feature",
"properties": { "name": "江苏", "cp": [118.7969, 32.9387], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[118, 31], [122, 31], [122, 34.5], [118, 34.5], [118, 31]]] }
},
{
"type": "Feature",
"properties": { "name": "浙江", "cp": [120.1551, 30.2741], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[118.5, 28], [123, 28], [123, 31.5], [118.5, 31.5], [118.5, 28]]] }
},
{
"type": "Feature",
"properties": { "name": "安徽", "cp": [117.2272, 31.8654], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[114.5, 29.5], [119.5, 29.5], [119.5, 34.5], [114.5, 34.5], [114.5, 29.5]]] }
},
{
"type": "Feature",
"properties": { "name": "福建", "cp": [119.295492, 26.0745], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[118.5, 23.5], [121.5, 23.5], [121.5, 28], [118.5, 28], [118.5, 23.5]]] }
},
{
"type": "Feature",
"properties": { "name": "江西", "cp": [115.8581, 28.6832], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[113, 24], [118, 24], [118, 30], [113, 30], [113, 24]]] }
},
{
"type": "Feature",
"properties": { "name": "山东", "cp": [117.1205, 36.6519], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[114, 34.5], [122, 34.5], [122, 38], [114, 38], [114, 34.5]]] }
},
{
"type": "Feature",
"properties": { "name": "河南", "cp": [113.6254, 34.7466], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[111.5, 32], [117, 32], [117, 36.5], [111.5, 36.5], [111.5, 32]]] }
},
{
"type": "Feature",
"properties": { "name": "湖北", "cp": [114.3055, 30.5928], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[108, 28.5], [116, 28.5], [116, 33], [108, 33], [108, 28.5]]] }
},
{
"type": "Feature",
"properties": { "name": "湖南", "cp": [112.9388, 28.2282], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[108.5, 24.5], [114, 24.5], [114, 30], [108.5, 30], [108.5, 24.5]]] }
},
{
"type": "Feature",
"properties": { "name": "广东", "cp": [113.2644, 23.1291], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[112, 20.5], [116, 20.5], [116, 25], [112, 25], [112, 20.5]]] }
},
{
"type": "Feature",
"properties": { "name": "广西", "cp": [108.3661, 22.8170], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[104.5, 20.5], [112, 20.5], [112, 26.5], [104.5, 26.5], [104.5, 20.5]]] }
},
{
"type": "Feature",
"properties": { "name": "四川", "cp": [104.0665, 30.5702], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[100, 26], [108, 26], [108, 35], [100, 35], [100, 26]]] }
},
{
"type": "Feature",
"properties": { "name": "贵州", "cp": [106.7135, 26.5783], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[103.5, 24.5], [109.5, 24.5], [109.5, 29.5], [103.5, 29.5], [103.5, 24.5]]] }
},
{
"type": "Feature",
"properties": { "name": "云南", "cp": [102.7103, 24.8801], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[97.5, 21], [106.5, 21], [106.5, 30], [97.5, 30], [97.5, 21]]] }
},
{
"type": "Feature",
"properties": { "name": "陕西", "cp": [108.9402, 34.3416], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[105.5, 31.5], [111.5, 31.5], [111.5, 39], [105.5, 39], [105.5, 31.5]]] }
},
{
"type": "Feature",
"properties": { "name": "甘肃", "cp": [103.8343, 35.0080], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[92, 32], [108, 32], [108, 42], [92, 42], [92, 32]]] }
},
{
"type": "Feature",
"properties": { "name": "青海", "cp": [101.7782, 36.6171], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[89, 31.5], [104.5, 31.5], [104.5, 39.5], [89, 39.5], [89, 31.5]]] }
},
{
"type": "Feature",
"properties": { "name": "宁夏", "cp": [106.2586, 38.4680], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[104.5, 35.5], [107.5, 35.5], [107.5, 40], [104.5, 40], [104.5, 35.5]]] }
},
{
"type": "Feature",
"properties": { "name": "新疆", "cp": [87.6278, 43.7929], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[73, 26], [104, 26], [104, 49], [73, 49], [73, 26]]] }
},
{
"type": "Feature",
"properties": { "name": "西藏", "cp": [88.0959, 29.6470], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[78, 26], [99, 26], [99, 36.5], [78, 36.5], [78, 26]]] }
},
{
"type": "Feature",
"properties": { "name": "台湾", "cp": [120.9605, 23.6978], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[120.1, 22.5], [121.5, 22.5], [121.5, 25], [120.1, 25], [120.1, 22.5]]] }
},
{
"type": "Feature",
"properties": { "name": "香港", "cp": [114.1694, 22.3193], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[113.8, 22], [114.5, 22], [114.5, 22.6], [113.8, 22.6], [113.8, 22]]] }
},
{
"type": "Feature",
"properties": { "name": "澳门", "cp": [113.5439, 22.2987], "childNum": 0 },
"geometry": { "type": "Polygon", "coordinates": [[[113.3, 22.1], [113.7, 22.1], [113.7, 22.5], [113.3, 22.5], [113.3, 22.1]]] }
}
]
};
// 加载并注册中国地图
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 数据
let geoJson = null;
let loaded = false;
try {
const response = await fetch('https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json', {
timeout: 5000
});
if (response.ok) {
geoJson = await response.json();
loaded = true;
console.log('[EChartsView] 从阿里云 CDN 加载中国地图数据成功');
}
} catch (e) {
console.warn('[EChartsView] 从阿里云 CDN 加载失败,尝试备用源...', e);
}
// 如果第一个源失败,尝试备用源
if (!loaded) {
try {
const backupResponse = await fetch('https://echarts.apache.org/examples/data/map/china.json', {
timeout: 5000
});
if (backupResponse.ok) {
geoJson = await backupResponse.json();
loaded = true;
console.log('[EChartsView] 从 ECharts 官方源加载中国地图数据成功');
}
} catch (e) {
console.warn('[EChartsView] 从备用源加载也失败,使用内置简化地图', e);
}
}
// 如果网络加载都失败,使用内置的简化地图
if (!loaded) {
geoJson = SIMPLIFIED_CHINA_MAP;
console.log('[EChartsView] 使用内置简化中国地图数据');
}
// 注册地图
if (geoJson) {
echarts.registerMap('china', geoJson);
chinaMapLoaded = true;
console.log('[EChartsView] 中国地图数据已注册');
} else {
throw new Error('Failed to load or create China map data');
}
} catch (error) {
console.error('[EChartsView] 加载中国地图数据失败:', error);
// 降级方案:使用简化地图
try {
echarts.registerMap('china', SIMPLIFIED_CHINA_MAP);
chinaMapLoaded = true;
console.log('[EChartsView] 已使用内置简化地图作为降级方案');
} catch (furtherError) {
console.error('[EChartsView] 即使使用简化地图也失败:', furtherError);
}
} 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>