ai-coding
2026-06-29 87957d2faf58d564d60e034f38897b2b2bbb4e50
fix(map): 重新生成车辆 marker id

Co-Authored-By: ai-coding <ai-coding@devgenie.local>
4个文件已修改
129 ■■■■■ 已修改文件
src/robotaxi/pages/begin-route/hooks/use-map-markers.ts 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-vehicle-tracking.ts 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/share-trip/hooks/use-map-markers.ts 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/share-trip/hooks/use-vehicle-tracking.ts 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-map-markers.ts
@@ -60,7 +60,7 @@
  arrivedMarkerReady: boolean;
  arrivedTooltipReady: boolean;
  onVehicleMarkerReady: () => void;
  onCarMarkerWillRebuild: () => void;
  onCarMarkerWillRebuild: (newMarkerId: number) => void;
};
const defaultDeps: Deps = {
@@ -78,7 +78,7 @@
  arrivedMarkerReady: false,
  arrivedTooltipReady: false,
  onVehicleMarkerReady: () => { },
  onCarMarkerWillRebuild: () => { }
  onCarMarkerWillRebuild: (_newMarkerId: number) => { }
};
export default function useMapMarkers(rawDeps?: Partial<Deps>) {
@@ -92,11 +92,22 @@
  const carMarkerInitialized = useRef(false);
  const tripStatus6MarkerRefreshedRef = useRef(false);
  const rebuildCarMarkerTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  // 每次 rebuild 递增,用于派生新的 car marker id,避免原生层因 id 相同跳过销毁/重建导致旋转矩阵残留
  const carMarkerEpochRef = useRef(0);
  /**
   * 当逻辑 markers 变化时,对展示 markers 做"先移除车标 → 50ms 后恢复完整列表"的两阶段提交。
   * 该 effect 与 markers 解耦,不会被外部 useEffect 频繁触发打断(即便 vehicleLocation/vehicleDirection 更新,
   * 只要 logical markers 引用未变化,rebuild dance 就不会重启)。
   * 派生新的 car marker id:base + ((epoch % 1000) + 1) * 1000
   * - 与业务 base id(3/30)和其他 marker(pickup=1, dropoff=2, beforeUser=31)不冲突
   * - epoch 取模 1000 防止无限增长
   */
  const deriveNewCarMarkerId = (baseId: number) => {
    return baseId + ((carMarkerEpochRef.current % 1000) + 1) * 1000;
  };
  /**
   * 当逻辑 markers 变化时,对展示 markers 做"先移除车标 → 100ms 后用新 id 恢复"的两阶段提交。
   * 关键:恢复时给车标分配一个全新 markerId(base + epoch 派生),强制原生层创建新 marker 实例,
   *      不会因 id 相同被原生层复用,translateMarker 残留的旋转矩阵随之清除。
   */
  useEffect(() => {
    if (rebuildCarMarkerTimerRef.current) {
@@ -108,14 +119,16 @@
      setDisplayMarkers(markers);
      return;
    }
    // 通知外部:CarMarker 即将重建,请暂停 translateMarker
    deps.onCarMarkerWillRebuild();
    // 第一步:展示层先去掉车标,原生层销毁旧 car marker
    carMarkerEpochRef.current += 1;
    const newCarId = deriveNewCarMarkerId(deps.vehicleMarkerId);
    // 通知外部:CarMarker 即将以 newCarId 重建,暂停 translateMarker 并切换到新 id
    deps.onCarMarkerWillRebuild(newCarId);
    // 第一步:展示层先去掉车标(按业务 base id 过滤),原生层销毁旧 car marker
    setDisplayMarkers(markers.filter((m: any) => m.id !== deps.vehicleMarkerId));
    // 第二步:下一帧恢复完整列表,原生层重建 car marker(清除 translateMarker 遗留的旋转矩阵)
    // 第二步:100ms 后用 newCarId 重新挂载车标,原生层创建全新 marker 实例
    rebuildCarMarkerTimerRef.current = setTimeout(() => {
      rebuildCarMarkerTimerRef.current = null;
      setDisplayMarkers(markers);
      setDisplayMarkers(markers.map((m: any) => m.id === deps.vehicleMarkerId ? { ...m, id: newCarId } : m));
    }, 100);
    return () => {
      if (rebuildCarMarkerTimerRef.current) {
@@ -314,15 +327,15 @@
  }, [markers]);
  useEffect(() => {
    // 这里使用 displayMarkers 而非 markers(逻辑态),以保证 rebuild 完成(车标已真实渲染到原生地图)后才通知 ready
    const hasVehicleMarker = deps.statusRule.showCar && displayMarkers.some((item: any) => item.id === deps.vehicleMarkerId);
    // 这里使用 displayMarkers + name 匹配(而非 id),因为 displayMarkers 中 car id 每次 rebuild 都会变成新 id
    const hasVehicleMarker = deps.statusRule.showCar && displayMarkers.some((item: any) => item.name === '车辆位置');
    if (!hasVehicleMarker) {
      return;
    }
    Taro.nextTick(() => {
      deps.onVehicleMarkerReady();
    });
  }, [displayMarkers, deps.statusRule.showCar, deps.vehicleMarkerId, deps.onVehicleMarkerReady]);
  }, [displayMarkers, deps.statusRule.showCar, deps.onVehicleMarkerReady]);
  useEffect(() => {
    const pickupSite = deps.startSite?.latitude && deps.startSite?.longitude ? deps.startSite : Taro.getStorageSync('startSite');
src/robotaxi/pages/begin-route/hooks/use-vehicle-tracking.ts
@@ -211,7 +211,9 @@
      latitude: vehicleLocation.latitude,
      longitude: vehicleLocation.longitude,
      rotate: targetRotate,
      markerId: vehicleMarkerId,
      // 用 ref 而非 props:rebuild 期间 suspendVehicleMarker(newId) 已经把 ref 切到新 id,
      // 必须读最新 ref 才能让 pending 的 markerId 与原生层新 marker 一致
      markerId: vehicleMarkerIdRef.current,
      duration: hasLocationDelta ? TRANSLATE_DURATION_MS : ROTATE_ONLY_DURATION_MS
    };
    flushTranslateTask();
@@ -239,21 +241,27 @@
  }, []);
  /**
   * 暂停 translateMarker:清除 ready 状态、lastApplied 任务,递增 session 让在飞回调失效。
   * 用于 CarMarker 重建期间阻止 translateMarker 作用于即将被销毁的旧 marker。
   * 暂停 translateMarker 并切换到新的 markerId。
   * 在 CarMarker 重建期间调用:原生层即将销毁旧 marker、用 newMarkerId 创建新 marker,
   * 这里同步把 vehicleMarkerIdRef 切到新 id,pending 任务的 markerId 也一并更新,
   * 确保 ready 恢复后 translateMarker 作用于新 marker。
   *
   * 关键:保留 pendingTranslateRef.current。
   * - 若 suspend 前已有 pending(最近一次 vehicleLocation/vehicleDirection 推送写入但尚未 flush),
   *   ready 恢复后会直接消费它,无需等下一次 socket 推送。
   * - 若 suspend 后再有新推送,translateCar useEffect 会用最新值覆盖 pending。
   * 后续调用 notifyVehicleMarkerReady 会重新走 ready 延时并 flush 平移任务。
   * 关键:保留 pendingTranslateRef.current(只改 markerId),
   * 若 suspend 前已有 pending(最近一次推送写入但尚未 flush),ready 恢复后会直接消费它。
   */
  const suspendVehicleMarker = useCallback(() => {
  const suspendVehicleMarker = useCallback((newMarkerId: number) => {
    isTranslatingRef.current = false;
    lastAppliedTranslateTaskRef.current = null;
    vehicleMarkerReadyRef.current = false;
    clearVehicleMarkerReadyTimer();
    translateSessionIdRef.current += 1;
    vehicleMarkerIdRef.current = newMarkerId;
    if (pendingTranslateRef.current) {
      pendingTranslateRef.current = {
        ...pendingTranslateRef.current,
        markerId: newMarkerId
      };
    }
  }, []);
  return {
src/robotaxi/pages/share-trip/hooks/use-map-markers.ts
@@ -54,7 +54,7 @@
    vehicleMarkerId: number;
    arrivedMarkerReady: boolean;
    onVehicleMarkerReady: () => void;
    onCarMarkerWillRebuild: () => void;
    onCarMarkerWillRebuild: (newMarkerId: number) => void;
};
const defaultDeps: Deps = {
@@ -68,7 +68,7 @@
    vehicleMarkerId: 3,
    arrivedMarkerReady: false,
    onVehicleMarkerReady: () => { },
    onCarMarkerWillRebuild: () => { }
    onCarMarkerWillRebuild: (_newMarkerId: number) => { }
};
export default function useMapMarkers(rawDeps?: Partial<Deps>) {
@@ -82,10 +82,22 @@
    const carMarkerInitialized = useRef(false);
    const tripStatus6MarkerRefreshedRef = useRef(false);
    const rebuildCarMarkerTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
    // 每次 rebuild 递增,用于派生新的 car marker id,避免原生层因 id 相同跳过销毁/重建导致旋转矩阵残留
    const carMarkerEpochRef = useRef(0);
    /**
     * 当逻辑 markers 变化时,对展示 markers 做"先移除车标 → 50ms 后恢复完整列表"的两阶段提交。
     * 该 effect 与外部 markers useEffect 解耦,避免被频繁的 vehicleLocation/vehicleDirection 更新打断。
     * 派生新的 car marker id:base + ((epoch % 1000) + 1) * 1000
     * - 与业务 base id(3/30)和其他 marker(pickup=1, dropoff=2)不冲突
     * - epoch 取模 1000 防止无限增长
     */
    const deriveNewCarMarkerId = (baseId: number) => {
        return baseId + ((carMarkerEpochRef.current % 1000) + 1) * 1000;
    };
    /**
     * 当逻辑 markers 变化时,对展示 markers 做"先移除车标 → 100ms 后用新 id 恢复"的两阶段提交。
     * 关键:恢复时给车标分配一个全新 markerId(base + epoch 派生),强制原生层创建新 marker 实例,
     *      不会因 id 相同被原生层复用,translateMarker 残留的旋转矩阵随之清除。
     */
    useEffect(() => {
      if (rebuildCarMarkerTimerRef.current) {
@@ -97,14 +109,16 @@
        setDisplayMarkers(markers);
        return;
      }
      // 通知外部:CarMarker 即将重建,请暂停 translateMarker
      deps.onCarMarkerWillRebuild();
      // 第一步:展示层先去掉车标,原生层销毁旧 car marker
      carMarkerEpochRef.current += 1;
      const newCarId = deriveNewCarMarkerId(deps.vehicleMarkerId);
      // 通知外部:CarMarker 即将以 newCarId 重建,暂停 translateMarker 并切换到新 id
      deps.onCarMarkerWillRebuild(newCarId);
      // 第一步:展示层先去掉车标(按业务 base id 过滤),原生层销毁旧 car marker
      setDisplayMarkers(markers.filter((m: any) => m.id !== deps.vehicleMarkerId));
      // 第二步:下一帧恢复完整列表,原生层重建 car marker(清除 translateMarker 遗留的旋转矩阵)
      // 第二步:100ms 后用 newCarId 重新挂载车标,原生层创建全新 marker 实例
      rebuildCarMarkerTimerRef.current = setTimeout(() => {
        rebuildCarMarkerTimerRef.current = null;
        setDisplayMarkers(markers);
        setDisplayMarkers(markers.map((m: any) => m.id === deps.vehicleMarkerId ? { ...m, id: newCarId } : m));
      }, 100);
      return () => {
        if (rebuildCarMarkerTimerRef.current) {
@@ -300,15 +314,15 @@
    }, [markers]);
    useEffect(() => {
        // 这里使用 displayMarkers 而非 markers(逻辑态),以保证 rebuild 完成(车标已真实渲染到原生地图)后才通知 ready
        const hasVehicleMarker = deps.statusRule.showCar && displayMarkers.some((item: any) => item.id === deps.vehicleMarkerId);
        // 这里使用 displayMarkers + name 匹配(而非 id),因为 displayMarkers 中 car id 每次 rebuild 都会变成新 id
        const hasVehicleMarker = deps.statusRule.showCar && displayMarkers.some((item: any) => item.name === '车辆位置');
        if (!hasVehicleMarker) {
            return;
        }
        Taro.nextTick(() => {
            deps.onVehicleMarkerReady();
        });
    }, [displayMarkers, deps.statusRule.showCar, deps.vehicleMarkerId, deps.onVehicleMarkerReady]);
    }, [displayMarkers, deps.statusRule.showCar, deps.onVehicleMarkerReady]);
    useEffect(() => {
        const pickupSite =
src/robotaxi/pages/share-trip/hooks/use-vehicle-tracking.ts
@@ -204,7 +204,9 @@
            latitude: vehicleLocation.latitude,
            longitude: vehicleLocation.longitude,
            rotate: targetRotate,
            markerId: vehicleMarkerId,
            // 用 ref 而非 props:rebuild 期间 suspendVehicleMarker(newId) 已经把 ref 切到新 id,
            // 必须读最新 ref 才能让 pending 的 markerId 与原生层新 marker 一致
            markerId: vehicleMarkerIdRef.current,
            duration: hasLocationDelta ? TRANSLATE_DURATION_MS : ROTATE_ONLY_DURATION_MS
        };
        flushTranslateTask();
@@ -232,20 +234,28 @@
    }, []);
    /**
     * 暂停 translateMarker:清除 ready 状态、lastApplied 任务,递增 session 让在飞回调失效。
     * 用于 CarMarker 重建期间阻止 translateMarker 作用于即将被销毁的旧 marker。
     * 暂停 translateMarker 并切换到新的 markerId。
     * 在 CarMarker 重建期间调用:原生层即将销毁旧 marker、用 newMarkerId 创建新 marker,
     * 这里同步把 vehicleMarkerIdRef 切到新 id,pending 任务的 markerId 也一并更新,
     * 确保 ready 恢复后 translateMarker 作用于新 marker。
     *
     * 关键:保留 pendingTranslateRef.current。
     * - 若 suspend 前已有 pending(最近一次推送写入但尚未 flush),ready 恢复后会直接消费它,无需等下一次 socket 推送。
     * - 若 suspend 后再有新推送,translateCar useEffect 会用最新值覆盖 pending。
     * 后续调用 notifyVehicleMarkerReady 会重新走 ready 延时并 flush 平移任务。
     * 关键:保留 pendingTranslateRef.current(只改 markerId)。
     * - 若 suspend 前已有 pending(最近一次推送写入但尚未 flush),ready 恢复后会直接消费它。
     * - 若 suspend 后再有新推送,translateCar useEffect 会用最新值覆盖 pending(markerId 用 ref 取最新)。
     */
    const suspendVehicleMarker = useCallback(() => {
    const suspendVehicleMarker = useCallback((newMarkerId: number) => {
        isTranslatingRef.current = false;
        lastAppliedTranslateTaskRef.current = null;
        vehicleMarkerReadyRef.current = false;
        clearVehicleMarkerReadyTimer();
        translateSessionIdRef.current += 1;
        vehicleMarkerIdRef.current = newMarkerId;
        if (pendingTranslateRef.current) {
            pendingTranslateRef.current = {
                ...pendingTranslateRef.current,
                markerId: newMarkerId
            };
        }
    }, []);
    return {