hejianhao
2026-05-19 1be2d1271a4aa35e12caba0b3fcf08fcdc12153c
refactor(robotaxi): 优化附近车辆动画并修复多项问题

- 重写useNearbyVehicles钩子的动画逻辑,改用原生地图translateMarker减少React重渲染
- 缓存首页地图markers计算结果避免重复运算
- 修复WebSocket心跳请求缺少appUserId的问题
- 更新修改目的地流程的接口参数字段
4个文件已修改
134 ■■■■■ 已修改文件
src/pages/robotaxi/home/components/home-map-panel.tsx 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/hooks/use-nearby-vehicles.ts 110 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-modify-destination-flow.ts 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/websocket.ts 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/components/home-map-panel.tsx
@@ -1,3 +1,4 @@
import { useMemo } from 'react';
import { View, Map, Text, Image } from '@tarojs/components';
import MapOverlayTips from './map-overlay-tips';
import MapPointer from './map-pointer';
@@ -93,13 +94,20 @@
 * 首页地图区域与地图浮层。
 */
export default function HomeMapPanel(props: Props) {
  const displayMarkers = useMemo(() => {
    if (props.isLocationAuthDenied) {
      return [];
    }
    return props.mergedMapMarkers.map((item) => ({ ...item, title: '', name: '' }));
  }, [props.isLocationAuthDenied, props.mergedMapMarkers]);
  return (
    <Map
      id={HOME_MAP_ID}
      longitude={props.longitude ?? DEFAULT_LONGITUDE}
      latitude={props.latitude ?? DEFAULT_LATITUDE}
      scale={props.mapScale}
      markers={props.isLocationAuthDenied ? [] : props.mergedMapMarkers.map((item) => ({ ...item, title: '', name: '' }))}
      markers={displayMarkers}
      polyline={props.fencePolylines}
      polygons={props.polygons}
      showLocation={!!props.longitude}
src/pages/robotaxi/home/hooks/use-nearby-vehicles.ts
@@ -1,6 +1,8 @@
import Taro from '@tarojs/taro';
import { useCallback, useEffect, useRef, useState } from 'react';
import { getNearbyVehicles } from '../../../../robotaxi/api/home';
import { getDistance } from '../../../../utils/Robotaxi-public-method';
import { HOME_MAP_ID } from '../constants';
type Deps = {
  isOperatingArea: boolean;
@@ -32,7 +34,6 @@
const SETTLE_DELAY_MS = 500; // 地图停止后的静止判定时间(毫秒)
const MOVE_THRESHOLD_METERS = 500; // 触发新搜索的位移阈值(米)
const ANIMATION_DURATION_MS = 4500; // 车辆位移动画时长(毫秒)
const ANIMATION_FRAME_MS = 120; // 动画帧间隔(毫秒)
const VEHICLE_MARKER_ID_BASE = 900000000; // 车辆 marker id 起始段
const VEHICLE_MARKER_ICON = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/marker-car.png'; // 车辆 icon 地址
@@ -87,6 +88,19 @@
};
/**
 * 比较两批车辆集合是否存在新增或删除。
 */
const hasVehicleCollectionChanged = (
  currentVehicleMap: Map<string, NearbyVehicleItem>,
  nextVehicles: NearbyVehicleItem[]
) => {
  if (currentVehicleMap.size !== nextVehicles.length) {
    return true;
  }
  return nextVehicles.some((vehicle) => !currentVehicleMap.has(vehicle.id));
};
/**
 * 规范化接口返回,过滤非法经纬度。
 */
const normalizeVehicleList = (response: any): NearbyVehicleItem[] => {
@@ -120,27 +134,36 @@
  const [vehicleMarkers, setVehicleMarkers] = useState<any[]>([]); // 车辆 marker 列表
  const pollTimerRef = useRef<any>(null); // 轮询定时器句柄
  const settleTimerRef = useRef<any>(null); // 停稳判定定时器句柄
  const animationTimerRef = useRef<any>(null); // 动画帧定时器句柄
  const animationSyncTimerRef = useRef<any>(null); // 动画结束后同步 React marker 的定时器
  const requestLockRef = useRef(false); // 请求并发锁,防止重入
  const latestSeqRef = useRef(0); // 请求序号,防止旧响应覆盖新状态
  const activeSearchCenterRef = useRef<NearbyCenter | null>(null); // 当前轮询中心点
  const lastSearchCenterRef = useRef<NearbyCenter | null>(null); // 上一次发请求的中心点
  const renderedVehicleMapRef = useRef<Map<string, NearbyVehicleItem>>(new Map()); // 当前渲染中的车辆
  const animationStateRef = useRef<{ startAt: number; endAt: number; tracks: VehicleTrackItem[] } | null>(null); // 当前动画轨迹状态
  const mapContextRef = useRef<any>(null); // 首页地图上下文,负责原生 marker 平移
  const fetchAndAnimateVehiclesRef = useRef<(center: NearbyCenter) => Promise<void>>(async () => {});
  const ensurePollingRef = useRef<() => void>(() => {});
  const activateCenterAndFetchRef = useRef<(center: NearbyCenter) => void>(() => {});
  const disposeVehiclesRef = useRef<() => void>(() => {});
  /**
   * 清理动画定时器。
   * 获取首页地图上下文。
   */
  const clearAnimationTimer = () => {
    if (animationTimerRef.current) {
      clearTimeout(animationTimerRef.current);
      animationTimerRef.current = null;
  const getMapContext = () => {
    if (!mapContextRef.current) {
      mapContextRef.current = Taro.createMapContext(HOME_MAP_ID);
    }
    animationStateRef.current = null;
    return mapContextRef.current;
  };
  /**
   * 清理动画同步定时器。
   */
  const clearAnimationSyncTimer = () => {
    if (animationSyncTimerRef.current) {
      clearTimeout(animationSyncTimerRef.current);
      animationSyncTimerRef.current = null;
    }
  };
  /**
@@ -169,7 +192,7 @@
  const clearVehicles = () => {
    renderedVehicleMapRef.current = new Map();
    setVehicleMarkers([]);
    clearAnimationTimer();
    clearAnimationSyncTimer();
  };
  /**
@@ -193,35 +216,23 @@
  }, []);
  /**
   * 基于插值轨迹推进车辆动画帧。
   * 使用原生地图能力平移已有车辆 marker。
   */
  const playAnimationFrame = () => {
    const animationState = animationStateRef.current;
    if (!animationState) return;
    const now = Date.now();
    const progress = Math.max(0, Math.min(1, (now - animationState.startAt) / Math.max(1, animationState.endAt - animationState.startAt)));
    const nextVehicleMap = new Map<string, NearbyVehicleItem>();
    animationState.tracks.forEach((track) => {
      const lat = track.fromLat + (track.toLat - track.fromLat) * progress;
      const lon = track.fromLon + (track.toLon - track.fromLon) * progress;
      const direction = interpolateDirection(track.fromDirection, track.toDirection, progress);
      nextVehicleMap.set(track.id, {
        id: track.id,
        latitude: lat,
        longitude: lon,
        direction
  const translateVehicleMarkers = (tracks: VehicleTrackItem[]) => {
    const mapContext = getMapContext();
    tracks.forEach((track) => {
      const markerId = hashToMarkerId(track.id);
      (mapContext as any).translateMarker?.({
        markerId,
        destination: {
          latitude: track.toLat,
          longitude: track.toLon
        },
        duration: ANIMATION_DURATION_MS,
        autoRotate: false,
        rotate: interpolateDirection(track.fromDirection, track.toDirection, 1)
      });
    });
    renderedVehicleMapRef.current = nextVehicleMap;
    setVehicleMarkers(buildVehicleMarkers(Array.from(nextVehicleMap.values())));
    if (progress >= 1) {
      animationStateRef.current = null;
      animationTimerRef.current = null;
      return;
    }
    animationTimerRef.current = setTimeout(() => {
      playAnimationFrame();
    }, ANIMATION_FRAME_MS);
  };
  /**
@@ -240,8 +251,17 @@
        clearVehicles();
        return;
      }
      const now = Date.now();
      const currentVehicleMap = renderedVehicleMapRef.current;
      const collectionChanged = hasVehicleCollectionChanged(currentVehicleMap, normalizedVehicles);
      const nextVehicleMap = new Map<string, NearbyVehicleItem>(
        normalizedVehicles.map((vehicle) => [vehicle.id, vehicle])
      );
      if (currentVehicleMap.size === 0 || collectionChanged) {
        clearAnimationSyncTimer();
        renderedVehicleMapRef.current = nextVehicleMap;
        setVehicleMarkers(buildVehicleMarkers(normalizedVehicles));
        return;
      }
      const tracks: VehicleTrackItem[] = normalizedVehicles.map((vehicle) => {
        const currentVehicle = currentVehicleMap.get(vehicle.id);
        return {
@@ -254,13 +274,15 @@
          toDirection: vehicle.direction
        };
      });
      clearAnimationTimer();
      animationStateRef.current = {
        startAt: now,
        endAt: now + ANIMATION_DURATION_MS,
        tracks
      };
      playAnimationFrame();
      renderedVehicleMapRef.current = nextVehicleMap;
      translateVehicleMarkers(tracks);
      clearAnimationSyncTimer();
      animationSyncTimerRef.current = setTimeout(() => {
        if (seq !== latestSeqRef.current) {
          return;
        }
        setVehicleMarkers(buildVehicleMarkers(Array.from(nextVehicleMap.values())));
      }, ANIMATION_DURATION_MS);
    } catch (error) {
      // 请求异常时保持当前车辆状态,避免闪断。
    } finally {
src/robotaxi/pages/begin-route/hooks/use-modify-destination-flow.ts
@@ -212,8 +212,8 @@
      await orderChange({
        endPointId: previewEndSite.id || 0,
        orderId: Taro.getStorageSync('orderId') || '',
        selectedRouteId: selectedRoute?.route?.routeID || selectedRoute?.routeId || '',
        selectedRoutePlanRule: selectedRoute?.amapStrategy || ''
        selectedRouteId: selectedRoute?.route?.routeID || selectedRoute?.pathFingerprint || '',
        selectedRoutePlanRule: selectedRoute?.primaryRoutePlanRule || ''
      });
      // 清除本地修改状态,不再作为“本地覆盖”存在,因为订单已真正变更
src/utils/websocket.ts
@@ -482,21 +482,25 @@
      if (this.pendingHeartbeatSeq !== null) {
        return;
      }
      this.sendHeartbeat(connectionId);
      void this.sendHeartbeat(connectionId);
    }, this.heartbeatInterval);
  }
  /**
   * 发送一轮 ping,并等待服务端回 pong。
   */
  private sendHeartbeat(connectionId: number) {
  private async sendHeartbeat(connectionId: number) {
    if (!this.isCurrentConnection(connectionId) || !this.isAppActive) {
      return;
    }
    const appUserId = await this.getValidUid();
    if (!this.isCurrentConnection(connectionId) || !this.isAppActive) {
      return;
    }
    const currentHeartbeatSeq = ++this.heartbeatSeq;
    this.pendingHeartbeatSeq = currentHeartbeatSeq;
    const hasSent = this.sendInternal(
      { type: 'ping', timestamp: Date.now(), seq: currentHeartbeatSeq },
      { type: 'ping', appUserId, timestamp: Date.now(), seq: currentHeartbeatSeq },
      { silentWhenDisconnected: true }
    );