zjk
2026-05-20 a8561e5b7a264e1a92a38c4539b417a3e5b86c3c
Merge branch 'feature_v1.0' of http://gitlab.xiaopeng.local:18080/robotaxi1/xp-robotaxi-miniprogram into feature_v1.0
2个文件已添加
26个文件已修改
1111 ■■■■■ 已修改文件
src/pages/robotaxi/home/components/home-map-panel.tsx 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/hooks/use-home-bootstrap.ts 86 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/hooks/use-location-flow.ts 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/hooks/use-navigation.ts 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/hooks/use-nearby-vehicles.ts 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/route-selection-panel.tsx 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/passed-route-cache.ts 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/runtime-sync.ts 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-active-trip-scale.ts 120 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-android-overlays.ts 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-bluetooth-access.ts 121 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-map-markers.ts 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-map-viewport.ts 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-modify-destination-flow.ts 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-order-submit.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-page-actions.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-page-restore.ts 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-polyline-render.ts 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-preloaded-map-assets.ts 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-route-manager.shared.ts 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-route-manager.ts 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-route-request.ts 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-trip-runtime.ts 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx 119 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-web-socket-events.ts 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/index.less 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/index.tsx 122 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/personal-center/index.tsx 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/components/home-map-panel.tsx
@@ -98,7 +98,7 @@
    if (props.isLocationAuthDenied) {
      return [];
    }
    return props.mergedMapMarkers.map((item) => ({ ...item, title: '', name: '' }));
    return props.mergedMapMarkers;
  }, [props.isLocationAuthDenied, props.mergedMapMarkers]);
  return (
src/pages/robotaxi/home/hooks/use-home-bootstrap.ts
@@ -1,7 +1,7 @@
import Taro from '@tarojs/taro';
import wsManager from '../../../../utils/websocket';
import { mapOrderStatusToTripStatus } from '../../../../utils/Robotaxi-public-method';
import { TripStatus, toTripStatus } from '../../../../robotaxi/pages/begin-route/constants/trip-status';
import { SocketOrderStatus, TripStatus, toTripStatus } from '../../../../robotaxi/pages/begin-route/constants/trip-status';
import {
  clearHomeStartSiteDisplayState,
  HOME_START_SITE_SOURCE_ACTIVE_ORDER,
@@ -21,6 +21,17 @@
  hasActiveOrder: boolean;
  /** 当前首页确认上车点。 */
  preferredStartSite: any;
  /** 当前是否应按用户位置吸附最近站点。 */
  shouldUseNearestSiteByOrder: boolean;
};
type GetLocationInfoOptions = {
  /** 是否更新地图中心。 */
  updateCenter?: boolean;
  /** 是否强制刷新城市信息。 */
  forceCityRefresh?: boolean;
  /** 是否强制按用户位置吸附最近站点。 */
  forceNearestSite?: boolean;
};
type Deps = {
@@ -61,7 +72,7 @@
  /** 设置经度 */
  setLongitude: (value: number) => void;
  /** 读取定位并更新站点 */
  getLocationInfo: (updateCenter?: boolean, forceCityRefresh?: boolean) => void;
  getLocationInfo: (options?: GetLocationInfoOptions) => void;
  /** 处理切城标记 */
  consumeCitySwitchFlag: () => { switched: boolean; shouldMoveToCityCenter: boolean };
  /** 跳过下一次居中 */
@@ -100,6 +111,13 @@
  };
  /**
   * 判断当前活跃订单是否应按用户位置吸附最近站点。
   */
  const shouldUseNearestSiteByActiveOrderStatus = (status: unknown) => (
    Number(status) >= SocketOrderStatus.PendingDeparture
  );
  /**
   * 将活跃订单起点映射为首页缓存站点。
   */
  const buildStartSiteByActiveOrder = (activeOrder: any) => ({
@@ -131,7 +149,12 @@
    deps.setNeedLogin(!otp);
    if (!otp) {
      deps.setPreferredStartSite(null);
      return { hasLogin: false, hasActiveOrder: false, preferredStartSite: null } as AuthOrderSyncResult;
      return {
        hasLogin: false,
        hasActiveOrder: false,
        preferredStartSite: null,
        shouldUseNearestSiteByOrder: false
      } as AuthOrderSyncResult;
    }
    wsManager.connect(deps.wsHost);
    const agreement = await deps.getAgreement();
@@ -162,26 +185,51 @@
      deps.setTripStatus(0);
      deps.setOrderData({});
      deps.setPreferredStartSite(preferredStartSite);
      return { hasLogin: true, hasActiveOrder: false, preferredStartSite } as AuthOrderSyncResult;
      return {
        hasLogin: true,
        hasActiveOrder: false,
        preferredStartSite,
        shouldUseNearestSiteByOrder: false
      } as AuthOrderSyncResult;
    }
    deps.setOrderData(activeOrder || {});
    const mappedStatus = toHomeTripStatus(mapOrderStatusToTripStatus(activeOrder.status || 0));
    const startSite = buildStartSiteByActiveOrder(activeOrder);
    const endSite = buildEndSiteByActiveOrder(activeOrder);
    const preferredState = readHomeStartSiteDisplayState();
    const shouldUseNearestSiteByOrder = shouldUseNearestSiteByActiveOrderStatus(activeOrder.status);
    const manualSelectedStartSite = isManualSelectedHomeStartSiteDisplayState(preferredState)
      ? resolveHomeStartSiteDisplaySite()
      : null; // 手动选择的上车点优先用于首页回显,避免返回首页时被订单起点覆盖
    const preferredStartSite = shouldUseNearestSiteByOrder ? null : (manualSelectedStartSite || startSite);
    deps.setTripStatus(mappedStatus);
    Taro.setStorageSync('tripStatus', mappedStatus);
    Taro.setStorageSync('orderId', activeOrder.id || '');
    Taro.setStorageSync('startSite', startSite);
    Taro.setStorageSync('endSite', endSite);
    writeHomeStartSiteDisplayState(startSite, HOME_START_SITE_SOURCE_ACTIVE_ORDER);
    deps.setPreferredStartSite(startSite);
    return { hasLogin: true, hasActiveOrder: true, preferredStartSite: startSite } as AuthOrderSyncResult;
    if (shouldUseNearestSiteByOrder) {
      Taro.removeStorageSync('startSite');
    } else {
      Taro.setStorageSync('startSite', preferredStartSite);
    }
    if (shouldUseNearestSiteByOrder && preferredState?.source === HOME_START_SITE_SOURCE_ACTIVE_ORDER) {
      clearHomeStartSiteDisplayState();
    }
    if (!shouldUseNearestSiteByOrder && !manualSelectedStartSite) {
      writeHomeStartSiteDisplayState(startSite, HOME_START_SITE_SOURCE_ACTIVE_ORDER);
    }
    deps.setPreferredStartSite(preferredStartSite);
    return {
      hasLogin: true,
      hasActiveOrder: true,
      preferredStartSite,
      shouldUseNearestSiteByOrder
    } as AuthOrderSyncResult;
  };
  /**
   * 处理定位与首屏地图状态。
   */
  const processLocationSetting = async (auth: any, startSite: any) => {
  const processLocationSetting = async (auth: any, startSite: any, shouldUseNearestSiteByOrder = false) => {
    const hasStartSite = startSite && startSite.latitude && startSite.longitude;
    deps.setIsLocationAuthDenied(auth === false);
    const switchState = deps.consumeCitySwitchFlag();
@@ -238,6 +286,14 @@
      deps.setLongitude(deps.defaultLongitude);
      return;
    }
    if (shouldUseNearestSiteByOrder) {
      deps.getLocationInfo({
        updateCenter: true,
        forceCityRefresh: true,
        forceNearestSite: true
      });
      return;
    }
    if (auth === undefined) {
      deps.getLocationInfo();
      return;
@@ -246,12 +302,12 @@
    deps.skipNextLocationCenterRef.current = false;
    const hasEntered = Taro.getStorageSync(deps.firstEnterKey) === 'true';
    if (!hasEntered) {
      deps.getLocationInfo(true);
      deps.getLocationInfo({ updateCenter: true });
      Taro.setStorageSync(deps.firstEnterKey, 'true');
      return;
    }
    deps.getLocationInfo(shouldUpdateCenter);
    if (!hasStartSite && auth === true) {
    deps.getLocationInfo({ updateCenter: shouldUpdateCenter });
    if (!hasStartSite && auth === true && !shouldUseNearestSiteByOrder) {
      await deps.toOrigin();
    }
  };
@@ -276,10 +332,8 @@
        const auth = res.authSetting['scope.userLocation'];
        Promise.resolve(authSyncTask)
          .then((syncResult) => {
            const startSite = syncResult.hasActiveOrder
              ? Taro.getStorageSync('startSite')
              : syncResult.preferredStartSite;
            processLocationSetting(auth, startSite);
            const startSite = syncResult.preferredStartSite || Taro.getStorageSync('startSite');
            processLocationSetting(auth, startSite, syncResult.shouldUseNearestSiteByOrder);
          })
          .catch(() => {
            processLocationSetting(auth, null);
src/pages/robotaxi/home/hooks/use-location-flow.ts
@@ -20,6 +20,15 @@
  onLocationSettled: (lat: number, lng: number, isInside: boolean) => void;
};
type GetLocationInfoOptions = {
  /** 是否更新地图中心。 */
  updateCenter?: boolean;
  /** 是否强制刷新城市信息。 */
  forceCityRefresh?: boolean;
  /** 是否强制按用户位置吸附最近站点。 */
  forceNearestSite?: boolean;
};
/**
 * 定位流程编排:不持有状态,仅组合 map/site/city 状态 hook 完成定位主链路。
 */
@@ -109,7 +118,12 @@
  /**
   * 定位入口:定位 -> 城市就绪 -> 地图数据刷新 -> 吸附判断 -> 地址兜底。
   */
  const getLocationInfo = (updateCenter = true, forceCityRefresh = false) => {
  const getLocationInfo = (options: GetLocationInfoOptions = {}) => {
    const {
      updateCenter = true,
      forceCityRefresh = false,
      forceNearestSite = false
    } = options;
    Taro.getLocation({ type: 'gcj02' })
      .then(async (res) => {
        mapState.setIsLocationAuthDenied(false);
@@ -142,7 +156,7 @@
          );
          if (cityInfo?.code) {
            const { isInside, newMarkers, sites } = await mapState.updateMapData(res.latitude, res.longitude, cityInfo.code);
            const lockedManualStartSite = resolveLockedManualStartSite(cityInfo.code);
            const lockedManualStartSite = forceNearestSite ? null : resolveLockedManualStartSite(cityInfo.code);
            const nearestCandidates = sites.length > 0 ? sites : newMarkers;
            const { adsorbed, marker } = lockedManualStartSite
              ? { adsorbed: false, marker: null }
src/pages/robotaxi/home/hooks/use-navigation.ts
@@ -39,7 +39,14 @@
  /** 设置地图缩放 */
  setMapScale: (v: number) => void;
  /** 主动定位 */
  getLocationInfo: (updateCenter?: boolean, forceCityRefresh?: boolean) => void;
  getLocationInfo: (options?: {
    /** 是否更新地图中心。 */
    updateCenter?: boolean;
    /** 是否强制刷新城市信息。 */
    forceCityRefresh?: boolean;
    /** 是否强制按用户位置吸附最近站点。 */
    forceNearestSite?: boolean;
  }) => void;
  /** 查询指针吸附 */
  queryPointerByCenter: (lat: number, lng: number) => Promise<void>;
};
@@ -95,7 +102,7 @@
        if (res.authSetting['scope.userLocation']) {
          deps.setIsLocationAuthDenied(false);
          deps.setMapScale(14);
          deps.getLocationInfo(true, true);
          deps.getLocationInfo({ updateCenter: true, forceCityRefresh: true });
        }
      }
    });
src/pages/robotaxi/home/hooks/use-nearby-vehicles.ts
@@ -15,6 +15,7 @@
type NearbyVehicleItem = {
  id: string;
  markerId: number;
  latitude: number;
  longitude: number;
  direction: number;
@@ -22,6 +23,7 @@
type VehicleTrackItem = {
  id: string;
  markerId: number;
  fromLat: number;
  fromLon: number;
  toLat: number;
@@ -33,8 +35,7 @@
const POLL_INTERVAL_MS = 5000; // 车辆轮询周期(毫秒)
const SETTLE_DELAY_MS = 500; // 地图停止后的静止判定时间(毫秒)
const MOVE_THRESHOLD_METERS = 500; // 触发新搜索的位移阈值(米)
const ANIMATION_DURATION_MS = 4500; // 车辆位移动画时长(毫秒)
const VEHICLE_MARKER_ID_BASE = 900000000; // 车辆 marker id 起始段
const ANIMATION_DURATION_MS = 2000; // 车辆位移动画时长(毫秒)
const VEHICLE_MARKER_ICON = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/marker-car.png'; // 车辆 icon 地址
/**
@@ -57,25 +58,15 @@
};
/**
 * 将任意字符串稳定映射到正整数,避免与站点 marker id 冲突。
 */
const hashToMarkerId = (rawId: string) => {
  let hash = 0;
  for (let i = 0; i < rawId.length; i += 1) {
    hash = (hash << 5) - hash + rawId.charCodeAt(i);
    hash |= 0;
  }
  return VEHICLE_MARKER_ID_BASE + (Math.abs(hash) % 9999999);
};
/**
 * 将车辆位置数据映射为地图 marker。
 */
const buildVehicleMarkers = (vehicles: NearbyVehicleItem[]) => {
  return vehicles.map((vehicle) => ({
    id: hashToMarkerId(vehicle.id),
    id: vehicle.markerId,
    markerType: 'vehicle',
    vehicleId: vehicle.id,
    title: '',
    name: '',
    latitude: vehicle.latitude,
    longitude: vehicle.longitude,
    rotate: vehicle.direction,
@@ -112,13 +103,15 @@
        ? response.vehicles
        : [];
  return rawList
    .map((item: any, index: number) => {
    .map((item: any) => {
      const latitude = Number(item?.latitude);
      const longitude = Number(item?.longitude);
      if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) return null;
      const rawId = item?.id ?? `vehicle-${index}-${latitude}-${longitude}`;
      const rawId = item?.id;
      const markerId = Number(rawId);
      if (!Number.isFinite(latitude) || !Number.isFinite(longitude) || rawId === undefined || rawId === null || String(rawId) === '' || !Number.isFinite(markerId)) return null;
      return {
        id: String(rawId),
        markerId,
        latitude,
        longitude,
        direction: normalizeDirection(Number(item?.direction))
@@ -157,16 +150,6 @@
  };
  /**
   * 清理动画同步定时器。
   */
  const clearAnimationSyncTimer = () => {
    if (animationSyncTimerRef.current) {
      clearTimeout(animationSyncTimerRef.current);
      animationSyncTimerRef.current = null;
    }
  };
  /**
   * 清理轮询定时器。
   */
  const clearPollTimer = () => {
@@ -183,6 +166,16 @@
    if (settleTimerRef.current) {
      clearTimeout(settleTimerRef.current);
      settleTimerRef.current = null;
    }
  };
  /**
   * 清理动画结束后的 React 坐标同步定时器。
   */
  const clearAnimationSyncTimer = () => {
    if (animationSyncTimerRef.current) {
      clearTimeout(animationSyncTimerRef.current);
      animationSyncTimerRef.current = null;
    }
  };
@@ -221,15 +214,15 @@
  const translateVehicleMarkers = (tracks: VehicleTrackItem[]) => {
    const mapContext = getMapContext();
    tracks.forEach((track) => {
      const markerId = hashToMarkerId(track.id);
      (mapContext as any).translateMarker?.({
        markerId,
        markerId: track.markerId,
        destination: {
          latitude: track.toLat,
          longitude: track.toLon
        },
        duration: ANIMATION_DURATION_MS,
        autoRotate: false,
        moveWithRotate: true,
        rotate: interpolateDirection(track.fromDirection, track.toDirection, 1)
      });
    });
@@ -266,6 +259,7 @@
        const currentVehicle = currentVehicleMap.get(vehicle.id);
        return {
          id: vehicle.id,
          markerId: vehicle.markerId,
          fromLat: currentVehicle?.latitude ?? vehicle.latitude,
          fromLon: currentVehicle?.longitude ?? vehicle.longitude,
          toLat: vehicle.latitude,
@@ -281,7 +275,7 @@
        if (seq !== latestSeqRef.current) {
          return;
        }
        setVehicleMarkers(buildVehicleMarkers(Array.from(nextVehicleMap.values())));
        setVehicleMarkers(buildVehicleMarkers(Array.from(renderedVehicleMapRef.current.values())));
      }, ANIMATION_DURATION_MS);
    } catch (error) {
      // 请求异常时保持当前车辆状态,避免闪断。
src/robotaxi/pages/begin-route/components/route-selection-panel.tsx
@@ -5,7 +5,7 @@
import trafficSignalIcon from '../../../assets/traffic-signal.png';
import RoutePreferencePopup, { RoutePreferenceOption } from './route-preference-popup';
import { queryRoutePreferences, saveRoutePreference } from '../../../api/begin-route';
import { formatDurationDisplay, formatTripDistance } from '../../../../utils/Robotaxi-public-method';
import { formatRemainingDurationDisplay, normalizeDistanceKm } from '../../../utils/format';
import './css-folder/route-selection-panel.less';
type Props = {
@@ -129,8 +129,9 @@
      <View className='route-select-content'>
        {routeOptions.map((item, index) => {
          const routeData = item.route || {};
          const time = formatDurationDisplay(routeData.duration || 0);
          const dist = formatTripDistance(routeData.distance || 0);
          const time = formatRemainingDurationDisplay(routeData.duration || 0);
          const distance = normalizeDistanceKm(routeData.distance || 0);
          const dist = distance ? `${distance.value}${distance.unit === 'm' ? '米' : '公里'}` : '--';
          const isSelected = index === selectedRouteIndex;
          return (
src/robotaxi/pages/begin-route/hooks/passed-route-cache.ts
New file
@@ -0,0 +1,65 @@
import Taro from '@tarojs/taro';
/** 已走过路线缓存键。 */
export const PASSED_ROUTE_CACHE_KEY = 'beginRoutePassedRouteCache';
/** 已走过路线缓存结构。 */
export type PassedRouteCacheState = {
  /** 订单 ID。 */
  orderId: string;
  /** 已走过路线点。 */
  points: Array<{ latitude: number; longitude: number }>;
  /** 最近更新时间。 */
  updatedAt: number;
};
/**
 * 归一化已走过路线点,仅保留有效经纬度。
 */
const normalizePassedRoutePoints = (points: any[] = []) => {
  return points
    .map((point) => ({
      latitude: Number(point?.latitude),
      longitude: Number(point?.longitude)
    }))
    .filter((point) => Number.isFinite(point.latitude) && Number.isFinite(point.longitude));
};
/**
 * 清理已走过路线缓存。
 */
export const clearPassedRouteCache = () => {
  Taro.removeStorageSync(PASSED_ROUTE_CACHE_KEY);
};
/**
 * 写入已走过路线缓存。
 */
export const writePassedRouteCache = (orderId: string, points: any[]) => {
  const normalizedOrderId = String(orderId || '');
  const normalizedPoints = normalizePassedRoutePoints(points);
  if (!normalizedOrderId || normalizedPoints.length <= 1) {
    return;
  }
  const nextState: PassedRouteCacheState = {
    orderId: normalizedOrderId,
    points: normalizedPoints,
    updatedAt: Date.now()
  };
  Taro.setStorageSync(PASSED_ROUTE_CACHE_KEY, nextState);
};
/**
 * 读取当前订单对应的已走过路线缓存。
 */
export const readPassedRouteCache = (orderId?: string) => {
  const cachedState = Taro.getStorageSync(PASSED_ROUTE_CACHE_KEY) as PassedRouteCacheState | null;
  if (!cachedState || !Array.isArray(cachedState.points)) {
    return [];
  }
  const normalizedOrderId = String(orderId || '');
  if (normalizedOrderId && String(cachedState.orderId || '') !== normalizedOrderId) {
    return [];
  }
  return normalizePassedRoutePoints(cachedState.points);
};
src/robotaxi/pages/begin-route/hooks/runtime-sync.ts
@@ -72,13 +72,13 @@
export function normalizeSocketRouteSnapshot(payload: any): RouteSnapshot {
  const route = safeParse(payload?.content?.route, {});
  const timestamp = Number(payload?.data?.timestamp);
  const speedMps = Number(route?.speed);
  const speedKmh = Number(route?.speed);
  return {
    points: normalizeRoutePoints(payload?.points),
    distance: normalizeDistanceKm(Number(route?.distance)),
    duration: normalizeDurationSeconds(Number(route?.duration)),
    direction: normalizeDirection(payload?.content?.direction),
    speedKmh: Number.isFinite(speedMps) ? speedMps * 3.6 : null,
    speedKmh: Number.isFinite(speedKmh) ? speedKmh : null,
    timestamp: Number.isFinite(timestamp) ? timestamp : null,
    vehicleMoveAlongPoints: normalizeRoutePoints(payload?.servedPoints),
    forceFullRedraw: payload?.forceFullRedraw === true,
src/robotaxi/pages/begin-route/hooks/use-active-trip-scale.ts
@@ -9,10 +9,10 @@
type Deps = {
  tripStatus: TripStatus;
  routePoints: RoutePoint[];
  speedKmh: number | null;
  mapScale: number;
  setMapScale: (value: number) => void;
  syncActiveTripViewportToVehicle: () => void;
  computeBottomPadding: (points: any[], viewportType: string) => number;
  resolveSafeTopInset: () => number;
  holdDynamicScale: (durationMs?: number) => void;
@@ -24,15 +24,30 @@
const USER_GESTURE_HOLD_MS = 3000; // 用户手势后暂停速度缩放时长
const ORDER_UPDATE_LINE_HOLD_MS = 2500; // ORDER_UPDATE_LINE 全路线入屏后暂停速度缩放时长
const SCALE_RESUME_BUFFER_MS = 80; // 冷却窗口结束后的恢复缓冲时间
const LOW_SPEED_KMH = 0; // 低速缩放区间起点
const HIGH_SPEED_KMH = 60; // 高速缩放区间终点
const MIN_DYNAMIC_SCALE = 14; // 高速时允许的最远视角比例尺
const MAX_DYNAMIC_SCALE = 18; // 低速时允许的最近视角比例尺
const SCALE_STEP = 0.5; // 比例尺离散步进,避免频繁抖动
const MIN_SCALE_CHANGE = 0.25; // 触发 setMapScale 的最小比例尺变化
const SPEED_SMOOTHING_SAMPLE_SIZE = 3; // 速度平滑采样数量
const SCALE_BAND_HYSTERESIS_KMH = 4; // 档位切换滞回,避免边界抖动
const MAX_SCALE_STEP_PER_TICK = 1; // 单次缩放最大变化,避免突然大幅跳变
const ORDER_UPDATE_LINE_SIDE_PADDING = 100; // ORDER_UPDATE_LINE 路线入屏左右留白
const ORDER_UPDATE_LINE_TOP_PADDING = 24; // ORDER_UPDATE_LINE 路线入屏最小顶部留白
const ORDER_UPDATE_LINE_TOP_EXTRA_PADDING = 32; // ORDER_UPDATE_LINE 顶部安全区附加留白
type ScaleBand = {
  minSpeedKmh: number;
  maxSpeedKmh: number;
  scale: number;
};
const DYNAMIC_SCALE_BANDS: ScaleBand[] = [
  { minSpeedKmh: 0, maxSpeedKmh: 10, scale: 18 },
  { minSpeedKmh: 10, maxSpeedKmh: 20, scale: 17.5 },
  { minSpeedKmh: 20, maxSpeedKmh: 30, scale: 17 },
  { minSpeedKmh: 30, maxSpeedKmh: 40, scale: 16 },
  { minSpeedKmh: 40, maxSpeedKmh: 50, scale: 15.5 },
  { minSpeedKmh: 50, maxSpeedKmh: 60, scale: 15 },
  { minSpeedKmh: 60, maxSpeedKmh: Number.POSITIVE_INFINITY, scale: 14 }
]; // 基于车速的动态缩放档位,覆盖低速精细、中速平衡、高速宏观三类视角
/**
 * 将数值限制在给定区间内。
@@ -44,13 +59,77 @@
};
/**
 * 根据速度命中所属缩放档位。
 */
const resolveScaleBandIndex = (speedKmh: number) => {
  const safeSpeed = Math.max(0, Number(speedKmh) || 0);
  const bandIndex = DYNAMIC_SCALE_BANDS.findIndex((band) => safeSpeed >= band.minSpeedKmh && safeSpeed < band.maxSpeedKmh);
  return bandIndex >= 0 ? bandIndex : DYNAMIC_SCALE_BANDS.length - 1;
};
/**
 * 基于上一次档位应用滞回,避免速度在边界附近频繁切换。
 */
const resolveStableScaleBandIndex = (speedKmh: number, previousBandIndex: number | null) => {
  const targetBandIndex = resolveScaleBandIndex(speedKmh);
  if (previousBandIndex === null || previousBandIndex === targetBandIndex) {
    return targetBandIndex;
  }
  let nextBandIndex = previousBandIndex;
  if (targetBandIndex > previousBandIndex) {
    while (nextBandIndex < targetBandIndex) {
      const fasterBand = DYNAMIC_SCALE_BANDS[nextBandIndex + 1];
      if (speedKmh >= fasterBand.minSpeedKmh + SCALE_BAND_HYSTERESIS_KMH) {
        nextBandIndex += 1;
        continue;
      }
      break;
    }
    return nextBandIndex;
  }
  while (nextBandIndex > targetBandIndex) {
    const currentBand = DYNAMIC_SCALE_BANDS[nextBandIndex];
    if (speedKmh < currentBand.minSpeedKmh - SCALE_BAND_HYSTERESIS_KMH) {
      nextBandIndex -= 1;
      continue;
    }
    break;
  }
  return nextBandIndex;
};
/**
 * 对最近几帧速度做均值平滑,降低单帧抖动对缩放的影响。
 */
const resolveSmoothedSpeed = (samples: number[], nextSpeedKmh: number) => {
  const nextSamples = [...samples, Math.max(0, nextSpeedKmh)].slice(-SPEED_SMOOTHING_SAMPLE_SIZE);
  const total = nextSamples.reduce((sum, item) => sum + item, 0);
  return {
    nextSamples,
    smoothedSpeedKmh: total / nextSamples.length
  };
};
/**
 * 将目标比例尺限制为渐进变化,避免视角一步跳太远。
 */
const resolveNextScale = (currentScale: number, targetScale: number) => {
  const scaleDelta = targetScale - currentScale;
  if (Math.abs(scaleDelta) <= MAX_SCALE_STEP_PER_TICK) {
    return targetScale;
  }
  return currentScale + Math.sign(scaleDelta) * MAX_SCALE_STEP_PER_TICK;
};
/**
 * 将速度换算成目标比例尺,速度越高视角越远。
 */
const resolveScaleBySpeed = (speedKmh: number) => {
  const safeSpeed = clamp(speedKmh, LOW_SPEED_KMH, HIGH_SPEED_KMH);
  const speedRatio = (safeSpeed - LOW_SPEED_KMH) / (HIGH_SPEED_KMH - LOW_SPEED_KMH || 1);
  const nextScale = MAX_DYNAMIC_SCALE - (MAX_DYNAMIC_SCALE - MIN_DYNAMIC_SCALE) * speedRatio;
  return Math.round(nextScale / SCALE_STEP) * SCALE_STEP;
const resolveScaleBySpeed = (speedKmh: number, previousBandIndex: number | null) => {
  const nextBandIndex = resolveStableScaleBandIndex(speedKmh, previousBandIndex);
  return {
    targetScale: DYNAMIC_SCALE_BANDS[nextBandIndex].scale,
    bandIndex: nextBandIndex
  };
};
/**
@@ -59,6 +138,8 @@
export default function useActiveTripScale(deps: Deps) {
  const [resumeVersion, setResumeVersion] = useState(0); // 冷却结束后触发重新评估
  const resumeTimerRef = useRef<any>(null);
  const speedSamplesRef = useRef<number[]>([]);
  const scaleBandIndexRef = useRef<number | null>(null);
  const isActiveStatus = useMemo(() => {
    return ACTIVE_TRIP_STATUSES.includes(deps.tripStatus);
  }, [deps.tripStatus]);
@@ -96,7 +177,7 @@
    }
    deps.holdDynamicScale(USER_GESTURE_HOLD_MS);
    scheduleResume(USER_GESTURE_HOLD_MS + SCALE_RESUME_BUFFER_MS);
  }, [deps, scheduleResume]);
  }, [deps.holdDynamicScale, scheduleResume]);
  /**
   * ORDER_UPDATE_LINE 渲染完成后,自动执行一次全路线入屏。
@@ -122,11 +203,13 @@
      }, 0);
    });
    scheduleResume(ORDER_UPDATE_LINE_HOLD_MS + SCALE_RESUME_BUFFER_MS);
  }, [deps, isActiveStatus, scheduleResume]);
  }, [deps.computeBottomPadding, deps.holdDynamicScale, deps.resolveSafeTopInset, isActiveStatus, scheduleResume]);
  useEffect(() => {
    if (!isActiveStatus) {
      clearResumeTimer();
      speedSamplesRef.current = [];
      scaleBandIndexRef.current = null;
      return;
    }
    const speedKmh = Number(deps.speedKmh);
@@ -139,12 +222,17 @@
      scheduleResume(holdUntil - now + SCALE_RESUME_BUFFER_MS);
      return;
    }
    const targetScale = resolveScaleBySpeed(speedKmh);
    if (Math.abs(targetScale - deps.mapScale) < MIN_SCALE_CHANGE) {
    const { nextSamples, smoothedSpeedKmh } = resolveSmoothedSpeed(speedSamplesRef.current, speedKmh);
    speedSamplesRef.current = nextSamples;
    const { targetScale, bandIndex } = resolveScaleBySpeed(smoothedSpeedKmh, scaleBandIndexRef.current);
    scaleBandIndexRef.current = bandIndex;
    const nextScale = resolveNextScale(deps.mapScale, targetScale);
    if (Math.abs(nextScale - deps.mapScale) < MIN_SCALE_CHANGE) {
      return;
    }
    deps.setMapScale(targetScale);
  }, [deps, isActiveStatus, resumeVersion, scheduleResume]);
    deps.setMapScale(clamp(nextScale, 14, 18));
    deps.syncActiveTripViewportToVehicle();
  }, [deps.getDynamicScaleHoldUntil, deps.mapScale, deps.setMapScale, deps.speedKmh, deps.syncActiveTripViewportToVehicle, isActiveStatus, resumeVersion, scheduleResume]);
  useEffect(() => {
    return () => {
src/robotaxi/pages/begin-route/hooks/use-android-overlays.ts
@@ -47,14 +47,22 @@
          const x = ((target.longitude - sw.longitude) / lngRange) * rect.width;
          const y = ((ne.latitude - target.latitude) / latRange) * rect.height;
          resolve({
            left: `${rect.left + x}px`,
            top: `${rect.top + y + transformTopOffset}px`,
            x: rect.left + x,
            y: rect.top + y + transformTopOffset,
          });
        },
        fail: () => resolve(null)
      });
    });
  };
  // 固定层样式:节点锚定在 (0,0),仅通过 transform 位移,减少 left/top 频繁更新导致的重排闪动。
  const buildTransformOverlayStyle = (position: { x: number; y: number }, anchorTransform: string) => ({
    position: 'fixed',
    left: '0px',
    top: '0px',
    transform: `translate3d(${position.x}px, ${position.y}px, 0) ${anchorTransform}`
  });
  // 获取并缓存地图容器矩形
  const ensureMapRect = async () => {
@@ -82,8 +90,8 @@
    if (!pos) return;
    setWaitingRadarStyle({
      position: 'fixed',
      left: pos.left,
      top: pos.top,
      left: `${pos.x}px`,
      top: `${pos.y}px`,
      transform: 'translate(-50%, -55%)'
    });
  };
@@ -97,12 +105,7 @@
    const rect = await ensureMapRect();
    const pos = await computeScreenPosition(deps.vehicleLocation, rect, 60);
    if (!pos) return;
    setCarCalloutStyle({
      position: 'fixed',
      left: pos.left,
      top: pos.top,
      transform: 'translate(-50%, -120%)'
    });
    setCarCalloutStyle(buildTransformOverlayStyle(pos, 'translate(-50%, -120%)'));
    setCarCalloutVisible(true);
  };
@@ -122,8 +125,8 @@
    if (!pos) return;
    setArriveCalloutStyle({
      position: 'fixed',
      left: pos.left,
      top: pos.top,
      left: `${pos.x}px`,
      top: `${pos.y}px`,
      transform: 'translate(-50%, -120%)'
    });
    setArriveCalloutVisible(true);
@@ -143,9 +146,9 @@
    }
    setRouteSummaryCalloutStyle({
      position: 'fixed',
      left: pos.left,
      top: pos.top,
      transform: 'translate(-50%, -100%)'
      left: `${pos.x}px`,
      top: `${pos.y}px`,
      transform: 'translate(0, 0)'
    });
    setRouteSummaryCalloutVisible(true);
  };
src/robotaxi/pages/begin-route/hooks/use-bluetooth-access.ts
@@ -17,6 +17,7 @@
};
const BLUETOOTH_AUTH_KEY = 'ROBOTAXI_BLUETOOTH_AUTHORIZED';
const BLUETOOTH_SCOPE = 'scope.bluetooth' as any;
const SYSTEM_OFF_ERROR_RE = /(not available|10001|未开启|开关未打开|bluetooth adapter is not available)/i;
const ENV_UNSUPPORTED_ERROR_RE = /(暂不支持|not support|devtools|Mac 以外)/i;
const PERMISSION_DENY_ERROR_RE = /deny|auth deny|authorize|permission|scope/i;
@@ -75,6 +76,58 @@
  const startOpenSettingPollingRef = useRef<() => void>(() => {});
  const startBluetoothStateWatchRef = useRef<() => void>(() => {});
  const stopBluetoothStateWatchRef = useRef<() => void>(() => {});
  /**
   * 读取当前小程序蓝牙授权状态。
   */
  const getMiniappBluetoothAuthState = useCallback(async () => {
    try {
      const setting = await Taro.getSetting();
      const authSetting = setting?.authSetting || {};
      if (Object.prototype.hasOwnProperty.call(authSetting, BLUETOOTH_SCOPE)) {
        return authSetting[BLUETOOTH_SCOPE] === true ? 'authorized' : 'denied';
      }
    } catch (error) {
    }
    return 'unknown';
  }, []);
  /**
   * 已确认拿到小程序蓝牙权限后,统一收口后续初始化与兜底提示。
   */
  const finishBluetoothAuthorization = useCallback(async () => {
    Taro.setStorageSync(BLUETOOTH_AUTH_KEY, true);
    setShowAuthBluetoothPopup(false);
    setStatus('authorized');
    setStatusReady(true);
    startBluetoothAuth();
    const openResult = await openBluetoothAdapterSafe();
    if (openResult.opened) {
      await refreshBluetoothState();
      return;
    }
    if (openResult.errorType === 'permission_deny') {
      setStatus('unauthorized');
      try {
        await Taro.openSetting();
      } catch (openSettingError) {
        console.error('打开设置失败:', openSettingError);
      }
      Taro.showToast({ title: '请先授权小程序蓝牙权限', icon: 'none' });
      return;
    }
    if (openResult.errorType === 'system_off') {
      setStatus('system_off');
      Taro.showToast({ title: '请先开启系统蓝牙', icon: 'none' });
      return;
    }
    Taro.showToast({ title: '蓝牙初始化失败,请稍后重试', icon: 'none' });
    await refreshBluetoothState();
  }, [refreshBluetoothState, startBluetoothAuth]);
  useEffect(() => {
    startBluetoothAuthRef.current = startBluetoothAuth;
@@ -241,53 +294,45 @@
  // 执行小程序蓝牙授权,成功后展示成功弹窗
  const handleAuthorizeBluetooth = useCallback(async () => {
    const miniappBluetoothAuthState = await getMiniappBluetoothAuthState();
    if (miniappBluetoothAuthState === 'denied') {
      try {
        await Taro.openSetting();
      } catch (openSettingError) {
        console.error('打开设置失败:', openSettingError);
        Taro.showToast({ title: '请先授权小程序蓝牙权限', icon: 'none' });
        return;
      }
      const latestAuthState = await getMiniappBluetoothAuthState();
      if (latestAuthState !== 'authorized') {
        Taro.showToast({ title: '请先授权小程序蓝牙权限', icon: 'none' });
        return;
      }
      await finishBluetoothAuthorization();
      return;
    }
    try {
      await Taro.authorize({ scope: 'scope.bluetooth' as any });
      await Taro.authorize({ scope: BLUETOOTH_SCOPE });
    } catch (authorizeError) {
      console.error('授权蓝牙失败:', authorizeError);
      console.log('授权蓝牙失败:', authorizeError);
      try {
        await Taro.openSetting();
      } catch (openSettingError) {
        console.error('打开设置失败:', openSettingError);
        console.log('打开设置失败:', openSettingError);
        Taro.showToast({ title: '请先授权小程序蓝牙权限', icon: 'none' });
        return;
      }
      Taro.showToast({ title: '请先授权小程序蓝牙权限', icon: 'none' });
      return;
    }
    // 用户同意授权后先更新 UI,避免出现“同意后仍显示去授权”的滞后感。
    Taro.setStorageSync(BLUETOOTH_AUTH_KEY, true);
    setShowAuthBluetoothPopup(false);
    setStatus('authorized');
    setStatusReady(true);
    startBluetoothAuth();
    const openResult = await openBluetoothAdapterSafe();
    if (openResult.opened) {
      await refreshBluetoothState();
      return;
    }
    if (openResult.errorType === 'permission_deny') {
      setStatus('unauthorized');
      try {
        await Taro.openSetting();
      } catch (openSettingError) {
        console.error('打开设置失败:', openSettingError);
        // 忽略打开设置失败,统一走文案提示
      const latestAuthState = await getMiniappBluetoothAuthState();
      if (latestAuthState !== 'authorized') {
        Taro.showToast({ title: '请先授权小程序蓝牙权限', icon: 'none' });
        return;
      }
      Taro.showToast({ title: '请先授权小程序蓝牙权限', icon: 'none' });
      await finishBluetoothAuthorization();
      return;
    }
    if (openResult.errorType === 'system_off') {
      setStatus('system_off');
      Taro.showToast({ title: '请先开启系统蓝牙', icon: 'none' });
      return;
    }
    Taro.showToast({ title: '蓝牙初始化失败,请稍后重试', icon: 'none' });
    await refreshBluetoothState();
  }, [refreshBluetoothState, openBluetoothAdapterSafe, startBluetoothAuth]);
    await finishBluetoothAuthorization();
  }, [finishBluetoothAuthorization, getMiniappBluetoothAuthState]);
  // 蓝牙卡片点击分发:未开蓝牙->去开启,未授权->弹授权,已授权->弹成功态说明
  const handleBluetoothCardClick = useCallback(() => {
src/robotaxi/pages/begin-route/hooks/use-map-markers.ts
@@ -1,7 +1,8 @@
import Taro from '@tarojs/taro';
import { useEffect, useRef, useState } from 'react';
import { TripStatus } from '../constants/trip-status';
import changeSiteUpIcon from '../../../assets/change-site-up.png';
const CHANGE_SITE_UP_ICON_URL = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/change-site-up.png'; // 行程中修改目的地时的上车点图标
type Deps = {
  tripStatus: TripStatus;
@@ -52,20 +53,21 @@
  const createPickupMarker = (pickupSite: any, useAndroidNativeTrip1Callout: boolean) => {
    if (!(pickupSite?.latitude && pickupSite?.longitude)) return null;
    const shouldUseChangePickupIcon = deps.isModifyDestinationReviewVisible && deps.tripStatus >= TripStatus.VehicleArrivedPickup; // 修改目的地确认态且进入行程阶段时使用换点上车图标
    const shouldHidePickupName = deps.tripStatus === TripStatus.InService && deps.isModifyDestinationReviewVisible; // 行程中修改目的地时只展示换点图标,不展示上车点名称
    return {
      id: 1,
      latitude: pickupSite.latitude,
      longitude: pickupSite.longitude,
      iconPath: shouldUseChangePickupIcon
        ? changeSiteUpIcon
        ? CHANGE_SITE_UP_ICON_URL
        : 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/maker-up.png',
      width: 22,
      height: 40,
      anchor: { x: 0.5, y: 1 },
      zIndex: 1,
      name: pickupSite?.name || '上车点',
      hideNameCallout: deps.isModifyDestinationReviewVisible, // 修改目的地确认态隐藏上车点名称气泡
      ...(useAndroidNativeTrip1Callout
      hideNameCallout: deps.isModifyDestinationReviewVisible || shouldHidePickupName, // 修改目的地确认态隐藏上车点名称气泡
      ...(!shouldHidePickupName && useAndroidNativeTrip1Callout
        ? {
          callout: {
            content: (deps.tripStatus === TripStatus.PublishRoute || deps.showTrip1StyleEndpoint) ? `${pickupSite?.name || '上车点'} >` : (pickupSite?.name || '上车点'),
@@ -78,9 +80,9 @@
            textAlign: 'center',
          },
        }
        : {
        : !shouldHidePickupName ? {
          customCallout: { anchorY: [TripStatus.Publishing, TripStatus.CallingCar].includes(deps.tripStatus) ? 150 : -5, anchorX: 0, display: 'ALWAYS' },
        })
        } : {})
    };
  };
@@ -186,7 +188,10 @@
          ...mergedMarkers[nextCarIndex],
          latitude: prevCar.latitude,
          longitude: prevCar.longitude,
          rotate: prevCar.rotate
          // 平移态仅保留当前位置,朝向始终以最新车辆状态为准,避免旧 marker 角度覆盖最新 socket 方向。
          rotate: typeof mergedMarkers[nextCarIndex]?.rotate === 'number'
            ? mergedMarkers[nextCarIndex].rotate
            : prevCar.rotate
        };
        return mergedMarkers;
      });
src/robotaxi/pages/begin-route/hooks/use-map-viewport.ts
@@ -33,6 +33,7 @@
const DROPOFF_MAX_SCALE = 20;
const DEFAULT_DYNAMIC_SCALE_HOLD_MS = 2500; // 一次性视口调整后,暂停速度缩放的默认冷却时长
const ACTIVE_TRIP_VIEWPORT_SYNC_MIN_DELTA = 0.00001; // 车辆中心跟随的最小经纬度变化,约 1 米,避免重复写入中心点状态
export default function useMapViewport(rawDeps?: Partial<Deps>) {
    const deps = { ...defaultDeps, ...(rawDeps || {}) } as Deps;
@@ -290,6 +291,26 @@
            .catch(() => { });
    };
    /**
     * 行程动态缩放生效时,将地图中心跟随到车辆位置,确保车辆留在当前视野内。
     */
    const syncActiveTripViewportToVehicle = useCallback(() => {
        const vehicleLatitude = Number(deps.vehicleLocation?.latitude);
        const vehicleLongitude = Number(deps.vehicleLocation?.longitude);
        if (!Number.isFinite(vehicleLatitude) || !Number.isFinite(vehicleLongitude)) {
            return;
        }
        const currentLatitude = Number(deps.latitude);
        const currentLongitude = Number(deps.longitude);
        const hasLatitudeDelta = !Number.isFinite(currentLatitude) || Math.abs(currentLatitude - vehicleLatitude) >= ACTIVE_TRIP_VIEWPORT_SYNC_MIN_DELTA;
        const hasLongitudeDelta = !Number.isFinite(currentLongitude) || Math.abs(currentLongitude - vehicleLongitude) >= ACTIVE_TRIP_VIEWPORT_SYNC_MIN_DELTA;
        if (!hasLatitudeDelta && !hasLongitudeDelta) {
            return;
        }
        deps.setLatitude(vehicleLatitude);
        deps.setLongitude(vehicleLongitude);
    }, [deps.latitude, deps.longitude, deps.setLatitude, deps.setLongitude, deps.vehicleLocation?.latitude, deps.vehicleLocation?.longitude]);
    useEffect(() => {
        if ([TripStatus.InService, TripStatus.ArrivedDestination].includes(deps.tripStatus)) {
            setShowLocationFlag(false);
@@ -405,6 +426,7 @@
        computeBottomPadding,
        holdDynamicScale,
        getDynamicScaleHoldUntil,
        syncActiveTripViewportToVehicle,
        updateMapViewportPadding,
        toOrigin,
        getLocationInfo
src/robotaxi/pages/begin-route/hooks/use-modify-destination-flow.ts
@@ -17,6 +17,11 @@
  setEndSite: (site: any) => void;
  toSelectSite: (pageType: 'start' | 'end', keyword?: string) => void;
  onModifySuccess?: (restoredRoutePoints?: any[]) => void;
  onCancelReview?: (payload: {
    previousEndSite: any;
    previousFareEstimate: any;
    previousRoutePoints: any[];
  }) => void;
};
type ExitModifyDestinationReviewOptions = {
@@ -89,6 +94,11 @@
    if (session?.active && isValidSite(candidate)) {
      if (isSameSite(session.previousEndSite, candidate)) {
        deps.onCancelReview?.({
          previousEndSite: session.previousEndSite || deps.endSite || null,
          previousFareEstimate: session.previousFareEstimate || null,
          previousRoutePoints: Array.isArray(cachedRoutePoints) ? cachedRoutePoints : []
        });
        clearModifyDestinationSession();
        setPreviewEndSite(null);
        setPreviousEndSite(null);
@@ -127,6 +137,10 @@
    setPreviousEndSite(null);
    setPreviousFareEstimate(null);
    setPreviousRoutePoints([]);
    if (isValidSite(cachedOverride)) {
      Taro.removeStorageSync(MODIFY_DESTINATION_OVERRIDE_KEY);
      setOverrideEndSite(null);
    }
  }, [
    deps.tripStatus,
    deps.orderId,
@@ -166,6 +180,17 @@
  const clearLocalDestinationOverride = () => {
    setOverrideEndSite(null);
    Taro.removeStorageSync(MODIFY_DESTINATION_OVERRIDE_KEY);
  };
  /**
   * 预览态下同步旧目的地路线,供置灰路线与取消预览后的回滚复用。
   */
  const updatePreviousRoutePoints = (points: any[]) => {
    const safeRoutePoints = Array.isArray(points) ? points : [];
    setPreviousRoutePoints(safeRoutePoints);
    if (Taro.getStorageSync(MODIFY_DESTINATION_SESSION_KEY)?.active) {
      Taro.setStorageSync(MODIFY_DESTINATION_ROUTE_POINTS_KEY, safeRoutePoints);
    }
  };
  /**
@@ -267,6 +292,7 @@
    previousEndSite,
    previousFareEstimate,
    previousRoutePoints,
    updatePreviousRoutePoints,
    isConfirmingModifyDestination,
    isModifyDestinationReviewVisible,
    hasLocalDestinationOverride,
src/robotaxi/pages/begin-route/hooks/use-order-submit.ts
@@ -90,7 +90,7 @@
      const selectedRoute = deps.routeOptions[deps.selectedRouteIndex];
      if (selectedRoute?.route?.routeID) {
        payload.selectedRouteId = selectedRoute.route.routeID;
        payload.navigationSnapshotId = selectedRoute.route.routeID;
        payload.navigationSnapshotId = selectedRoute.snapshotId;
      }
    }
src/robotaxi/pages/begin-route/hooks/use-page-actions.ts
@@ -7,6 +7,7 @@
  MODIFY_DESTINATION_ROUTE_POINTS_KEY,
  MODIFY_DESTINATION_SESSION_KEY
} from '../constants/modify-destination';
import { clearPassedRouteCache } from './passed-route-cache';
type Deps = {
  tripStatus: TripStatus;
@@ -70,6 +71,7 @@
   */
  const toBack = () => {
    clearModifyDestinationStorage();
    clearPassedRouteCache();
    if (deps.tripStatus === TripStatus.PaymentSuccess || deps.tripStatus === TripStatus.PublishRoute) {
      clearOrderTripStorage();
      Taro.removeStorageSync('endSite');
src/robotaxi/pages/begin-route/hooks/use-page-restore.ts
@@ -5,6 +5,7 @@
import { queryVehicleByOrderId } from '../../../api/begin-route';
import { toTripStatus, TripStatus } from '../constants/trip-status';
import { MODIFY_DESTINATION_SESSION_KEY } from '../constants/modify-destination';
import { readPassedRouteCache } from './passed-route-cache';
type Deps = {
  wsHost: string;
@@ -14,6 +15,7 @@
  setTripStatus: (value: TripStatus) => void;
  setStartSite: (value: any) => void;
  setEndSite: (value: any) => void;
  setVehicleMoveAlongPoints: (value: any[]) => void;
  setTrunkStatus: (value: boolean) => void;
  setLatitude: (value: number | null) => void;
  setLongitude: (value: number | null) => void;
@@ -138,6 +140,11 @@
    const cachedTripStatusStorage = Taro.getStorageSync('tripStatus');
    const cachedTripStatus = toTripStatus(cachedTripStatusStorage, deps.getCurrentTripStatus());
    deps.setTripStatus(cachedTripStatus);
    if ([TripStatus.VehicleApproaching, TripStatus.InService].includes(cachedTripStatus)) {
      deps.setVehicleMoveAlongPoints(readPassedRouteCache(String(cachedOrderId || '')));
    } else {
      deps.setVehicleMoveAlongPoints([]);
    }
    syncCachedOrderVehicle(cachedOrderId);
    syncSitesFromActiveOrder(cachedTripStatusStorage, s, e);
    if (cachedOrderId && [TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup, TripStatus.InService, TripStatus.ArrivedDestination, TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(cachedTripStatus)) {
src/robotaxi/pages/begin-route/hooks/use-polyline-render.ts
@@ -69,6 +69,8 @@
  buildPolylineLines: (displayRoutePoints: any[]) => any[];
};
const PASSED_ROUTE_POLYLINE_ID = 1999; // 已走过路线浅色折线 ID
/** 统一获取路线降采样上限。 */
const resolveRoutePointLimit = (isAndroid: boolean) => (
  isAndroid ? ANDROID_ROUTE_POINT_LIMIT : DEFAULT_ROUTE_POINT_LIMIT
@@ -232,18 +234,37 @@
  };
  /**
   * 获取修改目的地预览态下的已走过路线点。
   */
  const getPassedRoutePoints = () => {
    const shouldShowPassedRoute = deps.tripStatus === TripStatus.InService
      && deps.allowSelectableRoutes
      && Array.isArray(deps.vehicleMoveAlongPoints)
      && deps.vehicleMoveAlongPoints.length > 1;
    if (!shouldShowPassedRoute) {
      return [];
    }
    return reducePoints(deps.vehicleMoveAlongPoints || [], resolveRoutePointLimit(deps.isAndroid));
  };
  /**
   * 构建 polyline 渲染签名,避免重复 setState。
   */
  const buildPolylineKey = (displayRoutePoints: any[]) => {
    const passedRoutePoints = getPassedRoutePoints();
    const lastRoutePoint = displayRoutePoints[displayRoutePoints.length - 1];
    const lastPassedRoutePoint = passedRoutePoints[passedRoutePoints.length - 1];
    const lastWalkingPoint = walkingPoints[walkingPoints.length - 1];
    return [
      deps.shouldShowRoute ? 1 : 0,
      deps.statusRule.showWalking ? 1 : 0,
      displayRoutePoints.length,
      passedRoutePoints.length,
      walkingPoints.length,
      lastRoutePoint?.latitude ?? '',
      lastRoutePoint?.longitude ?? '',
      lastPassedRoutePoint?.latitude ?? '',
      lastPassedRoutePoint?.longitude ?? '',
      lastWalkingPoint?.latitude ?? '',
      lastWalkingPoint?.longitude ?? '',
      allRoutePoints.length,
@@ -256,6 +277,18 @@
   */
  const buildPolylineLines = (displayRoutePoints: any[]) => {
    const lines: any[] = [];
    const passedRoutePoints = getPassedRoutePoints();
    if (passedRoutePoints.length > 1) {
      lines.push({
        id: PASSED_ROUTE_POLYLINE_ID,
        points: passedRoutePoints,
        color: '#D7DDEF',
        width: 10,
        clickable: false,
        dottedLine: false,
        arrowLine: false,
      });
    }
    if (deps.shouldShowRoute && deps.allowSelectableRoutes && allRoutePoints.length > 1) {
      allRoutePoints.forEach((points, index) => {
        if (!points?.length) {
@@ -332,10 +365,12 @@
    lastPolylineUpdateAtRef.current = 0;
  }, [
    allRoutePoints,
    deps.allowSelectableRoutes,
    deps.isAndroid,
    deps.shouldShowRoute,
    deps.statusRule.showWalking,
    deps.tripStatus,
    deps.vehicleMoveAlongPoints,
    routePoints,
    selectedRouteIndex,
    walkingPoints
src/robotaxi/pages/begin-route/hooks/use-preloaded-map-assets.ts
New file
@@ -0,0 +1,83 @@
import { useEffect, useState } from 'react';
import { preloadImageToTemp } from '../../../../utils/Robotaxi-public-method';
import { TripStatus } from '../constants/trip-status';
/** 地图资源在线地址集合。 */
type MapAssetUrls = {
  /** 自定义气泡箭头图地址。 */
  customCalloutIconUrl: string;
  /** 等待态气泡背景图地址。 */
  waitingCalloutBgUrl: string;
  /** 等待态雷达图地址。 */
  waitingRadarUrl: string;
  /** 路线概览气泡背景图地址。 */
  routeSummaryBgUrl: string;
};
/** Hook 入参。 */
type Params = {
  /** 当前行程状态。 */
  tripStatus: TripStatus;
  /** 地图资源地址集合。 */
  assetUrls: MapAssetUrls;
};
/** Hook 返回值。 */
type UsePreloadedMapAssetsResult = {
  /** 自定义气泡箭头图本地地址。 */
  customCalloutIconSrc: string;
  /** 等待态气泡背景图本地地址。 */
  waitingCalloutBgSrc: string;
  /** 等待态雷达图本地地址。 */
  waitingRadarSrc: string;
  /** 路线概览气泡背景图本地地址。 */
  routeSummaryBgSrc: string;
};
/**
 * 统一预加载 BeginRoute 地图相关图片资源。
 */
export default function usePreloadedMapAssets(params: Params): UsePreloadedMapAssetsResult {
  const [customCalloutIconSrc, setCustomCalloutIconSrc] = useState(params.assetUrls.customCalloutIconUrl); // 自定义气泡箭头图本地地址
  const [waitingCalloutBgSrc, setWaitingCalloutBgSrc] = useState(params.assetUrls.waitingCalloutBgUrl); // 等待态气泡背景图本地地址
  const [waitingRadarSrc, setWaitingRadarSrc] = useState(params.assetUrls.waitingRadarUrl); // 等待态雷达图本地地址
  const [routeSummaryBgSrc, setRouteSummaryBgSrc] = useState(params.assetUrls.routeSummaryBgUrl); // 路线概览气泡背景图本地地址
  /**
   * 预加载单张图片并在组件仍挂载时回写结果。
   */
  const preloadSingleAsset = async (url: string, setter: (value: string) => void, cancelledRef: { current: boolean }) => {
    const local = await preloadImageToTemp(url);
    if (!cancelledRef.current) {
      setter(local);
    }
  };
  useEffect(() => {
    const cancelledRef = { current: false };
    preloadSingleAsset(params.assetUrls.customCalloutIconUrl, setCustomCalloutIconSrc, cancelledRef);
    preloadSingleAsset(params.assetUrls.routeSummaryBgUrl, setRouteSummaryBgSrc, cancelledRef);
    return () => {
      cancelledRef.current = true;
    };
  }, [params.assetUrls.customCalloutIconUrl, params.assetUrls.routeSummaryBgUrl]);
  useEffect(() => {
    if (![TripStatus.Publishing, TripStatus.CallingCar].includes(params.tripStatus)) {
      return;
    }
    const cancelledRef = { current: false };
    preloadSingleAsset(params.assetUrls.waitingCalloutBgUrl, setWaitingCalloutBgSrc, cancelledRef);
    preloadSingleAsset(params.assetUrls.waitingRadarUrl, setWaitingRadarSrc, cancelledRef);
    return () => {
      cancelledRef.current = true;
    };
  }, [params.tripStatus, params.assetUrls.waitingCalloutBgUrl, params.assetUrls.waitingRadarUrl]);
  return {
    customCalloutIconSrc,
    waitingCalloutBgSrc,
    waitingRadarSrc,
    routeSummaryBgSrc
  };
}
src/robotaxi/pages/begin-route/hooks/use-route-manager.shared.ts
@@ -93,6 +93,8 @@
  computeBottomPadding: (points: any[], viewportType: string) => number;
  /** 节流更新车辆位置。 */
  updateVehicleLocationThrottled: (lat: number, lng: number, force?: boolean) => void;
  /** 已行驶轨迹点(servedPoints)。 */
  vehicleMoveAlongPoints?: any[];
  /** 设置车辆朝向。 */
  setVehicleDirection: (value: number) => void;
  /** 设置订单路线摘要。 */
@@ -120,5 +122,6 @@
  resolveSafeTopInset: () => 0,
  computeBottomPadding: () => 280,
  updateVehicleLocationThrottled: () => { },
  vehicleMoveAlongPoints: [],
  setVehicleDirection: () => { }
};
src/robotaxi/pages/begin-route/hooks/use-route-manager.ts
@@ -4,10 +4,17 @@
import useRouteRequest from './use-route-request';
import { Deps, defaultDeps, RouteDistanceValue } from './use-route-manager.shared';
type UseRouteManagerOptions = {
  /** 接口回显主路线后,用于同步初始化平滑擦线基线。 */
  syncSmoothRouteBaselineRef?: React.MutableRefObject<((snapshot: any) => void) | null>;
  /** 最近一次由 socket 写入车辆朝向的时间。 */
  lastSocketDirectionAtRef?: React.MutableRefObject<number>;
};
/**
 * 聚合路线请求、渲染与交互逻辑,向页面保持稳定接口。
 */
export default function useRouteManager(rawDeps?: Partial<Deps>) {
export default function useRouteManager(rawDeps?: Partial<Deps>, options?: UseRouteManagerOptions) {
  /** 当前路由依赖。 */
  const deps = { ...defaultDeps, ...(rawDeps || {}) } as Deps;
  /** 路线选项集合。 */
@@ -71,7 +78,9 @@
    currentFullRouteRef: renderState.currentFullRouteRef,
    lastErasedIndexRef: renderState.lastErasedIndexRef,
    orderRouteInitializedRef: renderState.orderRouteInitializedRef,
    syncRouteViewport: interactionState.syncRouteViewport
    syncRouteViewport: interactionState.syncRouteViewport,
    syncSmoothRouteBaselineRef: options?.syncSmoothRouteBaselineRef,
    lastSocketDirectionAtRef: options?.lastSocketDirectionAtRef
  });
  return {
src/robotaxi/pages/begin-route/hooks/use-route-request.ts
@@ -5,6 +5,7 @@
import { ensureSelectedCityByLocation } from '../../../utils/city';
import { normalizeDistanceKm, normalizeDurationSeconds } from '../../../utils/format';
import { TripStatus } from '../constants/trip-status';
import type { RouteSnapshot } from './runtime-sync';
import {
  Deps,
  RequestRouteAndFareOptions,
@@ -53,6 +54,10 @@
  orderRouteInitializedRef: React.MutableRefObject<boolean>;
  /** 同步多路线视口。 */
  syncRouteViewport: (routePointGroups: any[][], nextWalkingPoints?: any[]) => void;
  /** 接口回显主路线后,同步初始化平滑擦线基线。 */
  syncSmoothRouteBaselineRef?: React.MutableRefObject<((snapshot: RouteSnapshot) => void) | null>;
  /** 最近一次由 socket 写入车辆朝向的时间。 */
  lastSocketDirectionAtRef?: React.MutableRefObject<number>;
};
/** 路线请求 Hook 返回值。 */
@@ -101,6 +106,27 @@
  ...(item || {}),
  snapshotId: item?.snapshotId ?? snapshotId,
  generatedAtMillis: item?.generatedAtMillis ?? generatedAtMillis
});
/** 当 socket 已提供较新的朝向时,接口方向只作为兜底,不再覆盖实时值。 */
const SOCKET_DIRECTION_PRIORITY_WINDOW_MS = 4000;
/** 将订单路线接口响应组装成平滑擦线基线快照。 */
const buildOrderRouteSnapshot = (
  response: any,
  points: Array<{ longitude: number; latitude: number }>,
  direction: number | null
): RouteSnapshot => ({
  points,
  distance: normalizeDistanceKm(Number(response?.distance)),
  duration: normalizeDurationSeconds(Number(response?.duration)),
  direction,
  speedKmh: Number.isFinite(Number(response?.speed)) ? Number(response?.speed) : null,
  timestamp: Date.now(),
  vehicleMoveAlongPoints: [],
  forceFullRedraw: true,
  eraseVehiclePointMode: 'first_then_served_last',
  normalVehiclePointMode: 'served_last_then_first'
});
/**
@@ -164,7 +190,9 @@
    currentFullRouteRef,
    lastErasedIndexRef,
    orderRouteInitializedRef,
    syncRouteViewport
    syncRouteViewport,
    syncSmoothRouteBaselineRef,
    lastSocketDirectionAtRef
  } = params;
  /** 路线请求签名引用。 */
@@ -527,8 +555,14 @@
        });
        const normalizedDirection = resolveDirectionValue(response);
        if (typeof normalizedDirection === 'number') {
          deps.setVehicleDirection(normalizedDirection);
        const hasFreshSocketDirection = Date.now() - Number(lastSocketDirectionAtRef?.current || 0) < SOCKET_DIRECTION_PRIORITY_WINDOW_MS;
        const fallbackDirection = hasFreshSocketDirection ? null : normalizedDirection;
        if (typeof fallbackDirection === 'number') {
          deps.setVehicleDirection(fallbackDirection);
        }
        if ([TripStatus.VehicleApproaching, TripStatus.InService].includes(status) && points.length > 0) {
          const routeSnapshot = buildOrderRouteSnapshot(response, points, fallbackDirection);
          syncSmoothRouteBaselineRef?.current?.(routeSnapshot);
        }
      })
      .catch(() => { });
src/robotaxi/pages/begin-route/hooks/use-trip-runtime.ts
@@ -1,6 +1,5 @@
import Taro, { useDidHide } from '@tarojs/taro';
import { useEffect } from 'react';
import { preloadImageToTemp } from '../../../../utils/Robotaxi-public-method';
import { getOrderDetail } from '../../../api/order';
import { TripStatus } from '../constants/trip-status';
@@ -99,18 +98,6 @@
  setShowCustomerService: (value: boolean) => void;
  /** 车辆信息 */
  vehicleInfo: any;
  /** 等待背景图 url */
  waitingCalloutBgUrl: string;
  /** 等待雷达图 url */
  waitingRadarUrl: string;
  /** 自定义气泡图 url */
  customCalloutIconUrl: string;
  /** 设置等待背景图 */
  setWaitingCalloutBgSrc: (value: string) => void;
  /** 设置等待雷达图 */
  setWaitingRadarSrc: (value: string) => void;
  /** 设置气泡图 */
  setCustomCalloutIconSrc: (value: string) => void;
  /** 费用信息 */
  fareEstimate: any;
  /** 请求预估路线 */
@@ -262,28 +249,7 @@
  useEffect(() => {
    if (deps.tripStatus === TripStatus.PublishRoute) checkAndShowPassengerNotice();
    if (![TripStatus.Publishing, TripStatus.CallingCar].includes(deps.tripStatus)) return;
    let cancelled = false;
    const load = async (url: string, setter: (value: string) => void) => {
      const local = await preloadImageToTemp(url);
      if (!cancelled) setter(local);
    };
    load(deps.waitingCalloutBgUrl, deps.setWaitingCalloutBgSrc);
    load(deps.waitingRadarUrl, deps.setWaitingRadarSrc);
    return () => {
      cancelled = true;
    };
  }, [deps.tripStatus]);
  useEffect(() => {
    let cancelled = false;
    const run = async () => {
      const local = await preloadImageToTemp(deps.customCalloutIconUrl);
      if (!cancelled) deps.setCustomCalloutIconSrc(local);
    };
    run();
    return () => { cancelled = true; };
  }, []);
  useEffect(() => {
    Taro.setStorageSync('tripStatus', deps.tripStatus);
src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx
@@ -1,4 +1,5 @@
import Taro from '@tarojs/taro';
import { memo, useCallback, useMemo } from 'react';
import { CoverImage, CoverView, Image, View } from '@tarojs/components';
import ReleaseRoute from '../components/release-route';
import ReleaseBeBeing from '../components/release-be-being';
@@ -6,7 +7,6 @@
import ModifyDestinationReview from '../components/modify-destination-review';
import { TripStatus } from '../constants/trip-status';
import { formatRemainingDurationDisplay } from '../../../utils/format';
import summaryIcon from '../../../assets/summary-icon.png';
type Deps = {
  /** 是否安卓 */
@@ -47,6 +47,8 @@
  hasBeforeUserEndSite: boolean;
  /** 自定义气泡图标 */
  customCalloutIconSrc: string;
  /** 路线概览气泡背景图 */
  routeSummaryBgSrc: string;
  /** 是否使用下单前端点气泡样式 */
  showTrip1StyleEndpoint: boolean;
  /** 上车点 */
@@ -151,21 +153,67 @@
  queuePollingTimeSeconds: number;
};
type DurationDisplay = {
  value: string | number;
  unit: string;
};
type AndroidCarCalloutContentProps = {
  hasBeforeUserEndSite: boolean;
  routeDistance: { value: number; unit: 'km' | 'm' } | null;
  durationDisplay: DurationDisplay;
};
/**
 * Android 车辆气泡内容独立 memo,位置变化时复用内容节点,降低快速移动时的闪烁感。
 */
const AndroidCarCalloutContent = memo(function AndroidCarCalloutContent(props: AndroidCarCalloutContentProps) {
  const { hasBeforeUserEndSite, routeDistance, durationDisplay } = props;
  return (
    <View className="custom-callout-content car-callout-with-before" style={{ padding: '6px 10px' }}>
      {hasBeforeUserEndSite && (
        <View className="car-time car-before-order-tip">即将完成上一单</View>
      )}
      <View className="car-time" style={{ alignItems: 'flex-end' }}>
        <View className="car-time-num">{routeDistance?.value ?? 0}</View>
        <View style={{ flexShrink: 0 }}>{routeDistance?.unit === 'm' ? '米' : '公里'}</View>
        <View style={{ marginLeft: '2.5px' }} className="car-time-num">{durationDisplay.value}</View>
        <View style={{ flexShrink: 0 }}>{durationDisplay.unit}</View>
      </View>
    </View>
  );
}, (prevProps, nextProps) => (
  prevProps.hasBeforeUserEndSite === nextProps.hasBeforeUserEndSite
  && prevProps.routeDistance?.value === nextProps.routeDistance?.value
  && prevProps.routeDistance?.unit === nextProps.routeDistance?.unit
  && prevProps.durationDisplay.value === nextProps.durationDisplay.value
  && prevProps.durationDisplay.unit === nextProps.durationDisplay.unit
));
/**
 * 页面展示委托:地图覆盖物与行程卡片渲染。
 */
export default function useViewDelegates(deps: Deps) {
  /** 时长文案 */
    const durationDisplay = formatRemainingDurationDisplay(deps.routeDuration);
  const routeSummaryText = deps.routeDistance && deps.routeDuration !== null
    ? `${durationDisplay.value}${durationDisplay.unit} ${deps.routeDistance.value}${deps.routeDistance.unit === 'm' ? '米' : '公里'}`
    : '';
  const durationDisplay = useMemo(() => formatRemainingDurationDisplay(deps.routeDuration), [deps.routeDuration]);
  const routeSummaryText = useMemo(() => (
    deps.routeDistance && deps.routeDuration !== null
      ? `${durationDisplay.value}${durationDisplay.unit} ${deps.routeDistance.value}${deps.routeDistance.unit === 'm' ? '米' : '公里'}`
      : ''
  ), [deps.routeDistance, deps.routeDuration, durationDisplay]);
  const shouldHideAndroidMarkerCallout = deps.isAndroid && [TripStatus.PublishRoute, TripStatus.VehicleArrivedPickup, TripStatus.TripFinished, TripStatus.PaymentSuccess, TripStatus.CancelOnlyPickup].includes(deps.tripStatus); // Android 指定状态下隐藏常规 marker 气泡
  const androidCarCalloutContent = useMemo(() => (
    <AndroidCarCalloutContent
      hasBeforeUserEndSite={deps.hasBeforeUserEndSite}
      routeDistance={deps.routeDistance}
      durationDisplay={durationDisplay}
    />
  ), [deps.hasBeforeUserEndSite, deps.routeDistance, durationDisplay]);
  /**
   * 渲染等待态气泡。
   */
  const renderWaitingCallout = (item: any, calloutKey: string) => (
  const renderWaitingCallout = useCallback((item: any, calloutKey: string) => (
    <CoverView slot='callout' marker-id={item.id} key={calloutKey} className='waiting-callout'>
      <CoverView className='waiting-callout-content'>
        <CoverView className={`waiting-callout-txt ${!deps.isAndroid && 'pr-6'}`}>
@@ -175,17 +223,17 @@
      </CoverView>
      <CoverImage className='waiting-radar' src={deps.waitingRadarSrc} />
    </CoverView>
  );
  ), [deps.isAndroid, deps.waitingRadarSrc]);
  /**
   * 渲染默认气泡。
   */
  const renderDefaultCallout = (item: any, calloutKey: string) => (
  const renderDefaultCallout = useCallback((item: any, calloutKey: string) => (
    <CoverView slot='callout' marker-id={item.id} key={calloutKey} className='custom-callout'>
      {item.isRouteSummaryBubble ? (
        routeSummaryText ? (
          <CoverView className='route-summary-callout'>
            <CoverImage className='route-summary-callout-bg' src={summaryIcon} />
            <CoverImage className='route-summary-callout-bg' src={deps.routeSummaryBgSrc} />
            <CoverView className='route-summary-callout-text'>
              {routeSummaryText}
            </CoverView>
@@ -225,12 +273,23 @@
        </>
      )}
    </CoverView>
  );
  ), [
    deps.arrivedTooltipReady,
    deps.customCalloutIconSrc,
    deps.hasBeforeUserEndSite,
    deps.isAndroid,
    deps.routeDistance,
    deps.routeSummaryBgSrc,
    deps.showTrip1StyleEndpoint,
    deps.tripStatus,
    durationDisplay,
    routeSummaryText
  ]);
  /**
   * 渲染单个 marker 气泡。
   */
  const renderMarkerCallout = (item: any, index: number) => {
  const renderMarkerCallout = useCallback((item: any, index: number) => {
    const calloutKey = `callout-${item.id}-${item.name || ''}`;
    // 路线摘要气泡需跨平台强制显示,避免被 Android 普通气泡隐藏规则误伤
    if (item.isRouteSummaryBubble) {
@@ -241,12 +300,12 @@
    if (isWaiting) return renderWaitingCallout(item, calloutKey);
    if (item.name === '车辆位置' && deps.isAndroid && [TripStatus.VehicleApproaching, TripStatus.InService].includes(deps.tripStatus)) return null;
    return renderDefaultCallout(item, calloutKey);
  };
  }, [deps.isAndroid, deps.tripStatus, renderDefaultCallout, renderWaitingCallout, shouldHideAndroidMarkerCallout]);
  /**
   * 渲染地图覆盖层。
   */
  const renderMapOverlays = () => (
  const renderMapOverlays = useCallback(() => (
    <>
      {deps.markers.map(renderMarkerCallout)}
      {deps.shouldShowWaitingRadar && deps.waitingRadarVisible && (
@@ -254,7 +313,7 @@
      )}
      {deps.isAndroid && deps.routeSummaryCalloutVisible && routeSummaryText && (
        <View className="route-summary-callout" style={deps.routeSummaryCalloutStyle}>
          <Image className="route-summary-callout-bg" src={summaryIcon} />
          <Image className="route-summary-callout-bg" src={deps.routeSummaryBgSrc} />
          <View className="route-summary-callout-text">
            {routeSummaryText}
          </View>
@@ -262,17 +321,7 @@
      )}
      {deps.isAndroid && deps.carCalloutVisible && (
        <View className="car-callout-android" style={deps.carCalloutStyle}>
          <View className="custom-callout-content car-callout-with-before" style={{ padding: '6px 10px' }}>
            {deps.hasBeforeUserEndSite && (
              <View className="car-time car-before-order-tip">即将完成上一单</View>
            )}
            <View className="car-time" style={{ alignItems: 'flex-end' }}>
              <View className="car-time-num">{deps.routeDistance?.value ?? 0}</View>
              <View style={{ flexShrink: 0 }}>{deps.routeDistance?.unit === 'm' ? '米' : '公里'}</View>
              <View style={{ marginLeft: '2.5px' }} className="car-time-num">{durationDisplay.value}</View>
              <View style={{ flexShrink: 0 }}>{durationDisplay.unit}</View>
            </View>
          </View>
          {androidCarCalloutContent}
        </View>
      )}
      {deps.isAndroid && deps.arriveCalloutVisible && deps.arrivedTooltipReady && (
@@ -283,7 +332,25 @@
        </View>
      )}
    </>
  );
  ), [
    androidCarCalloutContent,
    deps.arriveCalloutStyle,
    deps.arriveCalloutVisible,
    deps.arrivedTooltipReady,
    deps.carCalloutStyle,
    deps.carCalloutVisible,
    deps.isAndroid,
    deps.markers,
    deps.routeSummaryBgSrc,
    deps.routeSummaryCalloutStyle,
    deps.routeSummaryCalloutVisible,
    deps.shouldShowWaitingRadar,
    deps.waitingRadarSrc,
    deps.waitingRadarStyle,
    deps.waitingRadarVisible,
    renderMarkerCallout,
    routeSummaryText
  ]);
  /**
   * 渲染底部行程卡片。
src/robotaxi/pages/begin-route/hooks/use-web-socket-events.ts
@@ -7,9 +7,13 @@
import { toTripStatus, TripStatus } from '../constants/trip-status';
import { normalizeSocketRouteSnapshot } from './runtime-sync';
import { MODIFY_DESTINATION_SOCKET_STATUS_KEY } from '../constants/modify-destination';
import { clearPassedRouteCache, writePassedRouteCache } from './passed-route-cache';
import { resolveVehiclePoint } from './trip-motion-utils';
type UseWebSocketEventsDeps = {
    tripStatusRef: React.MutableRefObject<TripStatus>;
    modifyDestinationPickupSuccessAtRef: React.MutableRefObject<number>;
    modifyDestinationPickupStatusGuardMs: number;
    orderId: string;
    setOrderId: (orderId: string) => void;
    setTripStatus: (status: TripStatus) => void;
@@ -33,6 +37,7 @@
    setShowCancelPopup: (value: boolean) => void;
    checkAndShowPassengerNotice: () => void;
    setVehicleDirection: (value: number) => void;
    lastSocketDirectionAtRef?: React.MutableRefObject<number>;
    setVehicleMoveAlongPoints: (points: any[]) => void;
    setRouteDistance: (value: any) => void;
    setRouteDuration: (value: number) => void;
@@ -43,6 +48,9 @@
        speedKmh: number | null;
    }) => void;
    setBeforeUserEndSite: (site: { longitude: number; latitude: number } | null) => void;
    isModifyDestinationReviewVisible?: boolean;
    hasLocalDestinationOverride?: boolean;
    updatePreviousRoutePoints?: (points: any[]) => void;
    erasePassedRouteSegments: (points: any[], servedPoints?: any[]) => void;
    applyRoutePoints: (points: any[]) => boolean;
    updateVehicleLocationThrottled: (lat: number, lng: number, force?: boolean) => void;
@@ -74,6 +82,7 @@
    deps.setRoutePoints([]);
    deps.setPolyline([]);
    deps.setVehicleMoveAlongPoints([]);
    clearPassedRouteCache();
    deps.polylineKeyRef.current = '';
    deps.lastPolylineUpdateAtRef.current = 0;
    deps.lastRoutePointRef.current = null;
@@ -93,6 +102,15 @@
    }
    const currentTripStatus = deps.tripStatusRef.current;
    const mappedTripStatus = toTripStatus(incomingTripStatus, currentTripStatus);
    const modifyDestinationPickupSuccessAt = Number(deps.modifyDestinationPickupSuccessAtRef.current || 0);
    const shouldGuardModifyDestinationPickupStatus = mappedTripStatus === TripStatus.InService
        && [TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup].includes(currentTripStatus)
        && modifyDestinationPickupSuccessAt > 0
        && Date.now() - modifyDestinationPickupSuccessAt <= deps.modifyDestinationPickupStatusGuardMs;
    if (shouldGuardModifyDestinationPickupStatus) {
        deps.modifyDestinationPickupSuccessAtRef.current = 0;
        return currentTripStatus;
    }
    // 行程进行关键态不允许被回退,避免 4/6 被延迟状态覆盖导致路线链路重置
    if ([TripStatus.VehicleApproaching, TripStatus.InService].includes(currentTripStatus) && mappedTripStatus < currentTripStatus) {
        return currentTripStatus;
@@ -198,6 +216,9 @@
const updateRouteSummary = (payload: any, deps: UseWebSocketEventsDeps) => {
    const normalizedDirection = resolveDirectionValue(payload);
    if (typeof normalizedDirection === 'number') {
        if (deps.lastSocketDirectionAtRef) {
            deps.lastSocketDirectionAtRef.current = Date.now();
        }
        deps.setVehicleDirection(normalizedDirection);
    }
    const route = safeParse(payload?.content?.route, {});
@@ -219,15 +240,48 @@
    });
};
const syncVehiclePointOnly = (routeSnapshot: ReturnType<typeof normalizeSocketRouteSnapshot>, deps: UseWebSocketEventsDeps) => {
    const targetPoint = resolveVehiclePoint(
        routeSnapshot.points,
        routeSnapshot.vehicleMoveAlongPoints,
        routeSnapshot.normalVehiclePointMode
    );
    if (!targetPoint) {
        return;
    }
    deps.updateVehicleLocationThrottled(targetPoint.latitude, targetPoint.longitude, true);
};
const handleOrderRoutePayload = (payload: any, deps: UseWebSocketEventsDeps) => {
    deps.setBeforeUserEndSite(parseBeforeUserEndSite(payload));
    const currentTripStatus = deps.tripStatusRef.current;
    updateRouteSummary(payload, deps);
    // socket 路线拦截只跟随真实的“修改目的地预览态”,不要被本地终点覆盖态误伤。
    const isModifyDestinationPreviewActive = Boolean(deps.isModifyDestinationReviewVisible);
    const shouldKeepModifyPreviewSummary = isModifyDestinationPreviewActive;
    if (!shouldKeepModifyPreviewSummary) {
        updateRouteSummary(payload, deps);
    }
    // 行程结束态仅更新概要信息,不再消费路线点,避免视口反复重算造成跳动
    if ([TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(currentTripStatus)) {
        return;
    }
    const routeSnapshot = normalizeSocketRouteSnapshot(payload);
    const shouldOnlySyncModifyPreviewVehicle = isModifyDestinationPreviewActive && currentTripStatus === TripStatus.InService;
    const shouldFreezeModifyPreviewRoute = isModifyDestinationPreviewActive && currentTripStatus !== TripStatus.InService;
    if (isModifyDestinationPreviewActive) {
        deps.updatePreviousRoutePoints?.(routeSnapshot.points || []);
    }
    if (shouldFreezeModifyPreviewRoute) {
        return;
    }
    if (shouldOnlySyncModifyPreviewVehicle) {
        syncVehiclePointOnly(routeSnapshot, deps);
        return;
    }
    const currentOrderId = String(deps.orderId || Taro.getStorageSync('orderId') || '');
    if (routeSnapshot.vehicleMoveAlongPoints.length > 1) {
        writePassedRouteCache(currentOrderId, routeSnapshot.vehicleMoveAlongPoints);
    }
    deps.applySmoothRouteSnapshot(routeSnapshot);
    if (routeSnapshot.forceFullRedraw && typeof deps.onOrderUpdateLineRendered === 'function') {
        Taro.nextTick(() => {
src/robotaxi/pages/begin-route/index.less
@@ -191,6 +191,9 @@
.car-callout-android {
    z-index: 9;
    pointer-events: none;
    will-change: transform;
    backface-visibility: hidden;
    transition: transform 0.18s linear;
}
.before-user-site-callout {
src/robotaxi/pages/begin-route/index.tsx
@@ -40,6 +40,7 @@
// 修改目的地流程编排
import useModifyDestinationFlow from './hooks/use-modify-destination-flow';
import useModifyDestinationPreviewRoute from './hooks/use-modify-destination-preview-route';
import usePreloadedMapAssets from './hooks/use-preloaded-map-assets';
// 蓝牙状态与弹窗编排
import useBluetoothAccess from './hooks/use-bluetooth-access';
// 蓝牙车控连接与鉴权编排
@@ -59,6 +60,7 @@
import { _ROBOTAXI_WS_HOST } from '../../../constants/api';
const ROUTE_SUMMARY_MARKER_ID = 999001;
const MODIFY_DESTINATION_PICKUP_STATUS_GUARD_MS = 5000;
type StatusRule = {
  showPickup: boolean;
  showDropoff: boolean;
@@ -134,9 +136,7 @@
  const customCalloutIconUrl = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/maker-tooltip-right.png'; // 自定义气泡图标URL
  const waitingCalloutBgUrl = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/maker-tooltip-bg.png'; // 等待气泡背景图URL
  const waitingRadarUrl = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/waiting-radar.webp'; // 等待雷达动画图URL
  const [customCalloutIconSrc, setCustomCalloutIconSrc] = useState(customCalloutIconUrl); // 自定义气泡图标源(本地缓存路径)
  const [waitingCalloutBgSrc, setWaitingCalloutBgSrc] = useState(waitingCalloutBgUrl); // 等待气泡背景图源
  const [waitingRadarSrc, setWaitingRadarSrc] = useState(waitingRadarUrl); // 等待雷达动画图源
  const routeSummaryBgUrl = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/summary-icon.png'; // 路线概览气泡背景图URL
  const [isRouteSelectExpanded, setIsRouteSelectExpanded] = useState(false); // 路线选择展开状态
  const [pageDidShowVersion, setPageDidShowVersion] = useState(0); // 页面展示版本号,用于触发子模块重查
  const [resetDetailVersion, setResetDetailVersion] = useState(0); // 重置详情版本号,用于触发组件更新
@@ -153,10 +153,13 @@
  });
  const [currentUserLocation, setCurrentUserLocation] = useState<CurrentUserLocation>(null); // 当前用户真实定位,避免被地图中心点覆盖
  const [modifyDestinationSocketStatus, setModifyDestinationSocketStatus] = useState<number | null>(null); // 后端推送的修改目的地展示状态
  const syncSmoothRouteBaselineRef = useRef<((snapshot: any) => void) | null>(null); // 订单路线接口回显后,同步初始化平滑擦线基线
  const lastSocketDirectionAtRef = useRef(0); // 最近一次由 socket 写入车辆朝向的时间戳
  const tripStatusRef = useRef(tripStatus); // 行程状态引用
  const lastTripStatusRef = useRef(tripStatus); // 上一次行程状态,用于检测状态变化
  const tripStatusForMapSyncRef = useRef(tripStatus); // 用于地图同步的行程状态引用
  const modifyDestinationPickupSuccessAtRef = useRef(0); // 接驾态修改目的地成功时间戳,用于短时忽略异常的行程中状态推送
  useEffect(() => {
    tripStatusRef.current = tripStatus;
    tripStatusForMapSyncRef.current = tripStatus;
@@ -204,6 +207,19 @@
  const shouldShowWaitingRadar = isAndroid && [TripStatus.Publishing, TripStatus.CallingCar].includes(tripStatus); // 是否显示等待雷达动画(Android 且处于发布中/呼叫车中)
  const vehicleMarkerId = !isAndroid && tripStatus === TripStatus.InService ? 30 : 3; // 车辆标记 ID,iOS 行驶中阶段使用不同 ID
  const {
    customCalloutIconSrc,
    waitingRadarSrc,
    routeSummaryBgSrc
  } = usePreloadedMapAssets({
    tripStatus,
    assetUrls: {
      customCalloutIconUrl,
      waitingCalloutBgUrl,
      waitingRadarUrl,
      routeSummaryBgUrl
    }
  }); // 地图气泡资源预加载
  const {
    openService,
    toCoupon,
    toPersonalCenter,
@@ -225,6 +241,7 @@
    previousEndSite,
    previousFareEstimate,
    previousRoutePoints,
    updatePreviousRoutePoints,
    isConfirmingModifyDestination,
    isModifyDestinationReviewVisible,
    hasLocalDestinationOverride,
@@ -238,20 +255,40 @@
    endSite,
    setEndSite,
    toSelectSite,
    onModifySuccess: (restoredRoutePoints?: any[]) => {
    onCancelReview: ({ previousEndSite: cancelledPreviousEndSite, previousFareEstimate: cancelledPreviousFareEstimate, previousRoutePoints: cancelledPreviousRoutePoints }) => {
      if (
        cancelledPreviousEndSite
        && Number.isFinite(Number(cancelledPreviousEndSite?.latitude))
        && Number.isFinite(Number(cancelledPreviousEndSite?.longitude))
      ) {
        Taro.setStorageSync('endSite', cancelledPreviousEndSite);
        setEndSite(cancelledPreviousEndSite);
      }
      setIsRouteSelectExpanded(false);
      setRouteOptions([]);
      setRoutePoints(Array.isArray(cancelledPreviousRoutePoints) ? cancelledPreviousRoutePoints : []);
      setFareEstimate(cancelledPreviousFareEstimate || {});
      if (orderId) {
        routeRequestOrderRoute(orderId, tripStatus);
      }
    },
    onModifySuccess: () => {
      const shouldGuardPickupStatus = [TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup].includes(tripStatus); // 接驾态修改成功后,短时保护页面继续按接驾态渲染
      modifyDestinationPickupSuccessAtRef.current = shouldGuardPickupStatus ? Date.now() : 0;
      // 成功修改后触发路线重新获取
      // 下一次渲染时 isModifyDestinationReviewVisible 变为 false
      // 我们需要强制让选路状态退出,同时获取后端的新行程路线
      setIsRouteSelectExpanded(false);
      setRouteOptions([]);
      setFareEstimate({});
      const shouldSkipOrderRouteSync = [TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup].includes(tripStatus); // 接驾阶段不拉取订单路线,避免误渲染行程中路线
      if (shouldSkipOrderRouteSync) {
        if (restoredRoutePoints && restoredRoutePoints.length > 0) {
          routeApplyRoutePoints(restoredRoutePoints);
        } else {
          setRoutePoints([]);
      const shouldSyncPickupRoute = [TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup].includes(tripStatus); // 接驾态修改成功后重新拉取接驾路线,避免复用预览前缓存路线
      if (shouldSyncPickupRoute) {
        const cachedStartSite = Taro.getStorageSync('startSite'); // 接驾态上车点与名称始终以缓存站点为准回显
        if (cachedStartSite?.latitude && cachedStartSite?.longitude) {
          setStartSite(cachedStartSite);
        }
        setRoutePoints([]);
        routeRequestOrderRoute(orderId, tripStatus);
        return;
      }
      setRoutePoints([]);
@@ -283,6 +320,7 @@
  }); // 车辆追踪状态容器
  const {
    vehicleLocation,
    vehicleMoveAlongPoints,
    vehicleDirection,
    vehicleInfo,
    trunkStatus,
@@ -306,9 +344,13 @@
    setEndSite,
    setBeforeUserEndSite,
    updateVehicleLocationThrottled: vehicleState.updateVehicleLocationThrottled,
    vehicleMoveAlongPoints,
    setVehicleDirection,
    setOrderRouteMetrics,
    setCurrentUserLocation
  }, {
    syncSmoothRouteBaselineRef,
    lastSocketDirectionAtRef
  }); // 路线与费用状态容器
  const {
    polyline,
@@ -346,8 +388,14 @@
    requestOrderRoute: routeRequestOrderRoute,
    updateWalkingRouteForStart: routeUpdateWalkingRouteForStart
  } = routeState; // 路线状态解构
  const shouldShowModifyDestinationPreviousRoute = tripStatus === TripStatus.InService; // 仅行程中修改目的地时展示旧目的地浅色对比路线
  const displayPolyline = useMemo(() => {
    if (!(isModifyDestinationReviewVisible || hasLocalDestinationOverride) || !Array.isArray(previousRoutePoints) || previousRoutePoints.length === 0) {
    if (
      !shouldShowModifyDestinationPreviousRoute
      || !(isModifyDestinationReviewVisible || hasLocalDestinationOverride)
      || !Array.isArray(previousRoutePoints)
      || previousRoutePoints.length === 0
    ) {
      return polyline;
    }
    return [
@@ -362,7 +410,7 @@
      },
      ...polyline
    ];
  }, [polyline, previousRoutePoints, isModifyDestinationReviewVisible, hasLocalDestinationOverride]);
  }, [polyline, previousRoutePoints, isModifyDestinationReviewVisible, hasLocalDestinationOverride, shouldShowModifyDestinationPreviousRoute]);
  const routeSummaryMarker = useMemo(() => {
    if (!allowSelectableRoutes || !routeDistance || routeDuration === null || !Array.isArray(routePoints) || routePoints.length === 0) {
      return null;
@@ -381,7 +429,7 @@
      alpha: 0,
      zIndex: 3,
      isRouteSummaryBubble: true,
      customCallout: { anchorY: -0.1, anchorX: 50, display: 'ALWAYS' }
      customCallout: { anchorY: 22, anchorX: 42, display: 'ALWAYS' }
    };
  }, [allowSelectableRoutes, routeDistance, routeDuration, routePoints]);
  const viewportState = useMapViewport({
@@ -411,14 +459,15 @@
    toOrigin,
    getLocationInfo,
    computeBottomPadding,
    resolveSafeTopInset
    resolveSafeTopInset,
    syncActiveTripViewportToVehicle
  } = viewportState; // 视口状态解构
  const activeTripScale = useActiveTripScale({
    tripStatus,
    routePoints,
    speedKmh: orderRouteMetrics?.speedKmh,
    mapScale,
    setMapScale: viewportState.setMapScale,
    syncActiveTripViewportToVehicle,
    computeBottomPadding,
    resolveSafeTopInset,
    holdDynamicScale,
@@ -540,6 +589,7 @@
    setTripStatus,
    setStartSite,
    setEndSite,
    setVehicleMoveAlongPoints,
    setTrunkStatus,
    setLatitude,
    setLongitude,
@@ -600,12 +650,6 @@
    lastErasedIndexRef,
    setShowCustomerService,
    vehicleInfo,
    waitingCalloutBgUrl,
    waitingRadarUrl,
    customCalloutIconUrl,
    setWaitingCalloutBgSrc,
    setWaitingRadarSrc,
    setCustomCalloutIconSrc,
    fareEstimate,
    routeRequestRouteAndFare,
    routeUpdateWalkingRouteForStart,
@@ -657,6 +701,13 @@
    updateVehicleLocationThrottled: vehicleState.updateVehicleLocationThrottled
  });
  useEffect(() => {
    syncSmoothRouteBaselineRef.current = applySmoothRouteSnapshot;
    return () => {
      syncSmoothRouteBaselineRef.current = null;
    };
  }, [applySmoothRouteSnapshot]);
  // 页面进入后优先拉取一遍下单配置,供轮询频率、步行按钮和重新报价时效复用。
  useEffect(() => {
    ensureDistanceConfig();
@@ -687,18 +738,24 @@
    setShowCancelPopup,
    checkAndShowPassengerNotice: tripRuntime.checkAndShowPassengerNotice,
    setVehicleDirection,
    lastSocketDirectionAtRef,
    setVehicleMoveAlongPoints,
    setRouteDistance,
    setRouteDuration,
    setModifyDestinationSocketStatus,
    setOrderRouteMetrics,
    setBeforeUserEndSite,
    isModifyDestinationReviewVisible,
    hasLocalDestinationOverride,
    updatePreviousRoutePoints,
    modifyDestinationPickupSuccessAtRef,
    erasePassedRouteSegments: routeErasePassedRouteSegments,
    applyRoutePoints: routeApplyRoutePoints,
    updateVehicleLocationThrottled: vehicleState.updateVehicleLocationThrottled,
    onOrderUpdateLineRendered: activeTripScale.handleOrderUpdateLineRendered,
    applySmoothRouteSnapshot,
    resetSmoothRouteMotion
    resetSmoothRouteMotion,
    modifyDestinationPickupStatusGuardMs: MODIFY_DESTINATION_PICKUP_STATUS_GUARD_MS
  });
  const orderActions = useOrderActions({
@@ -832,12 +889,8 @@
    setModifyDestinationFailMessage(message || '');
    setShowModifyDestinationFailPopup(true);
  };
  // 失败弹窗确认后退出修改目的地确认态,并恢复修改前路线
  const handleAcknowledgeModifyDestinationFail = () => {
    setShowModifyDestinationFailPopup(false);
    setModifyDestinationFailMessage('');
    const modifyDestinationSession = Taro.getStorageSync(MODIFY_DESTINATION_SESSION_KEY);
    const fallbackPreviousEndSite = modifyDestinationSession?.previousEndSite;
  // 修改目的地退出后统一恢复地图路线、上下车点与费用展示,确保“失败”和“取消”表现一致
  const restoreModifyDestinationDisplay = (fallbackPreviousEndSite: any, fallbackPreviousFareEstimate: any, fallbackPreviousRoutePoints: any[]) => {
    if (
      fallbackPreviousEndSite
      && Number.isFinite(Number(fallbackPreviousEndSite?.latitude))
@@ -849,11 +902,19 @@
    exitModifyDestinationReview();
    setIsRouteSelectExpanded(false);
    setRouteOptions([]);
    setRoutePoints(Array.isArray(previousRoutePoints) ? previousRoutePoints : []);
    setFareEstimate(previousFareEstimate || {});
    setRoutePoints(Array.isArray(fallbackPreviousRoutePoints) ? fallbackPreviousRoutePoints : []);
    setFareEstimate(fallbackPreviousFareEstimate || {});
    if (orderId) {
      routeRequestOrderRoute(orderId, tripStatus);
    }
  };
  // 失败弹窗确认后退出修改目的地确认态,并恢复修改前路线
  const handleAcknowledgeModifyDestinationFail = () => {
    setShowModifyDestinationFailPopup(false);
    setModifyDestinationFailMessage('');
    const modifyDestinationSession = Taro.getStorageSync(MODIFY_DESTINATION_SESSION_KEY);
    const fallbackPreviousEndSite = modifyDestinationSession?.previousEndSite;
    restoreModifyDestinationDisplay(fallbackPreviousEndSite, previousFareEstimate, previousRoutePoints);
  };
  // 确认修改前进行阈值校验,命中限制或接口失败都展示失败弹窗
  const handleConfirmModifyDestinationWithGuard = async (selectedRoute?: any) => {
@@ -1018,6 +1079,7 @@
    routeDuration,
    hasBeforeUserEndSite: !!beforeUserEndSite,
    customCalloutIconSrc,
    routeSummaryBgSrc,
    showTrip1StyleEndpoint,
    startSite,
    endSite: effectiveEndSite,
src/robotaxi/pages/personal-center/index.tsx
@@ -69,7 +69,7 @@
  const queryCouponCount = async () => {
    try {
      const res = await getUserouponNum();
      const nextCouponCount = res.data || 0;
      const nextCouponCount = res || 0;
      setCouponCount(nextCouponCount);
    } catch (error) {
      setCouponCount(0);