"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(null); const instanceRef = useRef(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}
${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 (
); }