'x'
This commit is contained in:
135
app/lib/charts/MembersMap.tsx
Normal file
135
app/lib/charts/MembersMap.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
"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: { data?: { id?: number } }) => {
|
||||
const id = params.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%" }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user