hejianhao
2026-06-18 036c72f09b03809e572421b80dcfefd8446189da
feat(begin-route): 添加入驻接驾态的路线切换功能

详细实现包括:
- 新增切换导航路线的后端API接口及请求类型定义
- 完善接驾态路线切换弹窗的UI组件、状态管理与交互逻辑
- 新增页面恢复时的路线切换资格同步检查
- 优化WebSocket事件处理以正确过滤接驾态下的路线预览更新
- 集成确认提交后的流程与用户反馈提示
8个文件已修改
164 ■■■■■ 已修改文件
src/robotaxi/api/begin-route.ts 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/api/types/begin-route.ts 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car-modules/service-route-change.jsx 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-page-restore.ts 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-web-socket-events.ts 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/web-socket-handlers/create-order-route-socket-handler.ts 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/index.tsx 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/api/begin-route.ts
@@ -23,6 +23,7 @@
    QueryVehicleAirConditionerSwitchResponse,
    QueryVehicleByOrderIdResponse,
    QueryVehicleTrunkOpenStatusResponse,
    SelectRoutePayload,
    RouteStaleCheckPayload,
    RouteStaleCheckResponse,
    SaveOrderPayload,
@@ -90,6 +91,15 @@
    }, true);
}
// 导航-接驾态确认切换路线
export const selectNavigationRoute = async (payload: SelectRoutePayload): Promise<void> => {
    return request({
        url: _ROBOTAXI_HOST + '/itinerary/base/v1/mini-program/navigation/select-route',
        method: 'POST',
        payload
    }, true);
}
// 保存订单
export const saveOrder = async (payload: SaveOrderPayload): Promise<SaveOrderResponse> => {
    return request({
src/robotaxi/api/types/begin-route.ts
@@ -175,6 +175,16 @@
    needRefreshNotice?: boolean;
}
/** 接驾态切换路线请求。 */
export interface SelectRoutePayload {
    /** 订单 ID。 */
    orderId: OrderId | '';
    /** 导航快照 ID。 */
    snapshotId: OrderId;
    /** 路况指纹。 */
    pathFingerprint: string;
}
/** 保存订单请求。 */
export interface SaveOrderPayload {
    /** 终点站点 ID。 */
src/robotaxi/pages/begin-route/components/calling-car-modules/service-route-change.jsx
@@ -1,4 +1,3 @@
import Taro from "@tarojs/taro";
import { Image, View, Text } from "@tarojs/components";
import { formatRemainingDurationDisplay, normalizeDistanceKm } from '../../../../utils/format';
import RoutePreferencePopup from '../route-preference-popup';
@@ -12,6 +11,8 @@
  routeOptions = [],
  selectedRouteIndex = 0,
  handleSelectRoute,
  isSubmitting = false,
  onConfirm,
  onCancel,
}) {
  const {
@@ -73,13 +74,11 @@
        <View
          className='service-route-select-footer-item-confirm'
          onClick={() => {
            Taro.showToast({
              title: '确认修改功能待开放',
              icon: 'none'
            });
            if (isSubmitting) return;
            onConfirm && onConfirm();
          }}
        >
          确认修改
          {isSubmitting ? '提交中...' : '确认修改'}
        </View>
      </View>
src/robotaxi/pages/begin-route/hooks/use-page-restore.ts
@@ -2,7 +2,7 @@
import wsManager from '../../../../utils/websocket';
import { mapOrderStatusToTripStatus } from '../../../../utils/Robotaxi-public-method';
import { getOrderActive, } from '../../../api/home';
import { getAppUserCouponStatus, queryVehicleByOrderId } from '../../../api/begin-route';
import { getAppUserCouponStatus, queryRouteAndFare, queryVehicleByOrderId } from '../../../api/begin-route';
import { getOrderDetail } from '../../../api/order';
import { toTripStatus, TripStatus } from '../../../constants/trip-status';
import { MODIFY_DESTINATION_CANDIDATE_KEY, MODIFY_DESTINATION_SESSION_KEY } from '../../../constants/modify-destination';
@@ -49,6 +49,7 @@
  getCurrentLongitude: () => number | null;
  getCurrentLatitude: () => number | null;
  getCurrentVehicleLocation: () => { latitude: number; longitude: number } | null;
  updatePickupRouteChangeEligibleOrder?: (orderId: string) => void;
};
/**
@@ -145,6 +146,55 @@
        deps.setVehicleInfo(res);
      })
      .catch(() => { });
  };
  /**
   * 接驾态重进页面时,重新请求预估路线,判断当前订单是否允许展示切换路线入口。
   */
  const syncPickupRouteChangeEligibility = async (
    restoredOrderId: string,
    restoredTripStatus: TripStatus,
    restoredStartSite: any,
    restoredEndSite: any
  ) => {
    if (restoredTripStatus !== TripStatus.VehicleApproaching || !restoredOrderId) {
      return;
    }
    const originLat = Number(restoredStartSite?.latitude);
    const originLng = Number(restoredStartSite?.longitude);
    const destLat = Number(restoredEndSite?.latitude);
    const destLng = Number(restoredEndSite?.longitude);
    const openCityId = Number(Taro.getStorageSync('selectedCity')?.id || 0);
    if (
      !Number.isFinite(originLat)
      || !Number.isFinite(originLng)
      || !Number.isFinite(destLat)
      || !Number.isFinite(destLng)
      || !openCityId
    ) {
      return;
    }
    try {
      const response: any = await queryRouteAndFare({
        origin: {
          latitude: originLat,
          longitude: originLng
        },
        destination: {
          latitude: destLat,
          longitude: destLng
        },
        openCityId,
        startPointId: restoredStartSite?.id || '',
        orderId: restoredOrderId
      });
      const responseRoutes = Array.isArray(response?.routes)
        ? response.routes
        : (Array.isArray(response) ? response : (response ? [response] : []));
      deps.updatePickupRouteChangeEligibleOrder?.(responseRoutes.length > 1 ? restoredOrderId : '');
    } catch (error) {
    }
  };
  /**
@@ -354,6 +404,12 @@
      deps.routeRequestOrderRoute(resolvedOrderId, resolvedTripStatus);
    }
    await syncLocationAndInitRoute(resolvedStartSite, resolvedEndSite, resolvedOrderId, resolvedTripStatus);
    void syncPickupRouteChangeEligibility(
      resolvedOrderId,
      resolvedTripStatus,
      resolvedStartSite,
      resolvedEndSite
    );
  };
  /**
src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx
@@ -133,6 +133,10 @@
  selectedRouteIndex: number;
  /** 切换路线回调 */
  handleSelectRoute: (index: number, options?: { syncSites?: boolean; syncViewport?: boolean }) => void;
  /** 接驾态确认切换路线请求中 */
  isConfirmingPickupRouteChange?: boolean;
  /** 接驾态确认切换路线 */
  handleConfirmPickupRouteChange?: () => void;
  /** 地图点击 */
  handleMapTap?: (e: any) => void;
  /** 修改目的地确认态 */
@@ -473,6 +477,8 @@
          routeOptions={deps.routeOptions}
          selectedRouteIndex={deps.selectedRouteIndex}
          handleSelectRoute={deps.handleSelectRoute}
          isSubmitting={deps.isConfirmingPickupRouteChange}
          onConfirm={deps.handleConfirmPickupRouteChange}
          onCancel={deps.handleCancelPickupRouteChange}
        />
      );
src/robotaxi/pages/begin-route/hooks/use-web-socket-events.ts
@@ -71,6 +71,7 @@
    setBeforeUserEndSite: (site: { longitude: number; latitude: number } | null) => void;
    isModifyDestinationReviewVisible?: boolean;
    hasLocalDestinationOverride?: boolean;
    isPickupRouteChangeVisible?: boolean;
    updatePreviousRoutePoints?: (points: any[]) => void;
    erasePassedRouteSegments: (points: any[], servedPoints?: any[]) => void;
    applyRoutePoints: (points: any[]) => boolean;
src/robotaxi/pages/begin-route/hooks/web-socket-handlers/create-order-route-socket-handler.ts
@@ -113,6 +113,10 @@
  currentDeps.setBeforeUserEndSite(beforeUserSite);
  const currentTripStatus = currentDeps.tripStatusRef.current;
  const isModifyDestinationPreviewActive = Boolean(currentDeps.isModifyDestinationReviewVisible);
  const isPickupRouteChangePreviewActive = Boolean(
    currentDeps.isPickupRouteChangeVisible
    && currentTripStatus === TripStatus.VehicleApproaching
  );
  if (!isModifyDestinationPreviewActive) {
    updateRouteSummary(payload, currentDeps);
  }
@@ -125,6 +129,9 @@
  if (isModifyDestinationPreviewActive) {
    currentDeps.updatePreviousRoutePoints?.(routeSnapshot.points || []);
  }
  if (isPickupRouteChangePreviewActive) {
    return;
  }
  if (shouldFreezeModifyPreviewRoute) {
    return;
  }
src/robotaxi/pages/begin-route/index.tsx
@@ -58,6 +58,7 @@
import useBluetoothCarControl from './hooks/use-bluetooth-car-control';
import { toTripStatus, TripStatus } from '../../constants/trip-status';
import { MODIFY_DESTINATION_SOCKET_STATUS_KEY } from '../../constants/modify-destination';
import { selectNavigationRoute } from '../../api/begin-route';
import { getDistance, reportEvent } from '../../../utils/Robotaxi-public-method';
import { CurrentUserLocation, RouteDistanceValue } from './hooks/use-route-manager.shared';
import { MODIFY_DESTINATION_SESSION_KEY } from '../../constants/modify-destination';
@@ -242,6 +243,7 @@
  const routeSummaryBgUrl = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/summary-icon.png'; // 路线概览气泡背景图URL
  const [isRouteSelectExpanded, setIsRouteSelectExpanded] = useState(false); // 路线选择展开状态
  const [isPickupRouteChangeVisible, setIsPickupRouteChangeVisible] = useState(false); // 接驾态路线切换卡片展示状态
  const [isConfirmingPickupRouteChange, setIsConfirmingPickupRouteChange] = useState(false); // 接驾态确认切换路线提交中
  const [pickupRouteChangeEligibleOrderId, setPickupRouteChangeEligibleOrderId] = useState<string>(() => String(Taro.getStorageSync(PICKUP_ROUTE_CHANGE_ELIGIBLE_ORDER_ID_KEY) || '')); // 接驾态允许展示切换入口的订单 ID
  const [pageDidShowVersion, setPageDidShowVersion] = useState(0); // 页面展示版本号,用于触发子模块重查
  const [resetDetailVersion, setResetDetailVersion] = useState(0); // 重置详情版本号,用于触发组件更新
@@ -788,6 +790,58 @@
    walkingTime
  ]);
  /**
   * 确认接驾态切换路线,并通知后端更新当前导航路线。
   */
  const handleConfirmPickupRouteChange = useCallback(async () => {
    if (isConfirmingPickupRouteChange) {
      return;
    }
    const selectedRoute = Array.isArray(routeOptions) ? routeOptions[selectedRouteIndex] : null;
    const snapshotId = selectedRoute?.snapshotId;
    const pathFingerprint = selectedRoute?.pathFingerprint ?? selectedRoute?.route?.pathFingerprint;
    if (!orderId || !snapshotId || !pathFingerprint) {
      Taro.showToast({
        title: '路线信息不完整,请刷新后重试',
        icon: 'none'
      });
      return;
    }
    try {
      setIsConfirmingPickupRouteChange(true);
      Taro.showLoading({
        title: '正在修改路线',
        mask: true
      });
      await selectNavigationRoute({
        orderId,
        snapshotId,
        pathFingerprint
      });
      setIsPickupRouteChangeVisible(false);
      pickupRouteChangeSnapshotRef.current = null;
      routeRequestOrderRoute(orderId, tripStatus);
      Taro.hideLoading();
      setTimeout(() => {
        Taro.showToast({
          title: '切换成功,将按新路线行驶',
          icon: 'none'
        });
      }, 0);
    } catch (error) {
      Taro.hideLoading();
    } finally {
      setIsConfirmingPickupRouteChange(false);
    }
  }, [
    isConfirmingPickupRouteChange,
    orderId,
    routeOptions,
    selectedRouteIndex
  ]);
  useEffect(() => {
    if (tripStatus !== TripStatus.PublishRoute) {
      if (!orderId) {
@@ -822,6 +876,7 @@
      return;
    }
    setIsPickupRouteChangeVisible(false);
    setIsConfirmingPickupRouteChange(false);
    pickupRouteChangeSnapshotRef.current = null;
  }, [tripStatus]);
@@ -1087,7 +1142,8 @@
    getCurrentTripStatus: () => tripStatusRef.current,
    getCurrentLongitude: () => longitude,
    getCurrentLatitude: () => latitude,
    getCurrentVehicleLocation: () => vehicleLocation
    getCurrentVehicleLocation: () => vehicleLocation,
    updatePickupRouteChangeEligibleOrder
  }); // 页面恢复与初始化编排
  const router = Taro.useRouter();
  const tripRuntime = useTripRuntime({
@@ -1301,6 +1357,7 @@
    setBeforeUserEndSite: markBeforeUserEndSite,
    isModifyDestinationReviewVisible,
    hasLocalDestinationOverride,
    isPickupRouteChangeVisible,
    updatePreviousRoutePoints,
    modifyDestinationPickupSuccessAtRef,
    erasePassedRouteSegments: routeErasePassedRouteSegments,
@@ -1880,8 +1937,10 @@
    markers: markersWithRouteSummary,
    isRouteSelectExpanded,
    isPickupRouteChangeVisible,
    isConfirmingPickupRouteChange,
    onToggleRouteSelect: setIsRouteSelectExpanded,
    handleCancelPickupRouteChange: restorePickupRouteChangeSnapshot,
    handleConfirmPickupRouteChange,
    shouldShowWaitingRadar,
    waitingRadarVisible,
    waitingRadarStyle,