Files
2026-03-11 23:17:16 -05:00

137 lines
3.6 KiB
TypeScript

"use client";
import { useEffect, useRef, useState } from "react";
import * as echarts from "echarts";
import type { MapCity } from "./types";
const CHINA_MAP_URL = "/api/geo/china";
export interface MembersMapProps {
data: MapCity[];
locale?: "zh" | "en";
dark?: boolean;
className?: string;
onCityHover?: (city: MapCity | null) => void;
}
export function MembersMap({
data,
locale = "zh",
dark = false,
className = "",
onCityHover,
}: MembersMapProps) {
const chartRef = useRef<HTMLDivElement>(null);
const instanceRef = useRef<echarts.ECharts | null>(null);
const [ready, setReady] = useState(false);
useEffect(() => {
if (!chartRef.current || data.length === 0) return;
const init = async () => {
try {
const res = await fetch(CHINA_MAP_URL);
const geoJson = await res.json();
echarts.registerMap("china", geoJson);
} catch {
// 若加载失败,使用 scatter 在 geo 坐标系(无底图)
echarts.registerMap("china", {
type: "FeatureCollection",
features: [],
});
}
const chart = echarts.init(chartRef.current!);
instanceRef.current = chart;
const scatterData = data.map((c) => ({
name: locale === "zh" ? c.name : c.nameEn,
value: [c.lng, c.lat, c.nomadsNow],
id: c.id,
}));
const maxVal = Math.max(...data.map((c) => c.nomadsNow), 1);
const option: echarts.EChartsOption = {
backgroundColor: "transparent",
tooltip: {
trigger: "item",
formatter: (params: unknown) => {
const p = params as { data: { value: [number, number, number]; name: string } };
return `${p.data.name}<br/>${p.data.value[2]} 人在此`;
},
},
geo: {
map: "china",
roam: true,
zoom: 1.2,
itemStyle: {
areaColor: dark ? "#1e293b" : "#f1f5f9",
borderColor: dark ? "#334155" : "#e2e8f0",
},
emphasis: {
itemStyle: {
areaColor: dark ? "#334155" : "#e2e8f0",
},
},
},
series: [
{
type: "scatter",
coordinateSystem: "geo",
data: scatterData,
symbolSize: (val: number[]) => 8 + (val[2] / maxVal) * 24,
itemStyle: {
color: "#ff4d4f",
borderColor: "#fff",
borderWidth: 1,
},
emphasis: {
scale: 1.3,
itemStyle: {
borderWidth: 2,
},
},
},
],
};
chart.setOption(option);
setReady(true);
chart.on("mouseover", (params: unknown) => {
const p = params as { data?: { id?: number } };
const id = p.data?.id;
const city = id ? (data.find((c) => c.id === id) ?? null) : null;
onCityHover?.(city);
});
chart.on("mouseout", () => onCityHover?.(null));
};
init();
return () => {
instanceRef.current?.dispose();
instanceRef.current = null;
};
}, [data, locale, onCityHover]);
useEffect(() => {
if (!instanceRef.current || !ready) return;
instanceRef.current.resize();
}, [ready]);
useEffect(() => {
const handleResize = () => instanceRef.current?.resize();
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
return (
<div
ref={chartRef}
className={className}
style={{ minHeight: 360, width: "100%" }}
/>
);
}