2025-03-09 01:11:43 +08:00
|
|
|
import React, { useEffect, useRef } from 'react';
|
|
|
|
import * as echarts from 'echarts';
|
|
|
|
import worldData from '@/assets/world.zh.json';
|
|
|
|
import chinaData from '@/assets/china.json';
|
|
|
|
|
2025-03-10 17:22:18 +08:00
|
|
|
interface WorldHeatmapProps {
|
|
|
|
visitedPlaces: string[];
|
|
|
|
}
|
|
|
|
|
|
|
|
const WorldHeatmap: React.FC<WorldHeatmapProps> = ({ visitedPlaces }) => {
|
2025-03-09 01:11:43 +08:00
|
|
|
const chartRef = useRef<HTMLDivElement>(null);
|
2025-03-27 21:40:41 +08:00
|
|
|
const chartInstanceRef = useRef<echarts.ECharts | null>(null);
|
2025-03-09 01:11:43 +08:00
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (!chartRef.current) return;
|
|
|
|
|
2025-03-27 21:40:41 +08:00
|
|
|
// 确保之前的实例被正确销毁
|
|
|
|
if (chartInstanceRef.current) {
|
|
|
|
chartInstanceRef.current.dispose();
|
|
|
|
}
|
|
|
|
|
|
|
|
// 初始化图表并保存实例引用
|
|
|
|
const chart = echarts.init(chartRef.current, null, {
|
|
|
|
renderer: 'canvas',
|
|
|
|
useDirtyRect: false
|
|
|
|
});
|
|
|
|
chartInstanceRef.current = chart;
|
2025-03-09 01:11:43 +08:00
|
|
|
|
|
|
|
const mergedWorldData = {
|
|
|
|
...worldData,
|
|
|
|
features: worldData.features.map((feature: any) => {
|
|
|
|
if (feature.properties.name === '中国') {
|
|
|
|
return {
|
|
|
|
...feature,
|
|
|
|
geometry: {
|
|
|
|
type: 'MultiPolygon',
|
2025-03-10 17:22:18 +08:00
|
|
|
coordinates: []
|
2025-03-09 01:11:43 +08:00
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
return feature;
|
|
|
|
}).concat(
|
|
|
|
chinaData.features.map((feature: any) => ({
|
|
|
|
...feature,
|
|
|
|
properties: {
|
|
|
|
...feature.properties,
|
|
|
|
name: feature.properties.name
|
|
|
|
}
|
|
|
|
}))
|
|
|
|
)
|
|
|
|
};
|
|
|
|
|
|
|
|
echarts.registerMap('merged-world', mergedWorldData as any);
|
|
|
|
|
2025-03-27 21:40:41 +08:00
|
|
|
// 检查当前是否为暗色模式
|
|
|
|
const isDarkMode = document.documentElement.classList.contains('dark');
|
|
|
|
|
|
|
|
// 根据当前模式设置颜色
|
|
|
|
const getChartColors = () => {
|
|
|
|
return {
|
|
|
|
textColor: isDarkMode ? '#ffffff' : '#374151',
|
|
|
|
borderColor: isDarkMode ? '#374151' : '#e5e7eb',
|
|
|
|
unvisitedColor: isDarkMode ? '#1f2937' : '#e5e7eb',
|
|
|
|
visitedColor: isDarkMode ? '#059669' : '#10b981',
|
|
|
|
emphasisColor: isDarkMode ? '#059669' : '#10b981',
|
|
|
|
tooltipBgColor: isDarkMode ? '#111827' : '#ffffff',
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
const colors = getChartColors();
|
|
|
|
|
|
|
|
// 使用动态颜色方案
|
2025-03-09 01:11:43 +08:00
|
|
|
const option = {
|
|
|
|
title: {
|
|
|
|
text: '我的旅行足迹',
|
|
|
|
left: 'center',
|
2025-03-27 21:40:41 +08:00
|
|
|
top: 20,
|
|
|
|
textStyle: {
|
|
|
|
color: colors.textColor,
|
|
|
|
fontWeight: 'bold'
|
|
|
|
}
|
2025-03-09 01:11:43 +08:00
|
|
|
},
|
|
|
|
tooltip: {
|
|
|
|
trigger: 'item',
|
|
|
|
formatter: ({name}: {name: string}) => {
|
2025-03-10 17:22:18 +08:00
|
|
|
const visited = visitedPlaces.includes(name);
|
2025-03-09 01:11:43 +08:00
|
|
|
return `${name}<br/>${visited ? '✓ 已去过' : '尚未去过'}`;
|
2025-03-27 21:40:41 +08:00
|
|
|
},
|
|
|
|
backgroundColor: colors.tooltipBgColor,
|
|
|
|
borderColor: colors.borderColor,
|
|
|
|
textStyle: {
|
|
|
|
color: colors.textColor
|
2025-03-09 01:11:43 +08:00
|
|
|
}
|
|
|
|
},
|
|
|
|
visualMap: {
|
|
|
|
show: true,
|
|
|
|
type: 'piecewise',
|
|
|
|
pieces: [
|
|
|
|
{ value: 1, label: '已去过' },
|
|
|
|
{ value: 0, label: '未去过' }
|
|
|
|
],
|
|
|
|
inRange: {
|
2025-03-27 21:40:41 +08:00
|
|
|
color: [colors.unvisitedColor, colors.visitedColor]
|
2025-03-09 01:11:43 +08:00
|
|
|
},
|
|
|
|
outOfRange: {
|
2025-03-27 21:40:41 +08:00
|
|
|
color: [colors.unvisitedColor]
|
2025-03-09 01:11:43 +08:00
|
|
|
},
|
|
|
|
textStyle: {
|
2025-03-27 21:40:41 +08:00
|
|
|
color: colors.textColor,
|
|
|
|
fontWeight: 500
|
2025-03-09 01:11:43 +08:00
|
|
|
}
|
|
|
|
},
|
|
|
|
series: [{
|
|
|
|
name: '旅行足迹',
|
|
|
|
type: 'map',
|
|
|
|
map: 'merged-world',
|
|
|
|
roam: true,
|
|
|
|
emphasis: {
|
|
|
|
label: {
|
2025-03-27 21:40:41 +08:00
|
|
|
show: true,
|
|
|
|
color: colors.textColor
|
2025-03-09 01:11:43 +08:00
|
|
|
},
|
|
|
|
itemStyle: {
|
2025-03-27 21:40:41 +08:00
|
|
|
areaColor: colors.emphasisColor
|
2025-03-09 01:11:43 +08:00
|
|
|
}
|
|
|
|
},
|
2025-03-27 21:40:41 +08:00
|
|
|
itemStyle: {
|
|
|
|
borderColor: colors.borderColor,
|
|
|
|
borderWidth: 1,
|
|
|
|
borderType: 'solid'
|
|
|
|
},
|
2025-03-09 01:11:43 +08:00
|
|
|
data: mergedWorldData.features.map((feature: any) => ({
|
|
|
|
name: feature.properties.name,
|
2025-03-10 17:22:18 +08:00
|
|
|
value: visitedPlaces.includes(feature.properties.name) ? 1 : 0
|
2025-03-09 01:11:43 +08:00
|
|
|
})),
|
|
|
|
nameProperty: 'name'
|
|
|
|
}]
|
|
|
|
};
|
|
|
|
|
|
|
|
chart.setOption(option);
|
2025-03-27 21:40:41 +08:00
|
|
|
|
|
|
|
// 确保图表初始化后立即调整大小以适应容器
|
|
|
|
chart.resize();
|
|
|
|
|
|
|
|
const handleResize = () => {
|
|
|
|
if (chartInstanceRef.current) {
|
|
|
|
chartInstanceRef.current.resize();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
window.addEventListener('resize', handleResize);
|
2025-03-09 01:11:43 +08:00
|
|
|
|
2025-03-27 21:40:41 +08:00
|
|
|
// 监听暗色模式变化并更新图表
|
|
|
|
const darkModeObserver = new MutationObserver(() => {
|
|
|
|
if (!chartRef.current) return;
|
|
|
|
|
|
|
|
// 检查当前是否为暗色模式
|
|
|
|
const newIsDarkMode = document.documentElement.classList.contains('dark');
|
|
|
|
|
|
|
|
if (chartInstanceRef.current) {
|
|
|
|
// 更新颜色设置
|
|
|
|
const newColors = {
|
|
|
|
textColor: newIsDarkMode ? '#ffffff' : '#374151',
|
|
|
|
borderColor: newIsDarkMode ? '#4b5563' : '#d1d5db',
|
|
|
|
unvisitedColor: newIsDarkMode ? '#1f2937' : '#e5e7eb',
|
|
|
|
visitedColor: newIsDarkMode ? '#059669' : '#10b981',
|
|
|
|
emphasisColor: newIsDarkMode ? '#059669' : '#10b981',
|
|
|
|
tooltipBgColor: newIsDarkMode ? '#111827' : '#ffffff',
|
|
|
|
};
|
|
|
|
|
|
|
|
// 更新图表选项
|
|
|
|
const newOption = {
|
|
|
|
title: {
|
|
|
|
textStyle: {
|
|
|
|
color: newColors.textColor
|
|
|
|
}
|
|
|
|
},
|
|
|
|
tooltip: {
|
|
|
|
backgroundColor: newColors.tooltipBgColor,
|
|
|
|
borderColor: newColors.borderColor,
|
|
|
|
textStyle: {
|
|
|
|
color: newColors.textColor
|
|
|
|
}
|
|
|
|
},
|
|
|
|
visualMap: {
|
|
|
|
inRange: {
|
|
|
|
color: [newColors.unvisitedColor, newColors.visitedColor]
|
|
|
|
},
|
|
|
|
outOfRange: {
|
|
|
|
color: [newColors.unvisitedColor]
|
|
|
|
},
|
|
|
|
textStyle: {
|
|
|
|
color: newColors.textColor
|
|
|
|
}
|
|
|
|
},
|
|
|
|
series: [{
|
|
|
|
emphasis: {
|
|
|
|
label: {
|
|
|
|
show: true,
|
|
|
|
color: newColors.textColor
|
|
|
|
},
|
|
|
|
itemStyle: {
|
|
|
|
areaColor: newColors.emphasisColor
|
|
|
|
}
|
|
|
|
},
|
|
|
|
itemStyle: {
|
|
|
|
borderColor: newColors.borderColor,
|
|
|
|
borderWidth: 1,
|
|
|
|
borderType: 'solid'
|
|
|
|
}
|
|
|
|
}]
|
|
|
|
};
|
|
|
|
|
|
|
|
// 应用新选项
|
|
|
|
chartInstanceRef.current.setOption(newOption);
|
|
|
|
}
|
2025-03-09 01:11:43 +08:00
|
|
|
});
|
|
|
|
|
2025-03-27 21:40:41 +08:00
|
|
|
darkModeObserver.observe(document.documentElement, { attributes: true });
|
|
|
|
|
2025-03-09 01:11:43 +08:00
|
|
|
return () => {
|
2025-03-27 21:40:41 +08:00
|
|
|
if (chartInstanceRef.current) {
|
|
|
|
chartInstanceRef.current.dispose();
|
|
|
|
chartInstanceRef.current = null;
|
|
|
|
}
|
|
|
|
window.removeEventListener('resize', handleResize);
|
|
|
|
darkModeObserver.disconnect();
|
2025-03-09 01:11:43 +08:00
|
|
|
};
|
2025-03-10 17:22:18 +08:00
|
|
|
}, [visitedPlaces]);
|
2025-03-09 01:11:43 +08:00
|
|
|
|
2025-03-27 21:40:41 +08:00
|
|
|
return (
|
|
|
|
<div
|
|
|
|
ref={chartRef}
|
|
|
|
className="w-full h-[600px] md:h-[500px] lg:h-[600px] xl:h-[700px] dark:[&_.echarts-tooltip]:bg-[#111827] dark:[&_.echarts-tooltip]:border-[#374151] dark:[&_.echarts-tooltip]:text-white dark:[&_.echarts-title]:text-white dark:[&_.echarts-visual-map]:text-white dark:[&_.echarts-map]:border-[#4b5563] dark:[&_.echarts-map-emphasis]:text-white dark:[&_.echarts-map-emphasis]:bg-[#059669] dark:[&_.echarts-map-unvisited]:bg-[#1f2937] dark:[&_.echarts-map-visited]:bg-[#059669]"
|
|
|
|
/>
|
|
|
|
);
|
2025-03-09 01:11:43 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
export default WorldHeatmap;
|