feat(platform): consolidate production vehicle data workflows
This commit is contained in:
@@ -1,29 +1,36 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import type { HistoryLocationRow, TrackPlaybackEvent } from '../../api/types';
|
||||
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: string, label?: string) {
|
||||
if (kind === 'current') return '<div class="v2-track-current-marker"><span></span></div>';
|
||||
return `<div class="v2-track-marker is-${kind}">${label ?? ''}</div>`;
|
||||
function markerContent(kind: 'start' | 'end' | 'stop' | 'current', label?: string) {
|
||||
if (kind === 'current') return '<div class="v2-track-current-marker" aria-hidden="true"><i></i><span></span></div>';
|
||||
return `<div class="v2-track-marker is-${kind}"><span>${label ?? ''}</span></div>`;
|
||||
}
|
||||
|
||||
export function TrackMap({ points, events, activeIndex, onSelectIndex }: {
|
||||
export function TrackMap({ points, stops, activeIndex, showStops, follow, onSelectIndex, onFollowChange }: {
|
||||
points: HistoryLocationRow[];
|
||||
events: TrackPlaybackEvent[];
|
||||
stops: TrackStop[];
|
||||
activeIndex: number;
|
||||
showStops: boolean;
|
||||
follow: boolean;
|
||||
onSelectIndex: (index: number) => void;
|
||||
onFollowChange: (follow: boolean) => void;
|
||||
}) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const mapRef = useRef<AMapMap | null>(null);
|
||||
const amapRef = useRef<AMapLike | null>(null);
|
||||
const overlaysRef = useRef<AMapOverlay[]>([]);
|
||||
const currentMarkerRef = useRef<AMapOverlay | null>(null);
|
||||
const passedPathRef = useRef<AMapOverlay | null>(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; }
|
||||
@@ -34,10 +41,12 @@ export function TrackMap({ points, events, activeIndex, onSelectIndex }: {
|
||||
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
|
||||
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: '22px' } }));
|
||||
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');
|
||||
@@ -49,6 +58,8 @@ export function TrackMap({ points, events, activeIndex, onSelectIndex }: {
|
||||
mapRef.current?.destroy();
|
||||
overlaysRef.current = [];
|
||||
currentMarkerRef.current = null;
|
||||
passedPathRef.current = null;
|
||||
pathRef.current = [];
|
||||
mapRef.current = null;
|
||||
amapRef.current = null;
|
||||
};
|
||||
@@ -56,45 +67,54 @@ export function TrackMap({ points, events, activeIndex, onSelectIndex }: {
|
||||
|
||||
useEffect(() => {
|
||||
const AMap = amapRef.current;
|
||||
if (state !== 'ready' || !mapRef.current || !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));
|
||||
const polyline = new AMap.Polyline({ path, strokeColor: '#1268f3', strokeWeight: 5, strokeOpacity: 0.92, lineJoin: 'round', lineCap: 'round', showDir: true, zIndex: 80 });
|
||||
const first = valid[0];
|
||||
const last = valid[valid.length - 1];
|
||||
const overlays: AMapOverlay[] = [polyline];
|
||||
const start = new AMap.Marker({ position: wgs84ToGcj02(first.point.longitude, first.point.latitude), anchor: 'center', content: markerContent('start', '始'), zIndex: 110 });
|
||||
const end = new AMap.Marker({ position: wgs84ToGcj02(last.point.longitude, last.point.latitude), anchor: 'center', content: markerContent('end', '终'), zIndex: 110 });
|
||||
start.on?.('click', () => selectRef.current(first.index));
|
||||
end.on?.('click', () => selectRef.current(last.index));
|
||||
overlays.push(start, end);
|
||||
events.slice(1, -1).forEach((event, eventIndex) => {
|
||||
if (!isValidAMapCoordinate(event.longitude, event.latitude)) return;
|
||||
const exactIndex = points.findIndex((point) => point.deviceTime === event.time);
|
||||
const targetIndex = exactIndex >= 0 ? exactIndex : points.reduce((closest, point, index) => {
|
||||
const best = points[closest];
|
||||
const distance = (point.longitude - event.longitude) ** 2 + (point.latitude - event.latitude) ** 2;
|
||||
const bestDistance = (best.longitude - event.longitude) ** 2 + (best.latitude - event.latitude) ** 2;
|
||||
return distance < bestDistance ? index : closest;
|
||||
}, 0);
|
||||
const marker = new AMap.Marker({ position: wgs84ToGcj02(event.longitude, event.latitude), anchor: 'center', content: markerContent('event', String(eventIndex + 1)), zIndex: 105 });
|
||||
marker.on?.('click', () => selectRef.current(targetIndex));
|
||||
overlays.push(marker);
|
||||
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
|
||||
});
|
||||
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;
|
||||
mapRef.current.add([...overlays, current]);
|
||||
mapRef.current.setFitView(overlays, false, [52, 52, 52, 52]);
|
||||
}, [events, points, state, valid]);
|
||||
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;
|
||||
currentMarkerRef.current?.setPosition?.(wgs84ToGcj02(point.longitude, point.latitude));
|
||||
}, [activeIndex, points]);
|
||||
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 <div className="v2-track-map">
|
||||
<div ref={containerRef} className="v2-track-map-canvas" aria-label="历史轨迹地图" />
|
||||
@@ -103,6 +123,5 @@ export function TrackMap({ points, events, activeIndex, onSelectIndex }: {
|
||||
{state === 'fallback' ? `地图未配置,已载入 ${valid.length} 个有效轨迹点` : null}
|
||||
{state === 'error' ? '地图加载失败,请检查高德地图配置' : null}
|
||||
</div> : null}
|
||||
<div className="v2-track-map-legend"><span><i className="is-start" />开始</span><span><i className="is-current" />当前点</span><span><i className="is-end" />结束</span><b>{valid.length} 个有效点位</b></div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user