import { useEffect, useMemo, useRef, useState } from 'react'; import type { HistoryLocationRow, TrackStop } from '../../api/types'; import { getAMapConfig, isAMapConfigured } from '../../config/appConfig'; import { isValidAMapCoordinate, loadAMap, wgs84ToGcj02, type AMapLike, type AMapMap, type AMapOverlay } from '../../integrations/amap'; function markerContent(kind: 'start' | 'end' | 'stop' | 'current', label?: string) { if (kind === 'current') return ''; return `
${label ?? ''}
`; } export function TrackMap({ points, stops, activeIndex, showStops, follow, onSelectIndex, onFollowChange }: { points: HistoryLocationRow[]; stops: TrackStop[]; activeIndex: number; showStops: boolean; follow: boolean; onSelectIndex: (index: number) => void; onFollowChange: (follow: boolean) => void; }) { const containerRef = useRef(null); const mapRef = useRef(null); const amapRef = useRef(null); const overlaysRef = useRef([]); const currentMarkerRef = useRef(null); const passedPathRef = useRef(null); const pathRef = useRef<[number, number][]>([]); const selectRef = useRef(onSelectIndex); const followChangeRef = useRef(onFollowChange); const [state, setState] = useState<'loading' | 'ready' | 'fallback' | 'error'>('loading'); const valid = useMemo(() => points.map((point, index) => ({ point, index })).filter(({ point }) => isValidAMapCoordinate(point.longitude, point.latitude)), [points]); useEffect(() => { selectRef.current = onSelectIndex; }, [onSelectIndex]); useEffect(() => { followChangeRef.current = onFollowChange; }, [onFollowChange]); useEffect(() => { if (!containerRef.current || !isAMapConfigured(getAMapConfig())) { setState('fallback'); return; } let cancelled = false; loadAMap(['AMap.Scale', 'AMap.ToolBar']).then((AMap) => { if (cancelled || !containerRef.current) return; const first = valid[0]?.point; const map = new AMap.Map(containerRef.current, { zoom: first ? 13 : 5, center: first ? wgs84ToGcj02(first.longitude, first.latitude) : wgs84ToGcj02(105.4, 35.9), viewMode: '2D', mapStyle: 'amap://styles/whitesmoke', showLabel: true, resizeEnable: true, zooms: [3, 20] }); map.addControl(new AMap.Scale()); if (AMap.ToolBar) map.addControl(new AMap.ToolBar({ position: { right: '18px', bottom: '148px' } })); map.on?.('dragstart', () => followChangeRef.current(false)); mapRef.current = map; amapRef.current = AMap; setState('ready'); }).catch(() => { if (!cancelled) setState('error'); }); return () => { cancelled = true; overlaysRef.current.forEach((overlay) => overlay.setMap?.(null)); currentMarkerRef.current?.setMap?.(null); mapRef.current?.destroy(); overlaysRef.current = []; currentMarkerRef.current = null; passedPathRef.current = null; pathRef.current = []; mapRef.current = null; amapRef.current = null; }; }, []); useEffect(() => { const AMap = amapRef.current; const map = mapRef.current; if (state !== 'ready' || !map || !valid.length || !AMap) return; overlaysRef.current.forEach((overlay) => overlay.setMap?.(null)); currentMarkerRef.current?.setMap?.(null); const path = valid.map(({ point }) => wgs84ToGcj02(point.longitude, point.latitude)); pathRef.current = path; const nextValidIndex = valid.findIndex(({ index }) => index >= activeIndex); const activeValidIndex = nextValidIndex >= 0 ? nextValidIndex : valid.length - 1; const fullPath = new AMap.Polyline({ path, strokeColor: '#2563eb', strokeWeight: 6, strokeOpacity: 0.82, lineJoin: 'round', lineCap: 'round', showDir: true, zIndex: 70 }); const passedPath = new AMap.Polyline({ path: path.slice(0, activeValidIndex + 1), strokeColor: '#18a86b', strokeWeight: 7, strokeOpacity: 0.96, lineJoin: 'round', lineCap: 'round', zIndex: 80 }); passedPathRef.current = passedPath; const first = valid[0]; const last = valid[valid.length - 1]; const start = new AMap.Marker({ position: path[0], anchor: 'center', content: markerContent('start', '始'), zIndex: 115 }); const end = new AMap.Marker({ position: path[path.length - 1], anchor: 'center', content: markerContent('end', '终'), zIndex: 115 }); start.on?.('click', () => selectRef.current(first.index)); end.on?.('click', () => selectRef.current(last.index)); const overlays: AMapOverlay[] = [fullPath, passedPath, start, end]; if (showStops) stops.slice(0, 80).forEach((stop, index) => { if (!isValidAMapCoordinate(stop.longitude, stop.latitude)) return; const marker = new AMap.Marker({ position: wgs84ToGcj02(stop.longitude, stop.latitude), anchor: 'center', content: markerContent('stop', String(index + 1)), zIndex: 105 }); marker.on?.('click', () => selectRef.current(Math.min(points.length - 1, Math.max(0, stop.sampledIndex)))); overlays.push(marker); }); const active = valid.find(({ index }) => index === activeIndex) ?? first; const current = new AMap.Marker({ position: wgs84ToGcj02(active.point.longitude, active.point.latitude), anchor: 'center', content: markerContent('current'), zIndex: 130 }); currentMarkerRef.current = current; overlaysRef.current = overlays; map.add([...overlays, current]); map.setFitView([fullPath], false, [72, 72, 168, 72]); }, [points, showStops, state, stops, valid]); useEffect(() => { const point = points[activeIndex]; if (!point || !isValidAMapCoordinate(point.longitude, point.latitude)) return; const position = wgs84ToGcj02(point.longitude, point.latitude); currentMarkerRef.current?.setPosition?.(position); const passedCount = valid.reduce((count, entry) => count + (entry.index <= activeIndex ? 1 : 0), 0); passedPathRef.current?.setPath?.(pathRef.current.slice(0, Math.max(1, passedCount))); if (follow) mapRef.current?.panTo?.(position, 180); }, [activeIndex, follow, points, valid]); return
{state !== 'ready' ?
{state === 'loading' ? <>轨迹地图加载中 : null} {state === 'fallback' ? `地图未配置,已载入 ${valid.length} 个有效轨迹点` : null} {state === 'error' ? '地图加载失败,请检查高德地图配置' : null}
: null}
; }