hejianhao
2026-05-21 88213d3d71675d865c0e0b416d0ad6f4839c95e4
refactor(robotaxi): 多模块优化与bug修复

本次提交涵盖机器人出租车项目多个模块的优化与修复:
1. 新增导航快照ID字段,完善修改目的地流程的订单参数传递
2. 适配Android设备地图渲染,修正车辆标记点旋转角度,禁用平台特有动画行为
3. 重构地图标记管理逻辑,新增重复渲染校验,支持延迟提交标记点避让车辆动画,调整上一单终点标记点尺寸
4. 修复经纬度解析兼容问题,兼容多字段层级与大小写差异的接口返回
5. 修复个人中心菜单、车费详情卡片的图标路径错误
6. 完善页面返回前的状态清理逻辑,新增修改目的地相关缓存的自动清理
7. 优化空调控制组件,拆分常量配置,优化交互加载状态与提示文案,修复开关操作逻辑
8. 新增车辆动画剩余时长查询接口,支持地图吸附逻辑避让车辆动画周期
15个文件已修改
399 ■■■■■ 已修改文件
src/pages/robotaxi/home/hooks/use-map-state.ts 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/hooks/use-nearby-vehicles.ts 113 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/hooks/use-region-change.ts 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/index.tsx 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/api/types/begin-route.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car-modules/fare-detail-card.tsx 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car-modules/temperature-control.tsx 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-map-markers.ts 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-modify-destination-flow.ts 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-page-actions.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-route-request.ts 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-vehicle-tracking.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-web-socket-events.ts 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/index.tsx 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/personal-center/index.tsx 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/hooks/use-map-state.ts
@@ -1,9 +1,29 @@
import { useRef, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import Taro from '@tarojs/taro';
import { fetchMapData, getAutoScaleBySites } from '../../../../utils/Robotaxi-public-method';
const AUTO_SCALE_MAX = 17; // 自动缩放最大等级
const AUTO_SCALE_MIN = 11; // 自动缩放最小等级
/**
 * 比较两批站点 marker 的关键展示字段是否一致。
 */
const isSameMarkerList = (currentMarkers: any[], nextMarkers: any[]) => {
  if (currentMarkers.length !== nextMarkers.length) {
    return false;
  }
  return currentMarkers.every((marker, index) => {
    const nextMarker = nextMarkers[index];
    return marker?.id === nextMarker?.id
      && marker?.latitude === nextMarker?.latitude
      && marker?.longitude === nextMarker?.longitude
      && marker?.iconPath === nextMarker?.iconPath
      && marker?.width === nextMarker?.width
      && marker?.height === nextMarker?.height
      && marker?.zIndex === nextMarker?.zIndex
      && marker?.markerType === nextMarker?.markerType;
  });
};
/**
 * 地图状态管理:拥有地图坐标、围栏、标记点及地图业务数据刷新能力。
@@ -25,11 +45,56 @@
  const resetTimer = useRef<any>(null);
  const pendingNearbyRequestCountRef = useRef(0);
  const autoScaleAppliedCityCodeRef = useRef(''); // 已执行自动缩放的城市 code
  const markersRef = useRef<any[]>([]); // 当前站点 marker 快照
  const markerCommitTimerRef = useRef<any>(null); // 延迟提交站点 marker 的定时器
  /**
   * 清理延迟提交站点 marker 的定时器。
   */
  const clearMarkerCommitTimer = () => {
    if (markerCommitTimerRef.current) {
      clearTimeout(markerCommitTimerRef.current);
      markerCommitTimerRef.current = null;
    }
  };
  /**
   * 立即提交站点 marker 到地图。
   */
  const commitMarkers = (nextMarkers: any[]) => {
    if (!isSameMarkerList(markersRef.current, nextMarkers)) {
      markersRef.current = nextMarkers;
      setMarkers(nextMarkers);
    }
  };
  /**
   * 按指定延迟提交站点 marker,拖图查询可先拿结果、后避让车辆动画提交。
   */
  const scheduleMarkerCommit = (nextMarkers: any[], delayMs = 0) => {
    clearMarkerCommitTimer();
    if (delayMs <= 0) {
      commitMarkers(nextMarkers);
      return;
    }
    markerCommitTimerRef.current = setTimeout(() => {
      markerCommitTimerRef.current = null;
      commitMarkers(nextMarkers);
    }, delayMs);
  };
  /**
   * 根据经纬度和城市编码刷新地图业务数据(围栏内判断、电子围栏 ID、站点标记)。
   */
  const updateMapData = async (lat: number, lng: number, cityCode?: string) => {
  const updateMapData = async (
    lat: number,
    lng: number,
    cityCode?: string,
    options?: {
      commitMarkers?: boolean;
      markerCommitDelayMs?: number;
    }
  ) => {
    const code = cityCode || Taro.getStorageSync('selectedCity')?.code;
    if (!code) {
      return { isInside: false, electronicAreaId: '', newMarkers: [], sites: [] };
@@ -40,7 +105,9 @@
      const result = await fetchMapData(lat, lng, code);
      setIsOperatingArea(result.isInside);
      setElectronicAreaId(result.electronicAreaId);
      setMarkers(result.newMarkers);
      if (options?.commitMarkers !== false) {
        scheduleMarkerCommit(result.newMarkers, options?.markerCommitDelayMs || 0);
      }
      if (autoScaleAppliedCityCodeRef.current !== code) {
        const autoScale = getAutoScaleBySites(result?.sites || result?.newMarkers || [], lat, lng);
        if (typeof autoScale === 'number') {
@@ -60,6 +127,12 @@
      }
    }
  };
  useEffect(() => {
    return () => {
      clearMarkerCommitTimer();
    };
  }, []);
  return {
    longitude,
@@ -84,6 +157,8 @@
    shouldIgnoreRegionChange,
    isReturningToOrigin,
    resetTimer,
    commitMarkers,
    scheduleMarkerCommit,
    updateMapData,
  };
}
src/pages/robotaxi/home/hooks/use-nearby-vehicles.ts
@@ -32,11 +32,29 @@
  toDirection: number;
};
type VehicleAnimationState = {
  startedAt: number;
  durationMs: number;
  trackMap: Map<string, VehicleTrackItem>;
};
const POLL_INTERVAL_MS = 5000; // 车辆轮询周期(毫秒)
const SETTLE_DELAY_MS = 500; // 地图停止后的静止判定时间(毫秒)
const MOVE_THRESHOLD_METERS = 500; // 触发新搜索的位移阈值(米)
const ANIMATION_DURATION_MS = 2000; // 车辆位移动画时长(毫秒)
const VEHICLE_MARKER_ICON = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/marker-car.png'; // 车辆 icon 地址
/**
 * 识别当前是否为 Android 设备,避免原生地图对车辆旋转做额外处理。
 */
const detectIsAndroid = () => {
  try {
    const systemInfo = Taro.getSystemInfoSync();
    return systemInfo?.platform === 'android' || /android/i.test(systemInfo?.system || '');
  } catch (error) {
    return false;
  }
};
/**
 * 归一化角度到 [0, 360) 区间。
@@ -45,6 +63,17 @@
  const safe = Number(value);
  if (!Number.isFinite(safe)) return 0;
  return ((safe % 360) + 360) % 360;
};
/**
 * 统一将业务方向值转换为地图 marker 需要的展示角度。
 * Android 地图会在相同 rotate 值下比 iOS 顺时针多偏约 90 度,这里做平台补偿。
 */
const resolveMarkerRotate = (direction: number, isAndroid: boolean) => {
  const normalizedDirection = normalizeDirection(direction);
  return isAndroid
    ? normalizeDirection(normalizedDirection - 90)
    : normalizedDirection;
};
/**
@@ -60,7 +89,7 @@
/**
 * 将车辆位置数据映射为地图 marker。
 */
const buildVehicleMarkers = (vehicles: NearbyVehicleItem[]) => {
const buildVehicleMarkers = (vehicles: NearbyVehicleItem[], isAndroid: boolean) => {
  return vehicles.map((vehicle) => ({
    id: vehicle.markerId,
    markerType: 'vehicle',
@@ -69,7 +98,7 @@
    name: '',
    latitude: vehicle.latitude,
    longitude: vehicle.longitude,
    rotate: vehicle.direction,
    rotate: resolveMarkerRotate(vehicle.direction, isAndroid),
    width: 20,
    height: 40,
    zIndex: 6,
@@ -124,15 +153,18 @@
 * 附近车辆编排:处理停稳判定、500 米阈值、5 秒轮询与平滑动画。
 */
export default function useNearbyVehicles({ isOperatingArea }: Deps) {
  const [vehicleMarkers, setVehicleMarkers] = useState<any[]>([]); // 车辆 marker 列表
  const [vehicleMarkerVersion, setVehicleMarkerVersion] = useState(0); // 车辆 marker 结构版本号,仅在结构变化时触发重渲染
  const pollTimerRef = useRef<any>(null); // 轮询定时器句柄
  const settleTimerRef = useRef<any>(null); // 停稳判定定时器句柄
  const animationSyncTimerRef = useRef<any>(null); // 动画结束后同步 React marker 的定时器
  const activeAnimationRef = useRef<VehicleAnimationState | null>(null); // 当前进行中的车辆动画状态
  const isAndroidRef = useRef(detectIsAndroid()); // 当前设备是否为 Android,决定是否禁用原生随移动旋转
  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 vehicleMarkersRef = useRef<any[]>([]); // 首页传给地图的车辆 marker 快照
  const mapContextRef = useRef<any>(null); // 首页地图上下文,负责原生 marker 平移
  const fetchAndAnimateVehiclesRef = useRef<(center: NearbyCenter) => Promise<void>>(async () => {});
  const ensurePollingRef = useRef<() => void>(() => {});
@@ -180,12 +212,62 @@
  };
  /**
   * 替换车辆 marker 结构并通知 React 重新渲染地图。
   */
  const replaceVehicleMarkers = (nextMarkers: any[]) => {
    vehicleMarkersRef.current = nextMarkers;
    setVehicleMarkerVersion((prev) => prev + 1);
  };
  /**
   * 在不触发 React 重渲染的前提下,同步车辆 marker 的最终位置与朝向快照。
   */
  const syncVehicleMarkerSnapshot = (vehicles: NearbyVehicleItem[]) => {
    if (vehicleMarkersRef.current.length === 0) {
      return;
    }
    const vehicleMap = new Map<string, NearbyVehicleItem>(
      vehicles.map((vehicle) => [vehicle.id, vehicle])
    );
    vehicleMarkersRef.current = vehicleMarkersRef.current.map((marker) => {
      const vehicleId = String(marker?.vehicleId ?? marker?.id ?? '');
      const nextVehicle = vehicleMap.get(vehicleId);
      if (!nextVehicle) {
        return marker;
      }
      return {
        ...marker,
        latitude: nextVehicle.latitude,
        longitude: nextVehicle.longitude,
        rotate: resolveMarkerRotate(nextVehicle.direction, isAndroidRef.current)
      };
    });
  };
  /**
   * 读取车辆动画剩余时长,供其他模块避让 marker 整包提交。
   */
  const getVehicleAnimationRemainingMs = useCallback(() => {
    const animationState = activeAnimationRef.current;
    if (!animationState) {
      return 0;
    }
    const remainingMs = animationState.durationMs - (Date.now() - animationState.startedAt);
    if (remainingMs <= 0) {
      activeAnimationRef.current = null;
      return 0;
    }
    return remainingMs;
  }, []);
  /**
   * 清空车辆渲染状态。
   */
  const clearVehicles = () => {
    renderedVehicleMapRef.current = new Map();
    setVehicleMarkers([]);
    replaceVehicleMarkers([]);
    clearAnimationSyncTimer();
    activeAnimationRef.current = null;
  };
  /**
@@ -222,8 +304,11 @@
        },
        duration: ANIMATION_DURATION_MS,
        autoRotate: false,
        moveWithRotate: true,
        rotate: interpolateDirection(track.fromDirection, track.toDirection, 1)
        moveWithRotate: !isAndroidRef.current,
        rotate: resolveMarkerRotate(
          interpolateDirection(track.fromDirection, track.toDirection, 1),
          isAndroidRef.current
        )
      });
    });
  };
@@ -251,8 +336,9 @@
      );
      if (currentVehicleMap.size === 0 || collectionChanged) {
        clearAnimationSyncTimer();
        activeAnimationRef.current = null;
        renderedVehicleMapRef.current = nextVehicleMap;
        setVehicleMarkers(buildVehicleMarkers(normalizedVehicles));
        replaceVehicleMarkers(buildVehicleMarkers(normalizedVehicles, isAndroidRef.current));
        return;
      }
      const tracks: VehicleTrackItem[] = normalizedVehicles.map((vehicle) => {
@@ -269,13 +355,19 @@
        };
      });
      renderedVehicleMapRef.current = nextVehicleMap;
      activeAnimationRef.current = {
        startedAt: Date.now(),
        durationMs: ANIMATION_DURATION_MS,
        trackMap: new Map(tracks.map((track) => [track.id, track]))
      };
      translateVehicleMarkers(tracks);
      clearAnimationSyncTimer();
      animationSyncTimerRef.current = setTimeout(() => {
        if (seq !== latestSeqRef.current) {
          return;
        }
        setVehicleMarkers(buildVehicleMarkers(Array.from(renderedVehicleMapRef.current.values())));
        activeAnimationRef.current = null;
        syncVehicleMarkerSnapshot(Array.from(renderedVehicleMapRef.current.values()));
      }, ANIMATION_DURATION_MS);
    } catch (error) {
      // 请求异常时保持当前车辆状态,避免闪断。
@@ -372,10 +464,11 @@
  }, [disposeVehicles]);
  return {
    vehicleMarkers,
    vehicleMarkers: vehicleMarkerVersion >= 0 ? vehicleMarkersRef.current : [],
    handleRegionBegin,
    handleRegionSettled,
    tryStartPollingOnInit,
    disposeVehicles
    disposeVehicles,
    getVehicleAnimationRemainingMs
  };
}
src/pages/robotaxi/home/hooks/use-region-change.ts
@@ -9,6 +9,7 @@
  mapState: any;
  siteState: any;
  toOrigin: () => void;
  getMarkerCommitDelayMs?: () => number;
  onRegionBegin?: () => void;
  onRegionSettled?: (payload: { lat: number; lon: number; isInside: boolean }) => void;
};
@@ -16,7 +17,19 @@
/**
 * 地图 region-change 编排:处理地图拖拽/缩放后的吸附、回填和自动归位。
 */
export default function useRegionChange({ mapState, siteState, toOrigin, onRegionBegin, onRegionSettled }: Deps) {
export default function useRegionChange({ mapState, siteState, toOrigin, getMarkerCommitDelayMs, onRegionBegin, onRegionSettled }: Deps) {
  /**
   * 查询完成后提交站点 marker:命中吸附时立即提交,未吸附时避让车辆动画窗口再提交。
   */
  const commitQueriedMarkers = (newMarkers: any[], isInside: boolean, adsorbed: boolean) => {
    if (!isInside || adsorbed) {
      mapState.commitMarkers(newMarkers);
      return;
    }
    const delayMs = typeof getMarkerCommitDelayMs === 'function' ? getMarkerCommitDelayMs() : 0;
    mapState.scheduleMarkerCommit(newMarkers, delayMs);
  };
  /**
   * 将用户显式换点后的吸附结果同步为首页确认上车点。
   */
@@ -43,14 +56,16 @@
    siteState.setBounce(true);
    siteState.setShowToolTip(false);
    try {
      const { isInside, newMarkers, sites } = await mapState.updateMapData(lat, lng);
      const { isInside, newMarkers, sites } = await mapState.updateMapData(lat, lng, undefined, { commitMarkers: false });
      if (!isInside) {
        commitQueriedMarkers(newMarkers, false, false);
        settleRegionResult({ lat, lon: lng, isInside: false });
        siteState.setBounce(false);
        return;
      }
      const nearestCandidates = sites.length > 0 ? sites : newMarkers;
      const { adsorbed, marker } = siteState.tryAdsorb(lat, lng, nearestCandidates);
      commitQueriedMarkers(newMarkers, true, adsorbed);
      const finalLat = adsorbed && marker ? marker.latitude : lat; // 最终纬度
      const finalLng = adsorbed && marker ? marker.longitude : lng; // 最终经度
      settleRegionResult({ lat: finalLat, lon: finalLng, isInside: true });
@@ -92,14 +107,16 @@
          return;
        }
        try {
          const { isInside, newMarkers, sites } = await mapState.updateMapData(lat, lng);
          const { isInside, newMarkers, sites } = await mapState.updateMapData(lat, lng, undefined, { commitMarkers: false });
          if (!isInside) {
            commitQueriedMarkers(newMarkers, false, false);
            settleRegionResult({ lat, lon: lng, isInside: false });
            siteState.setBounce(false);
            return;
          }
          const nearestCandidates = sites.length > 0 ? sites : newMarkers;
          const { adsorbed, marker } = siteState.tryAdsorb(lat, lng, nearestCandidates);
          commitQueriedMarkers(newMarkers, true, adsorbed);
          const finalLat = adsorbed && marker ? marker.latitude : lat; // 最终纬度
          const finalLng = adsorbed && marker ? marker.longitude : lng; // 最终经度
          if (adsorbed && marker && (causedBy === 'gesture' || causedBy === 'drag' || wasReturningToOrigin)) {
src/pages/robotaxi/home/index.tsx
@@ -233,6 +233,7 @@
    vehicleMarkers,
    handleRegionBegin: handleVehicleRegionBegin,
    handleRegionSettled: handleVehicleRegionSettled,
    getVehicleAnimationRemainingMs,
    tryStartPollingOnInit,
    disposeVehicles
  } = useNearbyVehicles({ isOperatingArea });
@@ -243,6 +244,7 @@
    mapState,
    siteState,
    toOrigin,
    getMarkerCommitDelayMs: getVehicleAnimationRemainingMs,
    onRegionBegin: handleVehicleRegionBegin,
    onRegionSettled: (payload) => {
      handleVehicleRegionSettled(payload);
src/robotaxi/api/types/begin-route.ts
@@ -331,6 +331,8 @@
    selectedRouteId?: OrderId | '';
    /** 选中的路线规则。 */
    selectedRoutePlanRule?: NumericLike | '';
    /** 导航快照 ID。 */
    navigationSnapshotId?: OrderId | '';
}
/** 费用明细查询请求。 */
src/robotaxi/pages/begin-route/components/calling-car-modules/fare-detail-card.tsx
@@ -84,7 +84,7 @@
                                    fareBreakdown?.redPacketMoney + fareBreakdown?.couponMoney > 0 &&
                                    <View className="car-card-end-particulars-money-through">{formatMoney(fareBreakdown?.orderMoney)}元</View>
                                }
                                <Image style={{ transform: showDetail ? 'rotate(90deg)' : 'rotate(0deg)', transition: 'transform 0.3s' }} src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/ccc-right.png" />
                                <Image style={{ transform: showDetail ? 'rotate(90deg)' : 'rotate(0deg)', transition: 'transform 0.3s' }} src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/ccc-icon-right.png" />
                            </View>
                    }
                </View>
@@ -169,7 +169,7 @@
                    <View className="car-card-end-cost-money">
                        全程{formatTripDistance(fareBreakdown?.kilometer)}|{formatTripDurationFromSeconds(fareBreakdown?.time)}
                    </View>
                    <Image src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/ccc-right.png" />
                    <Image src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/ccc-icon-right.png" />
                </View>
                {
                    (fareBreakdown?.operateFree != 1 && [TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(tripStatus)) && Number(fareBreakdown?.redPacketMoney + fareBreakdown?.couponMoney) > 0 &&
@@ -178,7 +178,7 @@
                        <View className="car-card-end-through-content" onClick={onOpenCouponSelect}>
                            <View className="car-card-end-through-content-title">最佳优惠</View>
                            <View className="car-card-end-through-content-money">-{formatMoney(fareBreakdown?.redPacketMoney + fareBreakdown?.couponMoney)}元</View>
                            <Image src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/ccc-right.png" />
                            <Image src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/ccc-icon-right.png" />
                        </View>
                    </View>
                }
src/robotaxi/pages/begin-route/components/calling-car-modules/temperature-control.tsx
@@ -10,6 +10,13 @@
import { TripStatus } from '../../constants/trip-status';
import { MODIFY_DESTINATION_SKIP_AC_QUERY_ON_RETURN_KEY } from '../../constants/modify-destination';
const AC_SWITCH_OPEN_ICON = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/T-switch-open.png'; // 空调关闭态展示的开启动作图标
const AC_SWITCH_CLOSE_ICON = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/T-switch-close.png'; // 空调开启态展示的关闭动作图标
const AC_OPENING_TEXT = '开启中...'; // 开启空调时的 loading 文案
const AC_CLOSING_TEXT = '关闭中...'; // 关闭空调时的 loading 文案
const AC_QUERY_FAIL_TEXT = '空调状态查询失败,请稍后重试'; // 空调状态查询失败提示
const AC_TOGGLE_FAIL_TEXT = '空调开关操作失败,请重试'; // 空调开关操作失败提示
type Props = {
    tripStatus: TripStatus;
    orderId?: string;
@@ -31,6 +38,8 @@
    const TEMP_MAX = 28; // 空调最高温度
    const canToggleSwitch = tripStatus === TripStatus.VehicleApproaching && !isQueryingAc && !isTogglingAc; // 开关是否可点击
    const canAdjustTemperature = canToggleSwitch && acStatusLoaded && acSwitchOn && typeof tempStatus === 'number'; // 温度是否可调整
    const tempButtonIconSuffix = acSwitchOn ? '' : '-no'; // 空调关闭时展示置灰的温度调节图标
    const tempTextDisabledClassName = acSwitchOn ? '' : 'color-000-25'; // 空调关闭时温度和文案置灰
    useEffect(() => {
        const skipOnce = Number(Taro.getStorageSync(MODIFY_DESTINATION_SKIP_AC_QUERY_ON_RETURN_KEY)) === 1; // 修改目的地返回场景只跳过一次查询
        skipQueryOnThisCycleRef.current = skipOnce;
@@ -80,7 +89,7 @@
            }
            setAcSwitchOn(false);
            setAcStatusLoaded(false);
            Taro.showToast({ title: '空调状态查询失败,请稍后重试', icon: 'none' });
            Taro.showToast({ title: AC_QUERY_FAIL_TEXT, icon: 'none' });
        } finally {
            if (nextSeq === acQuerySeqRef.current) {
                setIsQueryingAc(false);
@@ -195,7 +204,7 @@
        });
    };
    // 处理空调开关点击:先调开关,再强制回查状态
    // 处理空调开关点击:以操作接口返回结果作为当前空调状态
    const handleToggleAcSwitch = async () => {
        if (!canToggleSwitch) {
            return;
@@ -204,16 +213,25 @@
        if (!targetOrderId) {
            return;
        }
        const targetOpenState = !acSwitchOn;
        setIsTogglingAc(true);
        Taro.showLoading({
            title: targetOpenState ? AC_OPENING_TEXT : AC_CLOSING_TEXT,
            mask: true
        });
        try {
            await airConditionerUnlock({
            const res = await airConditionerUnlock({
                orderId: targetOrderId,
                open: !acSwitchOn
                open: targetOpenState
            });
            await queryAcSwitchStatus(targetOrderId);
            const nextOpenState = parseAcSwitchState(typeof res === 'undefined' ? targetOpenState : res);
            setAcSwitchOn(nextOpenState);
            setAcStatusLoaded(true);
        } catch (error) {
            Taro.showToast({ title: '空调开关操作失败,请重试', icon: 'none' });
            Taro.hideLoading();
            Taro.showToast({ title: AC_TOGGLE_FAIL_TEXT, icon: 'none' });
        } finally {
            Taro.hideLoading();
            setIsTogglingAc(false);
        }
    };
@@ -229,30 +247,30 @@
                    <View className="car-card-temp-num-btn">
                        {
                            canAdjustTemperature ?
                                <Image className="car-card-temp-num-btn-img" src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/T-down.png" onClick={() => handleTempChange(-0.5)} />
                                <Image className="car-card-temp-num-btn-img" src={`https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/T-down${tempButtonIconSuffix}.png`} onClick={() => handleTempChange(-0.5)} />
                                :
                                <Image className="car-card-temp-num-btn-img" src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/T-down-no.png" />
                                <Image className="car-card-temp-num-btn-img" src={`https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/T-down${tempButtonIconSuffix}.png`} />
                        }
                    </View>
                    <View className="car-card-temp-num-text">
                        <View className="car-card-temp-num-text-temp color-000-25">{typeof tempStatus === 'number' ? `${tempStatus.toFixed(1)}℃` : '--'}</View>
                        <View className="car-card-temp-num-text-desc color-000-25">设定温度</View>
                        <View className={`car-card-temp-num-text-temp ${tempTextDisabledClassName}`.trim()}>{typeof tempStatus === 'number' ? `${tempStatus.toFixed(1)}℃` : '--'}</View>
                        <View className={`car-card-temp-num-text-desc ${tempTextDisabledClassName}`.trim()}>设定温度</View>
                    </View>
                    <View className="car-card-temp-num-btn">
                        {
                            canAdjustTemperature ?
                                <Image className="car-card-temp-num-btn-img" src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/T-up.png" onClick={() => handleTempChange(0.5)} />
                                <Image className="car-card-temp-num-btn-img" src={`https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/T-up${tempButtonIconSuffix}.png`} onClick={() => handleTempChange(0.5)} />
                                :
                                <Image className="car-card-temp-num-btn-img" src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/T-up-no.png" />
                                <Image className="car-card-temp-num-btn-img" src={`https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/T-up${tempButtonIconSuffix}.png`} />
                        }
                    </View>
                </View>
                <View className="car-card-temp-switch" onClick={handleToggleAcSwitch}>
                <View className={`car-card-temp-switch ${acSwitchOn ? 'car-card-temp-switch-open' : ''}`.trim()} onClick={handleToggleAcSwitch}>
                    {
                        acSwitchOn ?
                            <Image className="car-card-temp-switch-img" src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/T-switch-open.png" />
                            <Image className="car-card-temp-switch-img" src={AC_SWITCH_CLOSE_ICON} />
                            :
                            <Image className="car-card-temp-switch-img" src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/T-switch-close.png" />
                            <Image className="car-card-temp-switch-img" src={AC_SWITCH_OPEN_ICON} />
                    }
                    <View className="car-card-temp-switch-text">空调开关</View>
                </View>
src/robotaxi/pages/begin-route/hooks/use-map-markers.ts
@@ -5,6 +5,23 @@
const CHANGE_SITE_UP_ICON_URL = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/change-site-up.png'; // 行程中修改目的地时的上车点图标
const DYNAMIC_MARKER_POSITION_EPSILON = 0.000001; // 动态 marker 位置变化阈值,避免无意义重复刷新
/**
 * 生成 marker 签名,供平移态判断结构是否实际变化。
 */
const buildMarkersSignature = (markers: any[], vehicleMarkerId: number, shouldUsePureTranslateCarMarker: boolean) => markers
  .map((marker: any) => {
    const shouldIgnoreDynamicCarPosition = shouldUsePureTranslateCarMarker && marker.id === vehicleMarkerId;
    return [
      marker.id,
      shouldIgnoreDynamicCarPosition ? 'translate' : Number(marker.latitude).toFixed(6),
      shouldIgnoreDynamicCarPosition ? 'translate' : Number(marker.longitude).toFixed(6),
      marker.name || '',
      shouldIgnoreDynamicCarPosition ? 'translate' : (typeof marker.rotate === 'number' ? marker.rotate : ''),
      marker?.customCallout?.anchorY ?? '',
    ].join('|');
  })
  .join(';');
type Deps = {
  tripStatus: TripStatus;
  statusRule: any;
@@ -130,8 +147,8 @@
      latitude,
      longitude,
      iconPath: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/before-user-site.png',
      width: 22,
      height: 36,
      width: 18,
      height: 18,
      anchor: { x: 0.5, y: 1 },
      zIndex: 1,
      name: '上一单终点',
@@ -163,17 +180,18 @@
    if (shouldSkipUpdate) {
      // 平移态下保留已创建车辆 marker 的当前位置和角度,只同步其他 marker 结构变化
      setMarkers((prevMarkers: any[]) => {
        const prevMarkersSignature = buildMarkersSignature(prevMarkers, deps.vehicleMarkerId, shouldUsePureTranslateCarMarker);
        const targetCarIndex = prevMarkers.findIndex((item: any) => item.id === deps.vehicleMarkerId);
        const nextCarIndex = nextMarkers.findIndex((item: any) => item.id === deps.vehicleMarkerId);
        if (nextCarIndex === -1) {
          if (markersSignatureRef.current !== nextMarkersSignature) {
          if (prevMarkersSignature !== nextMarkersSignature) {
            markersSignatureRef.current = nextMarkersSignature;
            return nextMarkers;
          }
          return prevMarkers;
        }
        if (targetCarIndex === -1 || !shouldUsePureTranslateCarMarker) {
          if (markersSignatureRef.current !== nextMarkersSignature) {
          if (prevMarkersSignature !== nextMarkersSignature) {
            markersSignatureRef.current = nextMarkersSignature;
            return nextMarkers;
          }
@@ -189,7 +207,7 @@
            && Number(nextCar.rotate) !== Number(prevCar.rotate)
          )
        );
        if (markersSignatureRef.current === nextMarkersSignature && !shouldRefreshCarPosition) {
        if (prevMarkersSignature === nextMarkersSignature && !shouldRefreshCarPosition) {
          return prevMarkers;
        }
        markersSignatureRef.current = nextMarkersSignature;
@@ -235,19 +253,7 @@
    if (effectiveShowCar && carMarker) nextMarkers.push(carMarker);
    if (deps.statusRule.showDropoff && dropoffMarker) nextMarkers.push(dropoffMarker);
    if (beforeUserDropoffMarker) nextMarkers.push(beforeUserDropoffMarker);
    const nextMarkersSignature = nextMarkers
      .map((m: any) => {
        const shouldIgnoreDynamicCarPosition = shouldUsePureTranslateCarMarker && m.id === deps.vehicleMarkerId;
        return [
          m.id,
          shouldIgnoreDynamicCarPosition ? 'translate' : Number(m.latitude).toFixed(6),
          shouldIgnoreDynamicCarPosition ? 'translate' : Number(m.longitude).toFixed(6),
          m.name || '',
          shouldIgnoreDynamicCarPosition ? 'translate' : (typeof m.rotate === 'number' ? m.rotate : ''),
          m?.customCallout?.anchorY ?? '',
        ].join('|');
      })
      .join(';');
    const nextMarkersSignature = buildMarkersSignature(nextMarkers, deps.vehicleMarkerId, shouldUsePureTranslateCarMarker);
    const hasDropoffMarker = markersRef.current.some((m: any) => m.id === 2);
    const shouldHoldMarkerByTranslate = [TripStatus.VehicleApproaching, TripStatus.InService].includes(deps.tripStatus) && carMarkerInitialized.current && deps.statusRule.showCar && (!deps.statusRule.showDropoff || hasDropoffMarker);
    const shouldSkipUpdate = shouldHoldMarkerByTranslate && (deps.isAndroid || tripStatus6MarkerRefreshedRef.current);
src/robotaxi/pages/begin-route/hooks/use-modify-destination-flow.ts
@@ -238,7 +238,8 @@
        endPointId: previewEndSite.id || 0,
        orderId: Taro.getStorageSync('orderId') || '',
        selectedRouteId: selectedRoute?.route?.routeID || selectedRoute?.pathFingerprint || '',
        selectedRoutePlanRule: selectedRoute?.primaryRoutePlanRule || ''
        selectedRoutePlanRule: selectedRoute?.primaryRoutePlanRule || '',
        navigationSnapshotId: selectedRoute?.snapshotId || ''
      });
      // 清除本地修改状态,不再作为“本地覆盖”存在,因为订单已真正变更
src/robotaxi/pages/begin-route/hooks/use-page-actions.ts
@@ -5,6 +5,8 @@
  MODIFY_DESTINATION_CANDIDATE_KEY,
  MODIFY_DESTINATION_OVERRIDE_KEY,
  MODIFY_DESTINATION_ROUTE_POINTS_KEY,
  MODIFY_DESTINATION_SKIP_AC_QUERY_ON_RETURN_KEY,
  MODIFY_DESTINATION_SOCKET_STATUS_KEY,
  MODIFY_DESTINATION_SESSION_KEY
} from '../constants/modify-destination';
import { clearPassedRouteCache } from './passed-route-cache';
@@ -14,6 +16,7 @@
  startSite: any;
  getCheckedEnter: () => boolean;
  setCheckedEnter: (checked: boolean) => void;
  onBeforeBack?: () => void;
};
/**
@@ -28,6 +31,8 @@
    Taro.removeStorageSync(MODIFY_DESTINATION_CANDIDATE_KEY);
    Taro.removeStorageSync(MODIFY_DESTINATION_OVERRIDE_KEY);
    Taro.removeStorageSync(MODIFY_DESTINATION_ROUTE_POINTS_KEY);
    Taro.removeStorageSync(MODIFY_DESTINATION_SKIP_AC_QUERY_ON_RETURN_KEY);
    Taro.removeStorageSync(MODIFY_DESTINATION_SOCKET_STATUS_KEY);
  };
  /**
@@ -70,6 +75,7 @@
   * 返回首页并清理必要缓存。
   */
  const toBack = () => {
    deps.onBeforeBack?.();
    clearModifyDestinationStorage();
    clearPassedRouteCache();
    if (deps.tripStatus === TripStatus.PaymentSuccess || deps.tripStatus === TripStatus.PublishRoute) {
src/robotaxi/pages/begin-route/hooks/use-route-request.ts
@@ -503,8 +503,13 @@
    requestHandler({ orderId: targetOrderId, routeType })
      .then((response: any) => {
        const beforeUserSite = parseBeforeUserEndSite(response?.endLongitude, response?.endlatitude);
        deps.setBeforeUserEndSite?.(beforeUserSite);
        const beforeUserSite = parseBeforeUserEndSite(
          response?.endLongitude,
          response?.endLatitude ?? response?.endlatitude
        );
        if (beforeUserSite) {
          deps.setBeforeUserEndSite?.(beforeUserSite);
        }
        const rawPoints = Array.isArray(response?.polyline) ? response.polyline : [];
        const points = rawPoints
src/robotaxi/pages/begin-route/hooks/use-vehicle-tracking.ts
@@ -4,11 +4,13 @@
type Deps = {
  statusRule: any;
  vehicleMarkerId: number;
  isAndroid: boolean;
};
export default function useVehicleTracking({
  statusRule,
  vehicleMarkerId
  vehicleMarkerId,
  isAndroid
}: Deps) {
  // 车辆当前位置
  const [vehicleLocation, setVehicleLocation] = useState<{ latitude: number; longitude: number } | null>(null);
@@ -56,7 +58,7 @@
          longitude: task.longitude,
        },
        autoRotate: false,
        moveWithRotate: true,
        moveWithRotate: !isAndroid,
        rotate: task.rotate,
        duration: TRANSLATE_DURATION_MS,
        fail: () => {
src/robotaxi/pages/begin-route/hooks/use-web-socket-events.ts
@@ -62,10 +62,21 @@
const resolveOrderId = (payload: any, fallbackOrderId: string) =>
    payload?.content?.orderId || payload?.orderId || fallbackOrderId || Taro.getStorageSync('orderId');
// 解析“上一单乘客下车点”坐标,任一坐标无效则返回空,供 marker 显隐控制。
// 解析“上一单乘客下车点”坐标,兼容顶层字段、content 字段与历史大小写差异。
const parseBeforeUserEndSite = (payload: any) => {
    const longitude = Number(payload?.content?.endLongitude);
    const latitude = Number(payload?.content?.endlatitude);
    const longitude = Number(
        payload?.endLongitude
        ?? payload?.data?.endLongitude
        ?? payload?.content?.endLongitude
    );
    const latitude = Number(
        payload?.endLatitude
        ?? payload?.endlatitude
        ?? payload?.data?.endLatitude
        ?? payload?.data?.endlatitude
        ?? payload?.content?.endLatitude
        ?? payload?.content?.endlatitude
    );
    if (!Number.isFinite(longitude) || !Number.isFinite(latitude)) {
        return null;
    }
@@ -190,7 +201,6 @@
    if (!isSameOrder(incomingOrderId, currentOrderId)) {
        return;
    }
    deps.setBeforeUserEndSite(parseBeforeUserEndSite(payload));
    const mappedTripStatus = syncTripStatus(payload, deps);
    const effectiveOrderId = resolveOrderId(payload, currentOrderId);
@@ -253,7 +263,10 @@
};
const handleOrderRoutePayload = (payload: any, deps: UseWebSocketEventsDeps) => {
    deps.setBeforeUserEndSite(parseBeforeUserEndSite(payload));
    const beforeUserSite = parseBeforeUserEndSite(payload);
    if (beforeUserSite) {
        deps.setBeforeUserEndSite(beforeUserSite);
    }
    const currentTripStatus = deps.tripStatusRef.current;
    // socket 路线拦截只跟随真实的“修改目的地预览态”,不要被本地终点覆盖态误伤。
    const isModifyDestinationPreviewActive = Boolean(deps.isModifyDestinationReviewVisible);
src/robotaxi/pages/begin-route/index.tsx
@@ -149,6 +149,7 @@
  const [modifyDestinationSocketStatus, setModifyDestinationSocketStatus] = useState<number | null>(null); // 后端推送的修改目的地展示状态
  const syncSmoothRouteBaselineRef = useRef<((snapshot: any) => void) | null>(null); // 订单路线接口回显后,同步初始化平滑擦线基线
  const lastSocketDirectionAtRef = useRef(0); // 最近一次由 socket 写入车辆朝向的时间戳
  const beforeBackRef = useRef<() => void>(() => { }); // 左上角返回前的兜底清理,避免预览路线残留到下次进页
  const tripStatusRef = useRef(tripStatus); // 行程状态引用
  const lastTripStatusRef = useRef(tripStatus); // 上一次行程状态,用于检测状态变化
@@ -227,6 +228,9 @@
    getCheckedEnter: () => isCheckedEnter.current,
    setCheckedEnter: (checked) => {
      isCheckedEnter.current = checked;
    },
    onBeforeBack: () => {
      beforeBackRef.current();
    }
  }); // 页面动作编排
  const {
@@ -310,7 +314,8 @@
  const showTrip1StyleEndpoint = isModifyDestinationReviewVisible || hasLocalDestinationOverride;
  const vehicleState = useVehicleTracking({
    statusRule,
    vehicleMarkerId
    vehicleMarkerId,
    isAndroid
  }); // 车辆追踪状态容器
  const {
    vehicleLocation,
@@ -376,12 +381,30 @@
    lastPolylineUpdateAtRef,
    currentFullRouteRef,
    lastErasedIndexRef,
    resetRouteRuntimeState,
    applyRoutePoints: routeApplyRoutePoints,
    erasePassedRouteSegments: routeErasePassedRouteSegments,
    requestRouteAndFare: routeRequestRouteAndFare,
    requestOrderRoute: routeRequestOrderRoute,
    updateWalkingRouteForStart: routeUpdateWalkingRouteForStart
  } = routeState; // 路线状态解构
  beforeBackRef.current = () => {
    if (isModifyDestinationReviewVisible || hasLocalDestinationOverride || previousEndSite) {
      exitModifyDestinationReview();
    } else {
      clearLocalDestinationOverride();
    }
    setIsRouteSelectExpanded(false);
    setRouteOptions([]);
    setFareEstimate({});
    setShowModifyDestinationFailPopup(false);
    setModifyDestinationFailMessage('');
    setRouteDistance(null);
    setRouteDuration(null);
    setWalkingPoints([]);
    setWalkingTime(0);
    resetRouteRuntimeState();
  };
  const displayPolyline = polyline;
  const routeSummaryMarker = useMemo(() => {
    if (!allowSelectableRoutes || !routeDistance || routeDuration === null || !Array.isArray(routePoints) || routePoints.length === 0) {
@@ -496,6 +519,10 @@
    if (!routeSummaryMarker) return markers;
    return [...markers, routeSummaryMarker];
  }, [markers, routeSummaryMarker]);
  const hasValidBeforeUserEndSite = useMemo(() => (
    Number.isFinite(Number(beforeUserEndSite?.latitude))
    && Number.isFinite(Number(beforeUserEndSite?.longitude))
  ), [beforeUserEndSite?.latitude, beforeUserEndSite?.longitude]);
  const { distanceConfig, ensureDistanceConfig, checkDistanceAndCityReady } = useOrderDistanceConfig({
    endSite,
    setTipType,
@@ -1050,7 +1077,7 @@
    arrivedTooltipReady,
    routeDistance,
    routeDuration,
    hasBeforeUserEndSite: !!beforeUserEndSite,
    hasBeforeUserEndSite: hasValidBeforeUserEndSite,
    customCalloutIconSrc,
    routeSummaryBgSrc,
    showTrip1StyleEndpoint,
src/robotaxi/pages/personal-center/index.tsx
@@ -23,8 +23,8 @@
const tripMenuList: TripMenuItem[] = [
  { key: "pendingPay", label: "待支付", icon: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/mine-pay.png' },
  { key: "completed", label: "已完成", icon: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/finished.png' },
  { key: "cancelled", label: "已取消", icon: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/cancelled.png' },
  { key: "completed", label: "已完成", icon: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/mine-finished.png' },
  { key: "cancelled", label: "已取消", icon: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/mine-cancelled.png' },
  { key: "all", label: "全部订单", icon: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/mine-all.png' }
];