import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react'; import { Tag } from '@douyinfe/semi-ui'; import { getAMapConfig, isAMapConfigured } from '../config/appConfig'; export type VehicleMapPoint = { id: string; label: string; longitude: number; latitude: number; online?: boolean; title?: string; }; type AMapLike = { Map: new (container: HTMLDivElement, options: Record) => AMapMap; Marker: new (options: Record) => AMapOverlay; Polyline: new (options: Record) => AMapOverlay; Scale: new () => unknown; }; type AMapMap = { add: (overlay: AMapOverlay | AMapOverlay[]) => void; addControl: (control: unknown) => void; setFitView: (overlays?: AMapOverlay[] | null, immediate?: boolean, padding?: number[]) => void; destroy: () => void; }; type AMapOverlay = { setMap?: (map: AMapMap | null) => void; on?: (eventName: string, handler: () => void) => void; }; let amapLoaderPromise: Promise | null = null; function validPoint(point: VehicleMapPoint) { return Number.isFinite(point.longitude) && Number.isFinite(point.latitude) && point.longitude !== 0 && point.latitude !== 0; } function loadScript(src: string) { return new Promise((resolve, reject) => { const existing = document.querySelector(`script[src="${src}"]`); if (existing) { existing.addEventListener('load', () => resolve(), { once: true }); existing.addEventListener('error', () => reject(new Error('高德地图 Loader 加载失败')), { once: true }); if (window.AMapLoader) resolve(); return; } const script = document.createElement('script'); script.src = src; script.async = true; script.onload = () => resolve(); script.onerror = () => reject(new Error('高德地图 Loader 加载失败')); document.head.appendChild(script); }); } function loadAMap() { const config = getAMapConfig(); if (!isAMapConfigured(config)) { return Promise.reject(new Error('高德地图未配置')); } if (!amapLoaderPromise) { if (config.securityServiceHost) { window._AMapSecurityConfig = { serviceHost: config.securityServiceHost }; } else if (config.securityJsCode) { window._AMapSecurityConfig = { securityJsCode: config.securityJsCode }; } amapLoaderPromise = loadScript('https://webapi.amap.com/loader.js').then(() => { if (!window.AMapLoader) { throw new Error('高德地图 Loader 不可用'); } return window.AMapLoader.load({ key: config.webJsKey, version: '2.0', plugins: ['AMap.Scale'] }) as Promise; }); } return amapLoaderPromise; } function pointToPixelStyle(point: VehicleMapPoint, index: number) { if (!validPoint(point)) { return { left: `${18 + (index % 4) * 18}%`, top: `${28 + (index % 3) * 17}%` }; } const left = Math.min(88, Math.max(10, ((point.longitude + 180) / 360) * 100)); const top = Math.min(82, Math.max(12, ((90 - point.latitude) / 180) * 100)); return { left: `${left}%`, top: `${top}%` }; } export function VehicleMap({ points, mode = 'realtime', heightClassName = 'vp-map-monitor', fallbackLabel, maxFallbackPoints = 120, selectedId, onPointSelect, children }: { points: VehicleMapPoint[]; mode?: 'realtime' | 'track'; heightClassName?: string; fallbackLabel?: string; maxFallbackPoints?: number; selectedId?: string; onPointSelect?: (point: VehicleMapPoint) => void; children?: ReactNode; }) { const containerRef = useRef(null); const mapRef = useRef(null); const overlaysRef = useRef([]); const [status, setStatus] = useState<'ready' | 'fallback' | 'loading'>('fallback'); const config = getAMapConfig(); const validPoints = useMemo(() => points.filter(validPoint), [points]); useEffect(() => { let cancelled = false; if (!isAMapConfigured(config) || validPoints.length === 0 || !containerRef.current) { setStatus('fallback'); return; } setStatus('loading'); loadAMap() .then((AMap) => { if (cancelled || !containerRef.current) return; if (!mapRef.current) { mapRef.current = new AMap.Map(containerRef.current, { zoom: 11, center: [validPoints[0].longitude, validPoints[0].latitude], viewMode: '2D', mapStyle: 'amap://styles/normal' }); mapRef.current.addControl(new AMap.Scale()); } overlaysRef.current.forEach((overlay) => overlay.setMap?.(null)); const markers = validPoints.slice(0, 500).map((point, index) => { const marker = new AMap.Marker({ position: [point.longitude, point.latitude], title: point.title || point.label, zIndex: point.id === selectedId ? 300 : mode === 'track' ? 100 + index : 100, content: `
${mode === 'track' ? index + 1 : ''}
` }); marker.on?.('click', () => onPointSelect?.(point)); return marker; }); const nextOverlays: AMapOverlay[] = [...markers]; if (mode === 'track' && validPoints.length > 1) { nextOverlays.push(new AMap.Polyline({ path: validPoints.map((point) => [point.longitude, point.latitude]), strokeColor: '#1664ff', strokeOpacity: 0.85, strokeWeight: 5, lineJoin: 'round' })); } overlaysRef.current = nextOverlays; mapRef.current.add(nextOverlays); mapRef.current.setFitView(nextOverlays, false, [56, 56, 56, 56]); setStatus('ready'); }) .catch(() => { if (!cancelled) setStatus('fallback'); }); return () => { cancelled = true; }; }, [config.webJsKey, config.securityJsCode, config.securityServiceHost, mode, onPointSelect, selectedId, validPoints]); useEffect(() => () => { mapRef.current?.destroy(); mapRef.current = null; overlaysRef.current = []; }, []); const fallbackPoints = (validPoints.length > 0 ? validPoints : points).slice(0, maxFallbackPoints); const showFallback = status !== 'ready'; return (
{showFallback ? (
{fallbackPoints.map((point, index) => (
) : null} {children}
); }