hejianhao
2026-05-18 4e398c65d0d822b8b1ea6c451b24aa347f4c179f
feat(robotaxi): 优化行程功能与修复多类交互问题

本次提交优化机器人出租车行程全链路功能,修复各类已知问题:
1. 样式与交互修复:调整车辆marker高度、修正折扣文案负号显示、优化收藏按钮hover交互、统一首页面板高度动画逻辑
2. 搜索与收藏优化:兼容编码格式的路由关键词参数、将收藏弹窗与校验的分页大小改为全量拉取
3. 路线模块重构:迁移路线交互状态至父组件、新增速度驱动的动态地图比例尺适配、优化路线选中与偏好匹配逻辑、优化车辆marker更新策略
4. 修改目的地流程完善:新增报价超时重估逻辑、修复会话缓存回滚失效问题
5. 端侧适配优化:新增Android端路线摘要气泡覆盖物、修复Android端地图气泡遮挡与手势冲突问题
6. 订单功能扩展:将再来一单支持范围扩展至待评价订单、优化首页订单同步与用户实时定位缓存逻辑
1个文件已添加
22个文件已修改
899 ■■■■■ 已修改文件
src/pages/robotaxi/home/components/home-content-panel.tsx 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/hooks/use-home-bootstrap.ts 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/hooks/use-nearby-vehicles.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/css-folder/release-route.less 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/modify-destination-review.tsx 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/release-route.tsx 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/route-selection-panel.tsx 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-active-trip-scale.ts 159 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-android-overlays.ts 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-map-markers.ts 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-map-viewport.ts 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-modify-destination-flow.ts 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-modify-destination-preview-route.ts 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-route-interaction.ts 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-route-manager.shared.ts 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-route-manager.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-route-request.ts 139 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/index.tsx 135 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/order/index.tsx 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/site-search/components/favorite-popup/index.tsx 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/site-search/components/site-list-item.tsx 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/site-search/index.tsx 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/components/home-content-panel.tsx
@@ -77,7 +77,7 @@
  return (
    <View
      className="content-box"
      style={{ height: props.bannerList.length > 1 ? `${props.boxHeight}px` : 'auto', transition: props.isDragging ? 'none' : 'height 0.3s ease-out' }}
      style={{ height:`${props.boxHeight}px`, transition: props.isDragging ? 'none' : 'height 0.3s ease-out' }}
      catchMove
      onTouchStart={(event) => {
        event.stopPropagation();
src/pages/robotaxi/home/hooks/use-home-bootstrap.ts
@@ -5,6 +5,14 @@
type HomeTripStatus = TripStatus | 0;
/** 首页登录与订单同步结果。 */
type AuthOrderSyncResult = {
  /** 当前是否已登录。 */
  hasLogin: boolean;
  /** 当前是否存在活跃订单。 */
  hasActiveOrder: boolean;
};
type Deps = {
  /** ws 服务地址 */
  wsHost: string;
@@ -80,12 +88,38 @@
  };
  /**
   * 将活跃订单起点映射为首页缓存站点。
   */
  const buildStartSiteByActiveOrder = (activeOrder: any) => ({
    id: activeOrder.startPointId || '',
    name: activeOrder.startPointName || '',
    latitude: activeOrder.startLat || 0,
    longitude: activeOrder.startLon || 0,
    electronicAreaId: activeOrder.xpElectronicFenceAreaId || '',
    time: activeOrder.startParkingTimeLimit || 0,
  });
  /**
   * 将活跃订单终点映射为首页缓存站点。
   */
  const buildEndSiteByActiveOrder = (activeOrder: any) => ({
    id: activeOrder.endPointId || '',
    name: activeOrder.endPointName || '',
    latitude: activeOrder.endLat || 0,
    longitude: activeOrder.endLon || 0,
    electronicAreaId: activeOrder.xpElectronicFenceAreaId || '',
    time: activeOrder.endParkingTimeLimit || 0,
  });
  /**
   * 同步登录、协议与活跃订单。
   */
  const syncAuthAndOrder = async () => {
    const otp = await deps.checkOtp(false);
    deps.setNeedLogin(!otp);
    if (!otp) return false;
    if (!otp) {
      return { hasLogin: false, hasActiveOrder: false } as AuthOrderSyncResult;
    }
    wsManager.connect(deps.wsHost);
    const agreement = await deps.getAgreement();
    if (agreement === false) {
@@ -99,37 +133,22 @@
    if (!activeOrder) {
      Taro.removeStorageSync('orderId');
      Taro.removeStorageSync('tripStatus');
      Taro.removeStorageSync('startSite');
      Taro.removeStorageSync('endSite');
      deps.setTripStatus(0);
      deps.setOrderData({});
      return true;
      return { hasLogin: true, hasActiveOrder: false } as AuthOrderSyncResult;
    }
    deps.setOrderData(activeOrder || {});
    const mappedStatus = toHomeTripStatus(mapOrderStatusToTripStatus(activeOrder.status || 0));
    const startSite = buildStartSiteByActiveOrder(activeOrder);
    const endSite = buildEndSiteByActiveOrder(activeOrder);
    deps.setTripStatus(mappedStatus);
    Taro.setStorageSync('tripStatus', mappedStatus);
    Taro.setStorageSync('orderId', activeOrder.id || '');
    if (!Taro.getStorageSync('startSite')) {
      Taro.setStorageSync('startSite', {
        id: activeOrder.startPointId || '',
        name: activeOrder.startPointName || '',
        latitude: activeOrder.startLat || 0,
        longitude: activeOrder.startLon || 0,
        electronicAreaId: activeOrder.xpElectronicFenceAreaId || '',
        time: activeOrder.startParkingTimeLimit || 0,
      });
    }
    if (!Taro.getStorageSync('endSite')) {
      Taro.setStorageSync('endSite', {
        id: activeOrder.endPointId || '',
        name: activeOrder.endPointName || '',
        latitude: activeOrder.endLat || 0,
        longitude: activeOrder.endLon || 0,
        electronicAreaId: activeOrder.xpElectronicFenceAreaId || '',
        time: activeOrder.endParkingTimeLimit || 0,
      });
    }
    return true;
    Taro.setStorageSync('startSite', startSite);
    Taro.setStorageSync('endSite', endSite);
    return { hasLogin: true, hasActiveOrder: true } as AuthOrderSyncResult;
  };
  /**
@@ -228,11 +247,17 @@
    Taro.getSetting({
      success: (res) => {
        const auth = res.authSetting['scope.userLocation'];
        const startSite = Taro.getStorageSync('startSite');
        processLocationSetting(auth, startSite);
        Promise.resolve(authSyncTask)
          .then((syncResult) => {
            const startSite = syncResult.hasActiveOrder ? Taro.getStorageSync('startSite') : null;
            processLocationSetting(auth, startSite);
          })
          .catch(() => {
            processLocationSetting(auth, null);
          });
      }
    });
    return authSyncTask;
    return authSyncTask.then((syncResult) => syncResult.hasLogin);
  };
  return {
src/pages/robotaxi/home/hooks/use-nearby-vehicles.ts
@@ -79,7 +79,7 @@
    longitude: vehicle.longitude,
    rotate: vehicle.direction,
    width: 20,
    height: 20,
    height: 40,
    zIndex: 6,
    anchor: { x: 0.5, y: 0.5 },
    iconPath: VEHICLE_MARKER_ICON
src/robotaxi/pages/begin-route/components/css-folder/release-route.less
@@ -298,8 +298,6 @@
        z-index: 11;
        width: 100%;
        box-sizing: border-box;
        padding: 24px 38px 62px 32px;
        background: #FEFEFF;
        .discount-amount {
            display: flex;
@@ -308,7 +306,13 @@
            padding: 8px 32px 8px 24px;
            background: #FFF3E6;
            .flex-center {
                display: flex;
                align-items: center;
            }
            .discount-amount-text {
                flex-shrink: 0;
                border-radius: 6px;
                background: rgba(245, 137, 0, 0.06);
                box-sizing: border-box;
@@ -339,6 +343,7 @@
            }
            .change-coupon-icon {
                flex-shrink: 0;
                width: 24px;
                height: 24px;
            }
@@ -348,6 +353,8 @@
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 24px 38px 62px 32px;
            background: #FEFEFF;
            .money-box {
src/robotaxi/pages/begin-route/components/modify-destination-review.tsx
@@ -228,7 +228,7 @@
                )}
                {isOperateFree ? null : fareEstimate.discountAmount > 0 &&
                  <View className='modify-destination-review-fare-discount'>
                    已优惠<Text className='modify-destination-review-fare-discount-amount'>{-fareEstimate.discountAmount}元</Text>
                    已优惠<Text className='modify-destination-review-fare-discount-amount'>{fareEstimate.discountAmount}元</Text>
                  </View>
                }
              </View>
src/robotaxi/pages/begin-route/components/release-route.tsx
@@ -229,11 +229,11 @@
                    {
                        isOperateFree ? null : fareEstimate.discountAmount > 0 &&
                            <View className="discount-amount" onClick={onOpenCouponSelect}>
                                <View>
                                <View className="flex-center">
                                    <View className="discount-amount-text">最佳优惠</View>
                                    <View className="discount-amount-desc" >已为您选择最佳优惠</View>
                                </View>
                                <View>
                                <View className="flex-center">
                                    <View className="discount-amount-num">{fareEstimate.discountAmount}元</View>
                                    <Image className="change-coupon-icon" src={changeCouponIcon} />
                                </View>
src/robotaxi/pages/begin-route/components/route-selection-panel.tsx
@@ -11,8 +11,16 @@
type Props = {
  routeOptions?: any[];
  selectedRouteIndex?: number;
  handleSelectRoute?: (index: number) => void;
  handleSelectRoute?: (index: number, options?: { syncSites?: boolean; syncViewport?: boolean }) => void;
};
/**
 * 根据路线规则匹配偏好项。
 */
const findPreferenceByRouteRule = (
  options: RoutePreferenceOption[],
  routeRule: string | number | null | undefined
) => options.find((item) => String(item?.value ?? '') === String(routeRule ?? '')) || null;
export default function RouteSelectionPanel({
  routeOptions = [],
@@ -56,18 +64,34 @@
  }, [openCityId]);
  /**
   * 初始化回显:优先 selected=true,否则取第一个偏好,并按策略值匹配默认选中路线。
   * 初始化回显:
   * 1. 有 selected=true 时,优先按该偏好匹配路线;
   * 2. 没有 selected=true 时,优先回显第一个路线,并反向匹配对应偏好。
   */
  useEffect(() => {
    if (hasAppliedDefaultSelectionRef.current) return;
    if (!preferenceOptions.length) return;
    const defaultOption = preferenceOptions.find(item => item.selected === true) || preferenceOptions[0];
    if (!defaultOption) return;
    setPreference(defaultOption);
    if (!routeOptions.length || !handleSelectRoute) return;
    const targetIndex = routeOptions.findIndex(route => String(route?.primaryRoutePlanRule ?? '') === String(defaultOption.value));
    if (targetIndex !== -1 && targetIndex !== selectedRouteIndex) {
      handleSelectRoute(targetIndex);
    const selectedPreferenceOption = preferenceOptions.find((item) => item.selected === true) || null;
    if (selectedPreferenceOption) {
      setPreference(selectedPreferenceOption);
      if (!routeOptions.length || !handleSelectRoute) return;
      const targetIndex = routeOptions.findIndex((route) => (
        String(route?.primaryRoutePlanRule ?? '') === String(selectedPreferenceOption.value)
      ));
      if (targetIndex !== -1 && targetIndex !== selectedRouteIndex) {
        handleSelectRoute(targetIndex, { syncSites: false, syncViewport: false });
      }
      hasAppliedDefaultSelectionRef.current = true;
      return;
    }
    const firstRoute = routeOptions[0];
    if (!firstRoute) return;
    const matchedPreference = findPreferenceByRouteRule(preferenceOptions, firstRoute?.primaryRoutePlanRule);
    setPreference(matchedPreference || DEFAULT_PREFERENCE);
    if (handleSelectRoute && selectedRouteIndex !== 0) {
      handleSelectRoute(0, { syncSites: false, syncViewport: false });
    }
    hasAppliedDefaultSelectionRef.current = true;
  }, [preferenceOptions, routeOptions, handleSelectRoute, selectedRouteIndex]);
@@ -83,7 +107,7 @@
      });
      if (targetIndex !== -1) {
        handleSelectRoute(targetIndex);
        handleSelectRoute(targetIndex, { syncSites: false });
      }
    }
@@ -113,7 +137,7 @@
            <View
              className={`route-select-content-item ${isSelected ? 'active-item' : ''}`}
              key={index}
              onClick={() => handleSelectRoute && handleSelectRoute(index)}
              onClick={() => handleSelectRoute && handleSelectRoute(index, { syncSites: false })}
            >
              <View className='route-select-content-item-title'>{item?.primaryStrategyLabel}</View>
              <View className='route-select-content-item-time'>{time.value}{time.unit}</View>
src/robotaxi/pages/begin-route/hooks/use-active-trip-scale.ts
New file
@@ -0,0 +1,159 @@
import Taro from '@tarojs/taro';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { TripStatus } from '../constants/trip-status';
type RoutePoint = {
  latitude: number;
  longitude: number;
};
type Deps = {
  tripStatus: TripStatus;
  routePoints: RoutePoint[];
  speedKmh: number | null;
  mapScale: number;
  setMapScale: (value: number) => void;
  computeBottomPadding: (points: any[], viewportType: string) => number;
  resolveSafeTopInset: () => number;
  holdDynamicScale: (durationMs?: number) => void;
  getDynamicScaleHoldUntil: () => number;
};
const ACTIVE_TRIP_STATUSES = [TripStatus.VehicleApproaching, TripStatus.InService]; // 启用速度缩放的行程状态
const USER_GESTURE_CAUSED_BY = new Set(['gesture', 'drag', 'scale']); // 需要暂停速度缩放的用户手势来源
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 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 顶部安全区附加留白
/**
 * 将数值限制在给定区间内。
 */
const clamp = (value: number, min: number, max: number) => {
  if (value < min) return min;
  if (value > max) return max;
  return value;
};
/**
 * 将速度换算成目标比例尺,速度越高视角越远。
 */
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;
};
/**
 * 接驾中/行程中动态缩放:由速度驱动比例尺,并保留 ORDER_UPDATE_LINE 的一次性全路线入屏。
 */
export default function useActiveTripScale(deps: Deps) {
  const [resumeVersion, setResumeVersion] = useState(0); // 冷却结束后触发重新评估
  const resumeTimerRef = useRef<any>(null);
  const isActiveStatus = useMemo(() => {
    return ACTIVE_TRIP_STATUSES.includes(deps.tripStatus);
  }, [deps.tripStatus]);
  /**
   * 清理冷却恢复定时器。
   */
  const clearResumeTimer = () => {
    if (!resumeTimerRef.current) {
      return;
    }
    clearTimeout(resumeTimerRef.current);
    resumeTimerRef.current = null;
  };
  /**
   * 在冷却结束后触发一次动态缩放重算。
   */
  const scheduleResume = useCallback((delayMs: number) => {
    clearResumeTimer();
    const safeDelayMs = Math.max(0, Math.ceil(delayMs));
    resumeTimerRef.current = setTimeout(() => {
      setResumeVersion((value) => value + 1);
      resumeTimerRef.current = null;
    }, safeDelayMs);
  }, []);
  /**
   * 处理地图手势,手动缩放/拖拽后短暂暂停速度接管。
   */
  const handleRegionChange = useCallback((event: any) => {
    const causedBy = String(event?.causedBy || event?.detail?.causedBy || '').toLowerCase();
    if (!USER_GESTURE_CAUSED_BY.has(causedBy)) {
      return;
    }
    deps.holdDynamicScale(USER_GESTURE_HOLD_MS);
    scheduleResume(USER_GESTURE_HOLD_MS + SCALE_RESUME_BUFFER_MS);
  }, [deps, scheduleResume]);
  /**
   * ORDER_UPDATE_LINE 渲染完成后,自动执行一次全路线入屏。
   */
  const handleOrderUpdateLineRendered = useCallback((points: RoutePoint[]) => {
    if (!isActiveStatus) {
      return;
    }
    const safePoints = Array.isArray(points) ? points : [];
    if (safePoints.length < 2) {
      return;
    }
    const mapCtx = Taro.createMapContext('map');
    const bottomPadding = deps.computeBottomPadding(safePoints, 'route');
    const topPadding = Math.max(ORDER_UPDATE_LINE_TOP_PADDING, deps.resolveSafeTopInset() + ORDER_UPDATE_LINE_TOP_EXTRA_PADDING);
    deps.holdDynamicScale(ORDER_UPDATE_LINE_HOLD_MS);
    Taro.nextTick(() => {
      setTimeout(() => {
        mapCtx.includePoints({
          points: safePoints,
          padding: [topPadding, ORDER_UPDATE_LINE_SIDE_PADDING, bottomPadding, ORDER_UPDATE_LINE_SIDE_PADDING]
        });
      }, 0);
    });
    scheduleResume(ORDER_UPDATE_LINE_HOLD_MS + SCALE_RESUME_BUFFER_MS);
  }, [deps, isActiveStatus, scheduleResume]);
  useEffect(() => {
    if (!isActiveStatus) {
      clearResumeTimer();
      return;
    }
    const speedKmh = Number(deps.speedKmh);
    if (!Number.isFinite(speedKmh)) {
      return;
    }
    const holdUntil = Number(deps.getDynamicScaleHoldUntil());
    const now = Date.now();
    if (Number.isFinite(holdUntil) && holdUntil > now) {
      scheduleResume(holdUntil - now + SCALE_RESUME_BUFFER_MS);
      return;
    }
    const targetScale = resolveScaleBySpeed(speedKmh);
    if (Math.abs(targetScale - deps.mapScale) < MIN_SCALE_CHANGE) {
      return;
    }
    deps.setMapScale(targetScale);
  }, [deps, isActiveStatus, resumeVersion, scheduleResume]);
  useEffect(() => {
    return () => {
      clearResumeTimer();
    };
  }, []);
  return {
    handleRegionChange,
    handleOrderUpdateLineRendered
  };
}
src/robotaxi/pages/begin-route/hooks/use-android-overlays.ts
@@ -7,6 +7,7 @@
  isAndroid: boolean;
  tripStatus: TripStatus;
  mapScale: number;
  routeSummaryLocation: { latitude: number; longitude: number } | null;
  startSite: any;
  endSite: any;
  vehicleLocation: { latitude: number; longitude: number } | null;
@@ -24,6 +25,9 @@
  // 到达气泡样式与显示状态
  const [arriveCalloutStyle, setArriveCalloutStyle] = useState<any>({});
  const [arriveCalloutVisible, setArriveCalloutVisible] = useState(false);
  // 路线摘要气泡样式与显示状态
  const [routeSummaryCalloutStyle, setRouteSummaryCalloutStyle] = useState<any>({});
  const [routeSummaryCalloutVisible, setRouteSummaryCalloutVisible] = useState(false);
  const mapRectRef = useRef<any>(null);
  // 经纬度转屏幕坐标
@@ -125,12 +129,34 @@
    setArriveCalloutVisible(true);
  };
  // 更新路线摘要气泡位置
  const updateRouteSummaryCalloutPosition = async () => {
    if (!deps.isAndroid || !deps.routeSummaryLocation) {
      setRouteSummaryCalloutVisible(false);
      return;
    }
    const rect = await ensureMapRect();
    const pos = await computeScreenPosition(deps.routeSummaryLocation, rect, 0);
    if (!pos) {
      setRouteSummaryCalloutVisible(false);
      return;
    }
    setRouteSummaryCalloutStyle({
      position: 'fixed',
      left: pos.left,
      top: pos.top,
      transform: 'translate(-50%, -100%)'
    });
    setRouteSummaryCalloutVisible(true);
  };
  // 地图区域变化时同步覆盖物显隐与重算
  const handleRegionChange = (e: any) => {
    if (e?.type === 'begin') {
      setWaitingRadarVisible(false);
      setCarCalloutVisible(false);
      setArriveCalloutVisible(false);
      setRouteSummaryCalloutVisible(false);
      return;
    }
    if (e?.type === 'end') {
@@ -138,6 +164,7 @@
      updateWaitingRadarPosition();
      updateCarCalloutPosition();
      updateArriveCalloutPosition();
      updateRouteSummaryCalloutPosition();
    }
  };
