import { useEffect, useRef, useState, memo } from 'react'; import L from 'leaflet'; import { geocodedApi } from '@/services/api'; import type { GeocodedCase } from '@/types'; const WUHAN_CENTER: [number, number] = [30.59, 114.31]; interface CaseLocationMapProps { height?: string; district?: string | null; street?: string | null; } function CaseLocationMapComponent({ height = '400px', district = null, street = null }: CaseLocationMapProps) { const mapRef = useRef(null); const mapInstanceRef = useRef(null); const layerRef = useRef(null); const cancelledRef = useRef(false); const [isLoading, setIsLoading] = useState(true); const [caseCount, setCaseCount] = useState(0); useEffect(() => { if (!mapRef.current || mapInstanceRef.current) return; setIsLoading(true); cancelledRef.current = false; const map = L.map(mapRef.current, { center: WUHAN_CENTER, zoom: 11, zoomControl: true, }); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap', maxZoom: 18, }).addTo(map); mapInstanceRef.current = map; layerRef.current = L.layerGroup().addTo(map); // Fetch case locations geocodedApi.getGeocoded({ limit: 5000, district: district || undefined }) .then((data) => { if (cancelledRef.current) return; const cases: GeocodedCase[] = data.cases || []; const layer = layerRef.current; if (!layer) return; layer.clearLayers(); // Deduplicate by case_id to avoid overlapping markers const seen = new Set(); let unique: GeocodedCase[] = []; for (const c of cases) { if (!seen.has(c.case_id)) { seen.add(c.case_id); unique.push(c); } } // Client-side street filtering if (street) { unique = unique.filter((c) => c.street === street); } if (cancelledRef.current) return; for (const c of unique) { if (!c.latitude || !c.longitude) continue; const color = c.case_type === 'inpatient' ? '#ef4444' : '#3b82f6'; const marker = L.circleMarker([c.latitude, c.longitude], { radius: 3, fillColor: color, fillOpacity: 0.6, color: color, weight: 1, }); marker.bindTooltip( `
${c.district} ${c.street}
类型: ${c.case_type === 'inpatient' ? '住院' : '门诊'}
`, { direction: 'top', offset: [0, -4] } ); marker.addTo(layer); } setCaseCount(unique.length); setIsLoading(false); // Fit bounds to case locations if (unique.length > 0) { const bounds = L.latLngBounds(unique.map((c) => [c.latitude, c.longitude])); map.fitBounds(bounds, { padding: [30, 30] }); } }) .catch(() => { if (!cancelledRef.current) setIsLoading(false); }); return () => { cancelledRef.current = true; map.remove(); mapInstanceRef.current = null; }; }, [district, street]); return (
{isLoading && (
加载病例位置...
)} {!isLoading && (
{caseCount.toLocaleString()} 个病例位置 ● 住院 ● 门诊
)}
); } export const CaseLocationMap = memo(CaseLocationMapComponent);