feat(begin-route): 添加入驻接驾态的路线切换功能
详细实现包括:
- 新增切换导航路线的后端API接口及请求类型定义
- 完善接驾态路线切换弹窗的UI组件、状态管理与交互逻辑
- 新增页面恢复时的路线切换资格同步检查
- 优化WebSocket事件处理以正确过滤接驾态下的路线预览更新
- 集成确认提交后的流程与用户反馈提示
| | |
| | | QueryVehicleAirConditionerSwitchResponse, |
| | | QueryVehicleByOrderIdResponse, |
| | | QueryVehicleTrunkOpenStatusResponse, |
| | | SelectRoutePayload, |
| | | RouteStaleCheckPayload, |
| | | RouteStaleCheckResponse, |
| | | SaveOrderPayload, |
| | |
| | | }, 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({ |
| | |
| | | needRefreshNotice?: boolean; |
| | | } |
| | | |
| | | /** 接驾态切换路线请求。 */ |
| | | export interface SelectRoutePayload { |
| | | /** 订单 ID。 */ |
| | | orderId: OrderId | ''; |
| | | /** 导航快照 ID。 */ |
| | | snapshotId: OrderId; |
| | | /** 路况指纹。 */ |
| | | pathFingerprint: string; |
| | | } |
| | | |
| | | /** 保存订单请求。 */ |
| | | export interface SaveOrderPayload { |
| | | /** 终点站点 ID。 */ |
| | |
| | | import Taro from "@tarojs/taro"; |
| | | import { Image, View, Text } from "@tarojs/components"; |
| | | import { formatRemainingDurationDisplay, normalizeDistanceKm } from '../../../../utils/format'; |
| | | import RoutePreferencePopup from '../route-preference-popup'; |
| | |
| | | routeOptions = [], |
| | | selectedRouteIndex = 0, |
| | | handleSelectRoute, |
| | | isSubmitting = false, |
| | | onConfirm, |
| | | onCancel, |
| | | }) { |
| | | const { |
| | |
| | | <View |
| | | className='service-route-select-footer-item-confirm' |
| | | onClick={() => { |
| | | Taro.showToast({ |
| | | title: '确认修改功能待开放', |
| | | icon: 'none' |
| | | }); |
| | | if (isSubmitting) return; |
| | | onConfirm && onConfirm(); |
| | | }} |
| | | > |
| | | 确认修改 |
| | | {isSubmitting ? '提交中...' : '确认修改'} |
| | | </View> |
| | | </View> |
| | | |
| | |
| | | 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'; |
| | |
| | | getCurrentLongitude: () => number | null; |
| | | getCurrentLatitude: () => number | null; |
| | | getCurrentVehicleLocation: () => { latitude: number; longitude: number } | null; |
| | | updatePickupRouteChangeEligibleOrder?: (orderId: string) => void; |
| | | }; |
| | | |
| | | /** |
| | |
| | | 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) { |
| | | } |
| | | }; |
| | | |
| | | /** |
| | |
| | | deps.routeRequestOrderRoute(resolvedOrderId, resolvedTripStatus); |
| | | } |
| | | await syncLocationAndInitRoute(resolvedStartSite, resolvedEndSite, resolvedOrderId, resolvedTripStatus); |
| | | void syncPickupRouteChangeEligibility( |
| | | resolvedOrderId, |
| | | resolvedTripStatus, |
| | | resolvedStartSite, |
| | | resolvedEndSite |
| | | ); |
| | | }; |
| | | |
| | | /** |
| | |
| | | selectedRouteIndex: number; |
| | | /** 切换路线回调 */ |
| | | handleSelectRoute: (index: number, options?: { syncSites?: boolean; syncViewport?: boolean }) => void; |
| | | /** 接驾态确认切换路线请求中 */ |
| | | isConfirmingPickupRouteChange?: boolean; |
| | | /** 接驾态确认切换路线 */ |
| | | handleConfirmPickupRouteChange?: () => void; |
| | | /** 地图点击 */ |
| | | handleMapTap?: (e: any) => void; |
| | | /** 修改目的地确认态 */ |
| | |
| | | routeOptions={deps.routeOptions} |
| | | selectedRouteIndex={deps.selectedRouteIndex} |
| | | handleSelectRoute={deps.handleSelectRoute} |
| | | isSubmitting={deps.isConfirmingPickupRouteChange} |
| | | onConfirm={deps.handleConfirmPickupRouteChange} |
| | | onCancel={deps.handleCancelPickupRouteChange} |
| | | /> |
| | | ); |
| | |
| | | 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; |
| | |
| | | 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); |
| | | } |
| | |
| | | if (isModifyDestinationPreviewActive) { |
| | | currentDeps.updatePreviousRoutePoints?.(routeSnapshot.points || []); |
| | | } |
| | | if (isPickupRouteChangePreviewActive) { |
| | | return; |
| | | } |
| | | if (shouldFreezeModifyPreviewRoute) { |
| | | return; |
| | | } |
| | |
| | | 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'; |
| | |
| | | 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); // 重置详情版本号,用于触发组件更新 |
| | |
| | | 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) { |
| | |
| | | return; |
| | | } |
| | | setIsPickupRouteChangeVisible(false); |
| | | setIsConfirmingPickupRouteChange(false); |
| | | pickupRouteChangeSnapshotRef.current = null; |
| | | }, [tripStatus]); |
| | | |
| | |
| | | getCurrentTripStatus: () => tripStatusRef.current, |
| | | getCurrentLongitude: () => longitude, |
| | | getCurrentLatitude: () => latitude, |
| | | getCurrentVehicleLocation: () => vehicleLocation |
| | | getCurrentVehicleLocation: () => vehicleLocation, |
| | | updatePickupRouteChangeEligibleOrder |
| | | }); // 页面恢复与初始化编排 |
| | | const router = Taro.useRouter(); |
| | | const tripRuntime = useTripRuntime({ |
| | |
| | | setBeforeUserEndSite: markBeforeUserEndSite, |
| | | isModifyDestinationReviewVisible, |
| | | hasLocalDestinationOverride, |
| | | isPickupRouteChangeVisible, |
| | | updatePreviousRoutePoints, |
| | | modifyDestinationPickupSuccessAtRef, |
| | | erasePassedRouteSegments: routeErasePassedRouteSegments, |
| | |
| | | markers: markersWithRouteSummary, |
| | | isRouteSelectExpanded, |
| | | isPickupRouteChangeVisible, |
| | | isConfirmingPickupRouteChange, |
| | | onToggleRouteSelect: setIsRouteSelectExpanded, |
| | | handleCancelPickupRouteChange: restorePickupRouteChangeSnapshot, |
| | | handleConfirmPickupRouteChange, |
| | | shouldShowWaitingRadar, |
| | | waitingRadarVisible, |
| | | waitingRadarStyle, |