perf(tracks): bound playback rendering work

This commit is contained in:
lingniu
2026-07-16 04:06:08 +08:00
parent 982acb7730
commit 8e8f3f9f5c
7 changed files with 143 additions and 25 deletions

View File

@@ -9,8 +9,11 @@ const overlaySetMap = vi.fn();
const mapOff = vi.fn();
const mapDestroy = vi.fn();
const markerListeners: Array<{ overlay: TestOverlay; event: string; handler: () => void }> = [];
const overlayInstances: TestOverlay[] = [];
const mapInstances: TestMap[] = [];
class TestOverlay {
constructor() { overlayInstances.push(this); }
on = vi.fn((event: string, handler: () => void) => markerListeners.push({ overlay: this, event, handler }));
off = overlayOff;
setMap = overlaySetMap;
@@ -19,6 +22,7 @@ class TestOverlay {
}
class TestMap {
constructor() { mapInstances.push(this); }
add = vi.fn();
addControl = vi.fn();
setFitView = vi.fn();
@@ -90,6 +94,38 @@ afterEach(() => {
mapOff.mockReset();
mapDestroy.mockReset();
markerListeners.length = 0;
overlayInstances.length = 0;
mapInstances.length = 0;
});
test('uses the playback cadence for follow animation and advances the passed path', async () => {
window.__LINGNIU_APP_CONFIG__ = { amapWebJsKey: 'amap-web-key' };
window.AMapLoader = { load: vi.fn(async () => amapMock()) };
const view = render(<TrackMap
points={points}
stops={[]}
activeIndex={0}
showStops={false}
follow
followDurationMs={65}
onSelectIndex={() => undefined}
onFollowChange={() => undefined}
/>);
await waitFor(() => expect(mapInstances).toHaveLength(1));
view.rerender(<TrackMap
points={points}
stops={[]}
activeIndex={1}
showStops={false}
follow
followDurationMs={65}
onSelectIndex={() => undefined}
onFollowChange={() => undefined}
/>);
await waitFor(() => expect(mapInstances[0].panTo).toHaveBeenLastCalledWith(expect.any(Array), 65));
expect(overlayInstances.some((overlay) => overlay.setPath.mock.calls.some(([path]) => Array.isArray(path) && path.length === 2))).toBe(true);
});
test('detaches every marker listener and map listener on unmount', async () => {

View File

@@ -8,12 +8,13 @@ function markerContent(kind: 'start' | 'end' | 'stop' | 'current', label?: strin
return `<div class="v2-track-marker is-${kind}"><span>${label ?? ''}</span></div>`;
}
export function TrackMap({ points, stops, activeIndex, showStops, follow, onSelectIndex, onFollowChange }: {
export function TrackMap({ points, stops, activeIndex, showStops, follow, followDurationMs = 180, onSelectIndex, onFollowChange }: {
points: HistoryLocationRow[];
stops: TrackStop[];
activeIndex: number;
showStops: boolean;
follow: boolean;
followDurationMs?: number;
onSelectIndex: (index: number) => void;
onFollowChange: (follow: boolean) => void;
}) {
@@ -29,6 +30,15 @@ export function TrackMap({ points, stops, activeIndex, showStops, follow, onSele
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]);
const passedCountByPoint = useMemo(() => {
const counts = new Uint16Array(points.length);
let validIndex = 0;
for (let pointIndex = 0; pointIndex < points.length; pointIndex += 1) {
while (validIndex < valid.length && valid[validIndex].index <= pointIndex) validIndex += 1;
counts[pointIndex] = validIndex;
}
return counts;
}, [points.length, valid]);
useEffect(() => { selectRef.current = onSelectIndex; }, [onSelectIndex]);
useEffect(() => { followChangeRef.current = onFollowChange; }, [onFollowChange]);
@@ -127,10 +137,10 @@ export function TrackMap({ points, stops, activeIndex, showStops, follow, onSele
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);
const passedCount = passedCountByPoint[activeIndex] ?? 0;
passedPathRef.current?.setPath?.(pathRef.current.slice(0, Math.max(1, passedCount)));
if (follow) mapRef.current?.panTo?.(position, 180);
}, [activeIndex, follow, points, valid]);
if (follow) mapRef.current?.panTo?.(position, followDurationMs);
}, [activeIndex, follow, followDurationMs, passedCountByPoint, points]);
return <div className="v2-track-map">
<div ref={containerRef} className="v2-track-map-canvas" aria-label="历史轨迹地图" />