@@ -152,6 +179,10 @@
  useEffect(() => {
    updateArriveCalloutPosition();
  }, [deps.isAndroid, deps.tripStatus, deps.endSite?.latitude, deps.endSite?.longitude]);
  useEffect(() => {
    updateRouteSummaryCalloutPosition();
  }, [deps.isAndroid, deps.routeSummaryLocation?.latitude, deps.routeSummaryLocation?.longitude, deps.mapScale]);
  useEffect(() => {
    if (!deps.isAndroid || deps.tripStatus !== TripStatus.ArrivedDestination) return;
@@ -169,9 +200,12 @@
    carCalloutVisible,
    arriveCalloutStyle,
    arriveCalloutVisible,
    routeSummaryCalloutStyle,
    routeSummaryCalloutVisible,
    updateWaitingRadarPosition,
    updateCarCalloutPosition,
    updateArriveCalloutPosition,
    updateRouteSummaryCalloutPosition,
    handleRegionChange
  };
}
src/robotaxi/pages/begin-route/hooks/use-map-markers.ts
@@ -44,6 +44,10 @@
  const carMarkerInitialized = useRef(false);
  const tripStatus6MarkerRefreshedRef = useRef(false);
  // 行驶中/接驾中车辆 marker 统一交由 translateMarker 托管位置与角度
  const shouldUsePureTranslateCarMarker = [TripStatus.VehicleApproaching, TripStatus.InService].includes(deps.tripStatus)
    && deps.statusRule.showCar;
  // 构建上车点 marker
  const createPickupMarker = (pickupSite: any, useAndroidNativeTrip1Callout: boolean) => {
    if (!(pickupSite?.latitude && pickupSite?.longitude)) return null;
@@ -154,10 +158,18 @@
  // 应用 marker 状态并处理“行驶中”阶段的刷新标记
  const applyMarkerState = (nextMarkers: any[], nextMarkersSignature: string, shouldSkipUpdate: boolean, carMarker: any) => {
    if (shouldSkipUpdate) {
      // 跳过整包 markers 更新时,仍兜底同步车辆 marker 位置与角度,避免朝向卡旧值
      // 平移态下保留已创建车辆 marker 的当前位置和角度,只同步其他 marker 结构变化
      setMarkers((prevMarkers: any[]) => {
        const targetCarIndex = prevMarkers.findIndex((item: any) => item.id === deps.vehicleMarkerId);
        if (targetCarIndex === -1) {
        const nextCarIndex = nextMarkers.findIndex((item: any) => item.id === deps.vehicleMarkerId);
        if (nextCarIndex === -1) {
          if (markersSignatureRef.current !== nextMarkersSignature) {
            markersSignatureRef.current = nextMarkersSignature;
            return nextMarkers;
          }
          return prevMarkers;
        }
        if (targetCarIndex === -1 || !shouldUsePureTranslateCarMarker) {
          if (markersSignatureRef.current !== nextMarkersSignature) {
            markersSignatureRef.current = nextMarkersSignature;
            return nextMarkers;
@@ -165,22 +177,18 @@
          return prevMarkers;
        }
        const prevCar = prevMarkers[targetCarIndex] || {};
        const nextCarRotate = typeof carMarker?.rotate === 'number' ? carMarker.rotate : prevCar.rotate;
        const carLocationChanged =
          Number(prevCar.latitude) !== Number(carMarker?.latitude) ||
          Number(prevCar.longitude) !== Number(carMarker?.longitude);
        const carRotateChanged = Number(prevCar.rotate) !== Number(nextCarRotate);
        if (!carLocationChanged && !carRotateChanged) {
        if (markersSignatureRef.current === nextMarkersSignature) {
          return prevMarkers;
        }
        const next = [...prevMarkers];
        next[targetCarIndex] = {
          ...prevCar,
          latitude: carMarker?.latitude,
          longitude: carMarker?.longitude,
          rotate: nextCarRotate
        markersSignatureRef.current = nextMarkersSignature;
        const mergedMarkers = [...nextMarkers];
        mergedMarkers[nextCarIndex] = {
          ...mergedMarkers[nextCarIndex],
          latitude: prevCar.latitude,
          longitude: prevCar.longitude,
          rotate: prevCar.rotate
        };
        return next;
        return mergedMarkers;
      });
    }
    if (!shouldSkipUpdate && markersSignatureRef.current !== nextMarkersSignature) {
@@ -213,14 +221,17 @@
    if (deps.statusRule.showDropoff && dropoffMarker) nextMarkers.push(dropoffMarker);
    if (beforeUserDropoffMarker) nextMarkers.push(beforeUserDropoffMarker);
    const nextMarkersSignature = nextMarkers
      .map((m: any) => [
        m.id,
        Number(m.latitude).toFixed(6),
        Number(m.longitude).toFixed(6),
        m.name || '',
        typeof m.rotate === 'number' ? m.rotate : '',
        m?.customCallout?.anchorY ?? '',
      ].join('|'))
      .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 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);
src/robotaxi/pages/begin-route/hooks/use-map-viewport.ts
@@ -32,6 +32,7 @@
};
const DROPOFF_MAX_SCALE = 20;
const DEFAULT_DYNAMIC_SCALE_HOLD_MS = 2500; // 一次性视口调整后,暂停速度缩放的默认冷却时长
export default function useMapViewport(rawDeps?: Partial<Deps>) {
    const deps = { ...defaultDeps, ...(rawDeps || {}) } as Deps;
@@ -54,6 +55,7 @@
    const tripStatus6ViewportLockedRef = useRef(false);
    const orderDetailEnterSignatureRef = useRef('');
    const orderDetailRouteUpdatedAfterEnterRef = useRef(false);
    const dynamicScaleHoldUntilRef = useRef(0); // 速度缩放暂停截止时间
    const isAndroid = useMemo(() => {
        try {
@@ -62,6 +64,21 @@
        } catch (error) {
            return false;
        }
    }, []);
    /**
     * 暂停速度驱动缩放,避免一次性视口调整后立刻被比例尺逻辑抢回。
     */
    const holdDynamicScale = useCallback((durationMs: number = DEFAULT_DYNAMIC_SCALE_HOLD_MS) => {
        const safeDurationMs = Math.max(0, Number(durationMs) || 0);
        dynamicScaleHoldUntilRef.current = Date.now() + safeDurationMs;
    }, []);
    /**
     * 获取速度缩放暂停截止时间。
     */
    const getDynamicScaleHoldUntil = useCallback(() => {
        return dynamicScaleHoldUntilRef.current;
    }, []);
    // 获取底部安全区高度
@@ -211,6 +228,7 @@
        const padding = viewportType === 'order_detail'
            ? [topPadding, 50, bottomPadding, 50]
            : [topPadding, 100, bottomPadding, 100];
        holdDynamicScale();
        Taro.nextTick(() => {
            if (viewportType === 'dropoff') {
                mapCtx.moveToLocation({
@@ -232,6 +250,7 @@
        if (deps.tripStatus === TripStatus.InService && deps.routePoints.length > 0) {
            const bottomPadding = computeBottomPadding(deps.routePoints, 'route');
            const topPadding = Math.max(24, resolveSafeTopInset() + 32);
            holdDynamicScale();
            mapCtx.includePoints({
                points: deps.routePoints,
                padding: [topPadding, 100, bottomPadding, 100]
@@ -239,6 +258,7 @@
            tripStatus6ViewportLockedRef.current = true;
            return;
        }
        holdDynamicScale();
        mapCtx.moveToLocation();
    };
@@ -253,6 +273,7 @@
                if (deps.tripStatus === TripStatus.InService && deps.routePoints.length > 0) {
                    const bottomPadding = computeBottomPadding(deps.routePoints, 'route');
                    const topPadding = Math.max(24, resolveSafeTopInset() + 32);
                    holdDynamicScale();
                    Taro.createMapContext('map').includePoints({
                        points: deps.routePoints,
                        padding: [topPadding, 100, bottomPadding, 100]
@@ -260,6 +281,7 @@
                    tripStatus6ViewportLockedRef.current = true;
                    return;
                }
                holdDynamicScale();
                Taro.createMapContext('map').moveToLocation({
                    latitude: res.latitude,
                    longitude: res.longitude
@@ -381,6 +403,8 @@
        resolveSafeBottomInset,
        resolveSafeTopInset,
        computeBottomPadding,
        holdDynamicScale,
        getDynamicScaleHoldUntil,
        updateMapViewportPadding,
        toOrigin,
        getLocationInfo
src/robotaxi/pages/begin-route/hooks/use-modify-destination-flow.ts
@@ -168,12 +168,28 @@
    Taro.removeStorageSync(MODIFY_DESTINATION_OVERRIDE_KEY);
  };
  /**
   * 解析修改目的地流程中的“原始目的地”,优先使用会话缓存,避免内存态丢失导致回滚失败。
   */
  const resolvePreviousEndSite = () => {
    const session = Taro.getStorageSync(MODIFY_DESTINATION_SESSION_KEY);
    const cachedPreviousEndSite = session?.previousEndSite;
    if (isValidSite(cachedPreviousEndSite)) {
      return cachedPreviousEndSite;
    }
    if (isValidSite(previousEndSite)) {
      return previousEndSite;
    }
    return null;
  };
  // 退出修改目的地确认态并按需恢复修改前终点
  const exitModifyDestinationReview = (options: ExitModifyDestinationReviewOptions = {}) => {
    const { shouldRestorePreviousEndSite = true } = options;
    if (shouldRestorePreviousEndSite && isValidSite(previousEndSite) && !isSameSite(previousEndSite, deps.endSite)) {
      Taro.setStorageSync('endSite', previousEndSite);
      deps.setEndSite(previousEndSite);
    const nextPreviousEndSite = resolvePreviousEndSite();
    if (shouldRestorePreviousEndSite && nextPreviousEndSite && !isSameSite(nextPreviousEndSite, deps.endSite)) {
      Taro.setStorageSync('endSite', nextPreviousEndSite);
      deps.setEndSite(nextPreviousEndSite);
    }
    setPreviewEndSite(null);
    setPreviousEndSite(null);
src/robotaxi/pages/begin-route/hooks/use-modify-destination-preview-route.ts
@@ -1,5 +1,5 @@
import Taro from '@tarojs/taro';
import { useEffect, useRef } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { TripStatus } from '../constants/trip-status';
type RequestRouteOptions = {
@@ -89,18 +89,20 @@
 */
export default function useModifyDestinationPreviewRoute(deps: Deps) {
  const previewRouteKeyRef = useRef(''); // 预览路线请求签名,避免重复请求
  const requoteTriggeredRef = useRef(false); // 确认态 60 秒重估价触发标记
  const [pendingConfirmRequote, setPendingConfirmRequote] = useState(false); // 确认态停留超过 60 秒后,点击确认前需要先重估价
  useEffect(() => {
    const routeContext = resolveModifyDestinationRouteContext(deps);
    if (!routeContext) {
      previewRouteKeyRef.current = '';
      setPendingConfirmRequote(false);
      return;
    }
    if (routeContext.requestKey === previewRouteKeyRef.current) {
      return;
    }
    previewRouteKeyRef.current = routeContext.requestKey;
    setPendingConfirmRequote(false);
    deps.requestRouteAndFare(
      routeContext.originLat,
      routeContext.originLng,
@@ -127,30 +129,12 @@
  useEffect(() => {
    if (!deps.isModifyDestinationReviewVisible) {
      requoteTriggeredRef.current = false;
      setPendingConfirmRequote(false);
      return;
    }
    requoteTriggeredRef.current = false;
    const timer = setTimeout(() => {
      if (requoteTriggeredRef.current) {
        return;
      }
      const routeContext = resolveModifyDestinationRouteContext(deps);
      if (!routeContext) {
        return;
      }
      requoteTriggeredRef.current = true;
      deps.requestRouteAndFare(
        routeContext.originLat,
        routeContext.originLng,
        routeContext.destLat,
        routeContext.destLng,
        {
          startPointId: routeContext.startPointId,
          force: true
        }
      );
      setPendingConfirmRequote(true);
    }, MODIFY_DESTINATION_REQUOTE_EXPIRE_SECONDS * 1000);
    return () => {
@@ -168,4 +152,39 @@
    deps.vehicleLocation?.latitude,
    deps.vehicleLocation?.longitude
  ]);
  /**
   * 确认修改前消费 60 秒超时标记:先重估价一次,并要求用户再次确认。
   */
  const handleConfirmRequoteIfNeeded = useCallback(() => {
    if (!pendingConfirmRequote) {
      return false;
    }
    const routeContext = resolveModifyDestinationRouteContext(deps);
    if (!routeContext) {
      setPendingConfirmRequote(false);
      return false;
    }
    setPendingConfirmRequote(false);
    deps.requestRouteAndFare(
      routeContext.originLat,
      routeContext.originLng,
      routeContext.destLat,
      routeContext.destLng,
      {
        startPointId: routeContext.startPointId,
        force: true
      }
    );
    Taro.showToast({
      title: '报价已更新,请再次确认修改',
      icon: 'none'
    });
    return true;
  }, [deps, pendingConfirmRequote]);
  return {
    pendingConfirmRequote,
    handleConfirmRequoteIfNeeded
  };
}
src/robotaxi/pages/begin-route/hooks/use-route-interaction.ts
@@ -1,5 +1,4 @@
import Taro from '@tarojs/taro';
import { useState } from 'react';
import { getDistance } from '../../../../utils/Robotaxi-public-method';
import { normalizeDistanceKm, normalizeDurationSeconds } from '../../../utils/format';
import { TripStatus } from '../constants/trip-status';
@@ -19,6 +18,18 @@
type UseRouteInteractionParams = {
  /** 路线管理依赖。 */
  deps: Deps;
  /** 当前路线选项集合。 */
  routeOptions: any[];
  /** 设置路线选项集合。 */
  setRouteOptions: React.Dispatch<React.SetStateAction<any[]>>;
  /** 当前选择的路线索引。 */
  selectedRouteIndex: number;
  /** 设置当前选择的路线索引。 */
  setSelectedRouteIndex: React.Dispatch<React.SetStateAction<number>>;
  /** 所有路线点集合。 */
  allRoutePoints: any[][];
  /** 设置所有路线点集合。 */
  setAllRoutePoints: React.Dispatch<React.SetStateAction<any[][]>>;
  /** 当前步行路线点。 */
  walkingPoints: any[];
  /** 应用主路线点。 */
@@ -31,6 +42,14 @@
  setRouteDistance: React.Dispatch<React.SetStateAction<RouteDistanceValue>>;
  /** 设置路线时长。 */
  setRouteDuration: React.Dispatch<React.SetStateAction<number | null>>;
};
/** 路线交互 Hook 返回值。 */
type HandleSelectRouteOptions = {
  /** 是否同步更新起终点。 */
  syncSites?: boolean;
  /** 是否同步更新地图视口。 */
  syncViewport?: boolean;
};
/** 路线交互 Hook 返回值。 */
@@ -48,7 +67,7 @@
  /** 设置所有路线点集合。 */
  setAllRoutePoints: React.Dispatch<React.SetStateAction<any[][]>>;
  /** 选择路线。 */
  handleSelectRoute: (index: number) => void;
  handleSelectRoute: (index: number, options?: HandleSelectRouteOptions) => void;
  /** 处理 polyline 点击。 */
  handlePolylineTap: (event: any) => void;
  /** 处理地图点击。 */
@@ -68,6 +87,12 @@
export default function useRouteInteraction(params: UseRouteInteractionParams): UseRouteInteractionResult {
  const {
    deps,
    routeOptions,
    setRouteOptions,
    selectedRouteIndex,
    setSelectedRouteIndex,
    allRoutePoints,
    setAllRoutePoints,
    walkingPoints,
    applyRoutePoints,
    reducePoints,
@@ -75,13 +100,6 @@
    setRouteDistance,
    setRouteDuration,
  } = params;
  /** 路线选项集合。 */
  const [routeOptions, setRouteOptions] = useState<any[]>([]);
  /** 当前选择的路线索引。 */
  const [selectedRouteIndex, setSelectedRouteIndex] = useState<number>(0);
  /** 所有路线点集合。 */
  const [allRoutePoints, setAllRoutePoints] = useState<any[][]>([]);
  /**
   * 同步地图视口,确保路线和步行线都在可视范围内。
@@ -114,10 +132,11 @@
  /**
   * 处理路线选择后的业务联动。
   */
  const handleSelectRoute = (index: number) => {
  const handleSelectRoute = (index: number, options: HandleSelectRouteOptions = {}) => {
    if (!routeOptions[index] || index === selectedRouteIndex) {
      return;
    }
    const { syncSites = true, syncViewport = true } = options;
    const selectedRoute = routeOptions[index];
    setSelectedRouteIndex(index);
@@ -153,10 +172,13 @@
    }
    if (
      syncSites
      && (
      deps.tripStatusRef.current === TripStatus.PublishRoute
      && Number.isFinite(nextStartLatitude)
      && Number.isFinite(nextStartLongitude)
      && typeof deps.setStartSite === 'function'
      )
    ) {
      deps.setStartSite({
        ...deps.startSite,
@@ -167,10 +189,13 @@
    }
    if (
      syncSites
      && (
      deps.tripStatusRef.current === TripStatus.PublishRoute
      && Number.isFinite(nextEndLatitude)
      && Number.isFinite(nextEndLongitude)
      && typeof deps.setEndSite === 'function'
      )
    ) {
      deps.setEndSite({
        ...deps.endSite,
@@ -191,7 +216,9 @@
    if (points.length > 0) {
      applyRoutePoints(points);
      syncRouteViewport(allRoutePoints);
      if (syncViewport) {
        syncRouteViewport(allRoutePoints);
      }
      return;
    }
    applyRoutePoints([]);
@@ -259,7 +286,7 @@
  const handlePolylineTap = (event: any) => {
    const targetIndex = resolveRouteIndexByTap(event);
    if (targetIndex >= 0 && targetIndex < allRoutePoints.length) {
      handleSelectRoute(targetIndex);
      handleSelectRoute(targetIndex, { syncSites: false });
    }
  };
@@ -269,7 +296,7 @@
  const handleMapTap = (event: any) => {
    const targetIndex = resolveRouteIndexByTap(event);
    if (targetIndex >= 0 && targetIndex < allRoutePoints.length) {
      handleSelectRoute(targetIndex);
      handleSelectRoute(targetIndex, { syncSites: false });
    }
  };
src/robotaxi/pages/begin-route/hooks/use-route-manager.shared.ts
@@ -55,6 +55,14 @@
  cachedAt: number;
};
/** 当前用户定位结构。 */
export type CurrentUserLocation = {
  /** 纬度。 */
  latitude: number;
  /** 经度。 */
  longitude: number;
} | null;
/** 路线管理依赖。 */
export type Deps = {
  /** 是否为 Android 端。 */
@@ -96,6 +104,8 @@
    /** 速度,单位 km/h。 */
    speedKmh: number | null;
  }) => void;
  /** 设置当前用户定位。 */
  setCurrentUserLocation?: (location: CurrentUserLocation) => void;
};
/** 默认依赖。 */
src/robotaxi/pages/begin-route/hooks/use-route-manager.ts
@@ -37,6 +37,12 @@
  /** 路线交互能力。 */
  const interactionState = useRouteInteraction({
    deps,
    routeOptions,
    setRouteOptions,
    selectedRouteIndex,
    setSelectedRouteIndex,
    allRoutePoints,
    setAllRoutePoints,
    walkingPoints: renderState.walkingPoints,
    applyRoutePoints: renderState.applyRoutePoints,
    reducePoints: renderState.reducePoints,
src/robotaxi/pages/begin-route/hooks/use-route-request.ts
@@ -1,7 +1,7 @@
import Taro from '@tarojs/taro';
import { useRef } from 'react';
import { useEffect, useRef } from 'react';
import { getWalkingRoute } from '../../../../utils/Robotaxi-public-method';
import { queryOrderRoute, queryOrderTrajectory, queryRouteAndFare } from '../../../api/begin-route';
import { queryOrderRoute, queryOrderTrajectory, queryRouteAndFare, queryRoutePreferences } from '../../../api/begin-route';
import { ensureSelectedCityByLocation } from '../../../utils/city';
import { normalizeDistanceKm, normalizeDurationSeconds } from '../../../utils/format';
import { TripStatus } from '../constants/trip-status';
@@ -104,6 +104,44 @@
});
/**
 * 提取路线稳定匹配键,优先使用规划策略值。
 */
const getRouteStableKey = (item: any) => {
  const routeRule = item?.primaryRoutePlanRule;
  if (routeRule !== undefined && routeRule !== null && String(routeRule) !== '') {
    return `rule:${String(routeRule)}`;
  }
  const routeId = item?.route?.routeID;
  if (routeId !== undefined && routeId !== null && String(routeId) !== '') {
    return `route:${String(routeId)}`;
  }
  return '';
};
/**
 * 归一化路线偏好列表。
 */
const normalizeRoutePreferenceOptions = (response: any) => {
  const list = Array.isArray(response)
    ? response
    : (Array.isArray(response?.list) ? response.list : (Array.isArray(response?.data) ? response.data : []));
  return list.map((item: any) => {
    const label = item?.name;
    const value = item?.amapStrategy;
    const code = item?.code;
    if (!label || value === undefined || value === null) {
      return null;
    }
    return {
      label: String(label),
      value: String(value),
      code: String(code),
      selected: item?.selected === true
    };
  }).filter(Boolean);
};
/**
 * 管理路线请求、订单轨迹请求与步行路线请求。
 */
export default function useRouteRequest(params: UseRouteRequestParams): UseRouteRequestResult {
@@ -135,6 +173,17 @@
  const lastRouteAndFareRequestAtRef = useRef<number>(0);
  /** 步行起点定位缓存引用。 */
  const walkingOriginLocationRef = useRef<WalkingOriginLocation | null>(null);
  /** 最新路线选项引用,避免异步回包使用旧闭包状态。 */
  const latestRouteOptionsRef = useRef<any[]>(routeOptions);
  /** 最新选中路线索引引用,避免异步回包回退到旧索引。 */
  const latestSelectedRouteIndexRef = useRef<number>(selectedRouteIndex);
  /** 路线偏好缓存引用,避免重复请求。 */
  const routePreferenceCacheRef = useRef<Record<string, any[]>>({});
  useEffect(() => {
    latestRouteOptionsRef.current = routeOptions;
    latestSelectedRouteIndexRef.current = selectedRouteIndex;
  }, [routeOptions, selectedRouteIndex]);
  /**
   * 确保当前路线请求具备城市上下文。
@@ -153,6 +202,10 @@
      cachedLocation
      && now - cachedLocation.cachedAt <= WALKING_ORIGIN_LOCATION_CACHE_DURATION_MS
    ) {
      deps.setCurrentUserLocation?.({
        latitude: cachedLocation.latitude,
        longitude: cachedLocation.longitude
      });
      return cachedLocation;
    }
@@ -169,7 +222,73 @@
    const nextLocation: WalkingOriginLocation = { latitude, longitude, cachedAt: now };
    walkingOriginLocationRef.current = nextLocation;
    deps.setCurrentUserLocation?.({ latitude, longitude });
    return nextLocation;
  };
  /**
   * 根据最新已选路线,在新路线集合中解析应保留的索引。
   */
  const resolveSelectedRouteIndex = (options: any[]) => {
    if (!Array.isArray(options) || options.length === 0) {
      return -1;
    }
    const latestRouteOptions = latestRouteOptionsRef.current;
    const latestSelectedRouteIndex = latestSelectedRouteIndexRef.current;
    const currentSelectedRoute = latestRouteOptions[latestSelectedRouteIndex];
    const currentStableKey = getRouteStableKey(currentSelectedRoute);
    if (currentStableKey) {
      const matchedIndex = options.findIndex((item) => getRouteStableKey(item) === currentStableKey);
      if (matchedIndex !== -1) {
        return matchedIndex;
      }
    }
    const currentRouteId = currentSelectedRoute?.route?.routeID;
    if (currentRouteId !== undefined && currentRouteId !== null && String(currentRouteId) !== '') {
      const matchedIndex = options.findIndex((item) => String(item?.route?.routeID ?? '') === String(currentRouteId));
      if (matchedIndex !== -1) {
        return matchedIndex;
      }
    }
    return 0;
  };
  /**
   * 读取当前城市的路线偏好,优先命中内存缓存。
   */
  const getRoutePreferenceOptions = async () => {
    const openCityId = Number(Taro.getStorageSync('selectedCity')?.id || 0);
    const cacheKey = String(openCityId);
    if (routePreferenceCacheRef.current[cacheKey]) {
      return routePreferenceCacheRef.current[cacheKey];
    }
    try {
      const response = await queryRoutePreferences({ openCityId });
      const normalizedOptions = normalizeRoutePreferenceOptions(response);
      routePreferenceCacheRef.current[cacheKey] = normalizedOptions;
      return normalizedOptions;
    } catch (error) {
      routePreferenceCacheRef.current[cacheKey] = [];
      return [];
    }
  };
  /**
   * 首次请求路线时,按偏好策略确定默认选中项。
   */
  const resolveInitialSelectedRouteIndex = async (options: any[]) => {
    if (!Array.isArray(options) || options.length === 0) {
      return -1;
    }
    const preferenceOptions = await getRoutePreferenceOptions();
    const selectedPreferenceOption = preferenceOptions.find((item: any) => item?.selected === true);
    if (!selectedPreferenceOption) {
      return 0;
    }
    const matchedIndex = options.findIndex((item: any) => (
      String(item?.primaryRoutePlanRule ?? '') === String(selectedPreferenceOption.value)
    ));
    return matchedIndex >= 0 ? matchedIndex : 0;
  };
  /**
@@ -265,20 +384,19 @@
      let routeData = options[0] || null;
      let allRoutePointGroups: any[][] = [];
      if (refreshMode === 'price_only') {
        const currentRouteId = routeOptions[selectedRouteIndex]?.route?.routeID;
        routeData = options.find((item: any) => item?.route?.routeID === currentRouteId) || options[0] || null;
        const resolvedSelectedRouteIndex = resolveSelectedRouteIndex(options);
        routeData = options[resolvedSelectedRouteIndex] || options[0] || null;
        if (options.length > 0) {
          setRouteOptions((previousRouteOptions) => {
            if (!Array.isArray(previousRouteOptions) || previousRouteOptions.length === 0) {
              return options;
            }
            const optionMap = new Map(
              options.map((item: any) => [item?.route?.routeID, item])
              options.map((item: any) => [getRouteStableKey(item), item])
            );
            return previousRouteOptions.map((oldItem: any, index: number) => {
              const matchedOption = optionMap.get(oldItem?.route?.routeID) || options[index];
              const matchedOption = optionMap.get(getRouteStableKey(oldItem)) || options[index];
              if (!matchedOption) {
                return oldItem;
              }
@@ -296,12 +414,17 @@
        }
      } else {
        if (options.length > 0) {
          const hasCurrentSelectedRoute = Array.isArray(latestRouteOptionsRef.current) && latestRouteOptionsRef.current.length > 0;
          const resolvedSelectedRouteIndex = hasCurrentSelectedRoute
            ? resolveSelectedRouteIndex(options)
            : await resolveInitialSelectedRouteIndex(options);
          setRouteOptions(options);
          setSelectedRouteIndex(0);
          setSelectedRouteIndex(resolvedSelectedRouteIndex);
          allRoutePointGroups = options.map((item) => (
            Array.isArray(item?.route?.polyline) ? item.route.polyline : []
          ));
          setAllRoutePoints(allRoutePointGroups);
          routeData = options[resolvedSelectedRouteIndex] || options[0] || null;
        } else {
          setRouteOptions([]);
          setAllRoutePoints([]);
src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx
@@ -33,6 +33,10 @@
  arriveCalloutVisible: boolean;
  /** 到达气泡样式 */
  arriveCalloutStyle: any;
  /** 路线摘要气泡可见 */
  routeSummaryCalloutVisible: boolean;
  /** 路线摘要气泡样式 */
  routeSummaryCalloutStyle: any;
  /** 到达态气泡渲染就绪(角度与缩放均就绪) */
  arrivedTooltipReady: boolean;
  /** 路线距离 */
@@ -229,7 +233,9 @@
  const renderMarkerCallout = (item: any, index: number) => {
    const calloutKey = `callout-${item.id}-${item.name || ''}`;
    // 路线摘要气泡需跨平台强制显示,避免被 Android 普通气泡隐藏规则误伤
    if (item.isRouteSummaryBubble) return renderDefaultCallout(item, calloutKey);
    if (item.isRouteSummaryBubble) {
      return deps.isAndroid ? null : renderDefaultCallout(item, calloutKey);
    }
    if (shouldHideAndroidMarkerCallout && !item.isBeforeUserSite) return null;
    const isWaiting = [TripStatus.Publishing, TripStatus.CallingCar].includes(deps.tripStatus) && index === 0;
    if (isWaiting) return renderWaitingCallout(item, calloutKey);
@@ -246,6 +252,14 @@
      {deps.shouldShowWaitingRadar && deps.waitingRadarVisible && (
        <Image className="waiting-radar-img" src={deps.waitingRadarSrc} webp={true} style={deps.waitingRadarStyle} />
      )}
      {deps.isAndroid && deps.routeSummaryCalloutVisible && routeSummaryText && (
        <View className="route-summary-callout" style={deps.routeSummaryCalloutStyle}>
          <Image className="route-summary-callout-bg" src={summaryIcon} />
          <View className="route-summary-callout-text">
            {routeSummaryText}
          </View>
        </View>
      )}
      {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' }}>
src/robotaxi/pages/begin-route/index.tsx
@@ -1,4 +1,4 @@
import Taro, { useDidHide, useDidShow, useShareAppMessage } from "@tarojs/taro"
import Taro, { useDidShow, useShareAppMessage } from "@tarojs/taro"
import { useEffect, useState, useRef, useMemo } from 'react';
import { View, Image } from '@tarojs/components';
import './index.less';
@@ -24,7 +24,7 @@
import useAndroidOverlays from './hooks/use-android-overlays';
// 地图视口状态管理
import useMapViewport from './hooks/use-map-viewport';
import useAutoRouteViewport from './hooks/use-auto-route-viewport';
import useActiveTripScale from './hooks/use-active-trip-scale';
// 页面恢复与初始化编排
import usePageRestore from './hooks/use-page-restore';
// 页面动作编排
@@ -47,6 +47,8 @@
import { toTripStatus, TripStatus } from './constants/trip-status';
import { MODIFY_DESTINATION_SOCKET_STATUS_KEY } from './constants/modify-destination';
import { getDistance } from '../../../utils/Robotaxi-public-method';
import { CurrentUserLocation } from './hooks/use-route-manager.shared';
import { MODIFY_DESTINATION_SESSION_KEY } from './constants/modify-destination';
import backIcon from '../../../robotaxi/assets/back-icon.png'
import personalCenter from '../../../robotaxi/assets/personal-center.png'
@@ -149,6 +151,7 @@
    durationSeconds: null,
    speedKmh: null
  });
  const [currentUserLocation, setCurrentUserLocation] = useState<CurrentUserLocation>(null); // 当前用户真实定位,避免被地图中心点覆盖
  const [modifyDestinationSocketStatus, setModifyDestinationSocketStatus] = useState<number | null>(null); // 后端推送的修改目的地展示状态
  const tripStatusRef = useRef(tripStatus); // 行程状态引用
@@ -299,7 +302,8 @@
    setBeforeUserEndSite,
    updateVehicleLocationThrottled: vehicleState.updateVehicleLocationThrottled,
    setVehicleDirection,
    setOrderRouteMetrics
    setOrderRouteMetrics,
    setCurrentUserLocation
  }); // 路线与费用状态容器
  const {
    polyline,
@@ -313,9 +317,9 @@
    routeOptions,
    setRouteOptions,
    selectedRouteIndex,
    handleSelectRoute,
    handlePolylineTap,
    handleMapTap,
    handleSelectRoute: baseHandleSelectRoute,
    handlePolylineTap: baseHandlePolylineTap,
    handleMapTap: baseHandleMapTap,
    fareEstimate,
    setFareEstimate,
    routeAndFareError,
@@ -396,18 +400,25 @@
    setOriginOpacity,
    showLocationFlag,
    tripStatus6ViewportLockedRef,
    holdDynamicScale,
    getDynamicScaleHoldUntil,
    updateMapViewportPadding,
    toOrigin,
    getLocationInfo,
    computeBottomPadding,
    resolveSafeTopInset
  } = viewportState; // 视口状态解构
  const autoRouteViewport = useAutoRouteViewport({
  const activeTripScale = useActiveTripScale({
    tripStatus,
    routePoints,
    speedKmh: orderRouteMetrics?.speedKmh,
    mapScale,
    setMapScale: viewportState.setMapScale,
    computeBottomPadding,
    resolveSafeTopInset
  }); // 自动路线视口巡航编排
    resolveSafeTopInset,
    holdDynamicScale,
    getDynamicScaleHoldUntil
  }); // 行程中速度缩放与 ORDER_UPDATE_LINE 视口编排
  const arrivedMarkerReady = tripStatus !== TripStatus.ArrivedDestination || typeof vehicleDirection === 'number';
  const arrivedTooltipReady = tripStatus !== TripStatus.ArrivedDestination || (arrivedMarkerReady && mapScale >= 20);
  const markerState = useMapMarkers({
@@ -436,12 +447,18 @@
    carCalloutVisible,
    arriveCalloutStyle,
    arriveCalloutVisible,
    routeSummaryCalloutStyle,
    routeSummaryCalloutVisible,
    handleRegionChange: handleAndroidOverlayRegionChange
  } = useAndroidOverlays({
    shouldShowWaitingRadar,
    isAndroid,
    tripStatus,
    mapScale,
    routeSummaryLocation: routeSummaryMarker ? {
      latitude: routeSummaryMarker.latitude,
      longitude: routeSummaryMarker.longitude
    } : null,
    startSite,
    endSite: effectiveEndSite,
    vehicleLocation,
@@ -596,6 +613,30 @@
    polylineKeyRef.current = '';
    lastPolylineUpdateAtRef.current = 0;
  };
  /**
   * 手动切线前先暂停速度缩放,避免一次性全览后立即被速度接管。
   */
  const handleSelectRoute = (index: number, options?: any) => {
    holdDynamicScale();
    baseHandleSelectRoute(index, options);
  };
  /**
   * 处理路线 polyline 点击切换。
   */
  const handlePolylineTap = (event: any) => {
    holdDynamicScale();
    baseHandlePolylineTap(event);
  };
  /**
   * 处理地图点击切换路线。
   */
  const handleMapTap = (event: any) => {
    holdDynamicScale();
    baseHandleMapTap(event);
  };
  const {
    applyRouteSnapshot: applySmoothRouteSnapshot,
    resetSmoothRouteMotion
@@ -650,7 +691,7 @@
    erasePassedRouteSegments: routeErasePassedRouteSegments,
    applyRoutePoints: routeApplyRoutePoints,
    updateVehicleLocationThrottled: vehicleState.updateVehicleLocationThrottled,
    onOrderUpdateLineRendered: autoRouteViewport.handleOrderUpdateLineRendered,
    onOrderUpdateLineRendered: activeTripScale.handleOrderUpdateLineRendered,
    applySmoothRouteSnapshot,
    resetSmoothRouteMotion
  });
@@ -716,7 +757,7 @@
    bluetoothAuthSuccess,
    startBluetoothAuth
  });
  useModifyDestinationPreviewRoute({
  const { handleConfirmRequoteIfNeeded } = useModifyDestinationPreviewRoute({
    tripStatus,
    isModifyDestinationReviewVisible,
    hasLocalDestinationOverride,
@@ -738,10 +779,6 @@
    setPageDidShowVersion((v) => v + 1);
    handleDidShow(router.params?.skipPageFlag);
    updateMapViewportPadding();
    autoRouteViewport.resume();
  });
  useDidHide(() => {
    autoRouteViewport.dispose();
  });
  // 打开评价弹窗
@@ -761,7 +798,7 @@
  }
  const handleRegionChange = (e) => {
    autoRouteViewport.handleRegionChange(e);
    activeTripScale.handleRegionChange(e);
    handleAndroidOverlayRegionChange(e);
  };
@@ -794,6 +831,16 @@
  const handleAcknowledgeModifyDestinationFail = () => {
    setShowModifyDestinationFailPopup(false);
    setModifyDestinationFailMessage('');
    const modifyDestinationSession = Taro.getStorageSync(MODIFY_DESTINATION_SESSION_KEY);
    const fallbackPreviousEndSite = modifyDestinationSession?.previousEndSite;
    if (
      fallbackPreviousEndSite
      && Number.isFinite(Number(fallbackPreviousEndSite?.latitude))
      && Number.isFinite(Number(fallbackPreviousEndSite?.longitude))
    ) {
      Taro.setStorageSync('endSite', fallbackPreviousEndSite);
      setEndSite(fallbackPreviousEndSite);
    }
    exitModifyDestinationReview();
    setIsRouteSelectExpanded(false);
    setRouteOptions([]);
@@ -821,6 +868,9 @@
    if (hitDistanceLimit || hitDurationLimit) {
      openModifyDestinationFailPopup('即将到达终点,已无法修改目的地');
      return;
    }
    if (handleConfirmRequoteIfNeeded()) {
      return;
    }
    try {
@@ -874,7 +924,7 @@
    fareEstimate?.couponId || fareEstimate?.userCouponId || Taro.getStorageSync('selectedCouponId') || ''
  );
  const couponOrderMoney = Number(fareEstimate?.originalFare || fareEstimate?.finalFare || 0); // 优惠券可用性计算订单金额
  const showWalkNavigationButton = useMemo(() => {
  const canWalkNavigateToPickup = useMemo(() => {
    if (isModifyDestinationReviewVisible) {
      return false;
    }
@@ -883,8 +933,8 @@
    }
    const startLatitude = Number(startSite?.latitude);
    const startLongitude = Number(startSite?.longitude);
    const currentLatitude = Number(latitude);
    const currentLongitude = Number(longitude);
    const currentLatitude = Number(currentUserLocation?.latitude);
    const currentLongitude = Number(currentUserLocation?.longitude);
    if (!Number.isFinite(startLatitude) || !Number.isFinite(startLongitude)) {
      return false;
    }
@@ -896,7 +946,48 @@
    }
    const walkingDistance = getDistance(currentLatitude, currentLongitude, startLatitude, startLongitude);
    return Number.isFinite(walkingDistance) && walkingDistance >= walkingNavigationDistance;
  }, [isModifyDestinationReviewVisible, tripStatus, startSite?.latitude, startSite?.longitude, latitude, longitude, walkingNavigationDistance]);
  }, [isModifyDestinationReviewVisible, tripStatus, startSite?.latitude, startSite?.longitude, currentUserLocation?.latitude, currentUserLocation?.longitude, walkingNavigationDistance]);
  const showWalkNavigationButton = canWalkNavigateToPickup; // 步行入口展示与点击统一使用真实用户定位语义
  /**
   * 接驾态下补充刷新当前用户真实定位,避免按钮显隐依赖步行路线请求时机。
   */
  useEffect(() => {
    const shouldRefreshWalkLocation = [TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup].includes(tripStatus);
    if (!shouldRefreshWalkLocation) {
      return;
    }
    if (!startSite?.latitude || !startSite?.longitude) {
      return;
    }
    if (currentUserLocation?.latitude && currentUserLocation?.longitude) {
      return;
    }
    let cancelled = false;
    Taro.getLocation({
      type: 'gcj02',
      isHighAccuracy: true,
      highAccuracyExpireTime: 35 * 1000
    } as any).then((location) => {
      if (cancelled) {
        return;
      }
      const nextLatitude = Number(location?.latitude);
      const nextLongitude = Number(location?.longitude);
      if (!Number.isFinite(nextLatitude) || !Number.isFinite(nextLongitude)) {
        return;
      }
      setCurrentUserLocation({
        latitude: nextLatitude,
        longitude: nextLongitude
      });
    }).catch(() => {
    });
    return () => {
      cancelled = true;
    };
  }, [tripStatus, startSite?.latitude, startSite?.longitude, currentUserLocation?.latitude, currentUserLocation?.longitude]);
  const { renderMapOverlays, renderTripCard } = useViewDelegates({
    isAndroid,
    tripStatus,
@@ -912,6 +1003,8 @@
    carCalloutStyle,
    arriveCalloutVisible,
    arriveCalloutStyle,
    routeSummaryCalloutVisible,
    routeSummaryCalloutStyle,
    arrivedTooltipReady,
    routeDistance,
    routeDuration,
@@ -985,7 +1078,7 @@
    toPersonalCenter();
  };
  const handleWalkNavigation = () => {
    if (![TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup].includes(tripStatus) || !startSite?.latitude || !startSite?.longitude) {
    if (!canWalkNavigateToPickup || !startSite?.latitude || !startSite?.longitude) {
      return;
    }
src/robotaxi/pages/order/index.tsx
@@ -172,12 +172,12 @@
              <View className='order-action-row'>
                {
                  formatOrderStatus(item.status) == '待支付' &&
                  <View onClick={(e) => e.stopPropagation()}>
                  <View onClick={(e) => { e.stopPropagation(); againOrder(item) }}>
                    <View className='order-pay-text' hoverClass='common-hover-class'>去支付</View>
                  </View>
                }
                {
                  ['已完成', '已取消'].includes(formatOrderStatus(item.status)) &&
                  ['已完成', '已取消', '待评价'].includes(formatOrderStatus(item.status)) &&
                  <View onClick={(e) => { e.stopPropagation(); againOrder(item) }}>
                    <View className='order-again-text' hoverClass='common-hover-class'>再来一单</View>
                  </View>
src/robotaxi/pages/site-search/components/favorite-popup/index.tsx
@@ -30,7 +30,7 @@
    try {
      const res = await getCollectPoints({
        pageNum: page,
        pageSize: 4,
        pageSize: 999,
      });
      if (res && res.records) {
        setList(prev => page === 1 ? res.records : [...prev, ...res.records]);
src/robotaxi/pages/site-search/components/site-list-item.tsx
@@ -101,10 +101,10 @@
  };
  const checkCollectLimit = async () => {
    // 只取总数即可,避免为校验拉完整列表
    // 只取总数即可
    const res = await getCollectPoints({
      pageNum: 1,
      pageSize: 1
      pageSize: 999,
    });
    if (Number(res?.total) >= MAX_COLLECT_COUNT) {
@@ -174,11 +174,15 @@
          </View>
        </View>
        {showDistance ?
          <View className='collect-box'>
            <Image onClick={(e) => {
          <View
            className='collect-box'
            hoverStopPropagation
            hoverClass='common-hover-class'
            onClick={(e) => {
              e.stopPropagation();
              handleCollect(e);
            }} className='collect-icon' src={isCollected ? starSrc : noStarSrc} />
            }}>
            <Image className='collect-icon' src={isCollected ? starSrc : noStarSrc} />
            {renderHighlightedText(item.distance || '-', 'history-distance')}
          </View>
          : null}
src/robotaxi/pages/site-search/index.tsx
@@ -28,7 +28,20 @@
  MODIFY_DESTINATION_SESSION_KEY
} from '../begin-route/constants/modify-destination';
/**
 * 安全解析路由关键字,兼容已编码和未编码两种入参。
 */
const decodeRouteKeyword = (keyword: unknown) => {
  const normalizedKeyword = String(keyword || '').trim();
  if (!normalizedKeyword) {
    return '';
  }
  try {
    return decodeURIComponent(normalizedKeyword);
  } catch (error) {
    return normalizedKeyword;
  }
};
export default function SiteSearchPage() {
  const [searchValue, setSearchValue] = useState('') //搜索值
@@ -44,7 +57,7 @@
  const MAX_EXPANDED_COUNT = 10
  const router = Taro.useRouter();
  const initialKeyword = String(router.params?.keyword || '').trim();
  const initialKeyword = decodeRouteKeyword(router.params?.keyword);
  const {
    pageType,
    siteList,