feat(robotaxi):
- 新增订单ID参数到路线查询API与请求钩子,支持关联当前订单进行路线查询,同步更新修改目的地钩子的订单关联逻辑
- 修复获取附近站点时未传递type参数的问题,指定type:1为地图查询场景
- 优化无可用站点的提示文案,区分超出服务范围和无可选站点两种情况
- 添加页面级车门解锁状态追踪,订单切换时重置状态避免跨订单状态串用
- 蓝牙车控解锁成功的处理逻辑,统一同步UI状态并暴露回调接口
| | |
| | | startPointId: OrderId | ''; |
| | | /** 用户优惠券 ID。 */ |
| | | userCouponId?: OrderId | ''; |
| | | /** 当前订单 ID。 */ |
| | | orderId?: OrderId | ''; |
| | | } |
| | | |
| | | /** 路线与费用查询对象响应。 */ |
| | |
| | | tripStatus: TripStatus; |
| | | orderId?: string; |
| | | orderTrunkStatus?: boolean; |
| | | doorUnlockedInCurrentPage?: boolean; |
| | | handleOpenOverRoutePopup?: () => void; |
| | | onDoorUnlockSuccess?: () => void; |
| | | }; |
| | |
| | | tripStatus, |
| | | orderId, |
| | | orderTrunkStatus, |
| | | doorUnlockedInCurrentPage = false, |
| | | handleOpenOverRoutePopup, |
| | | onDoorUnlockSuccess |
| | | }: Props) { |
| | |
| | | setTrunkStatus(orderTrunkStatus); |
| | | }, [orderTrunkStatus]); |
| | | |
| | | // 订单切换后重置车锁展示态,避免上一单状态串到下一单 |
| | | useEffect(() => { |
| | | setLockStatus(false); |
| | | }, [orderId]); |
| | | |
| | | // 蓝牙或手动解锁成功后,同步车锁为打开态 |
| | | useEffect(() => { |
| | | if (doorUnlockedInCurrentPage) { |
| | | setLockStatus(true); |
| | | } |
| | | }, [doorUnlockedInCurrentPage]); |
| | | |
| | | // 组件卸载时清理轮询定时器 |
| | | useEffect(() => { |
| | | return () => { |
| | |
| | | }; |
| | | orderId?: string; |
| | | orderTrunkStatus?: boolean; |
| | | /** 当前页面内是否已经解锁过车门 */ |
| | | doorUnlockedInCurrentPage?: boolean; |
| | | bluetoothStatusReady?: boolean; |
| | | bluetoothCardDescription?: string; |
| | | bluetoothCardActionText?: string; |
| | |
| | | vehicleInfo, |
| | | orderId, |
| | | orderTrunkStatus, |
| | | doorUnlockedInCurrentPage = false, |
| | | bluetoothStatusReady = false, |
| | | bluetoothCardDescription = '授权并打开蓝牙链接,享受无感出行', |
| | | bluetoothCardActionText = '去开启', |
| | |
| | | tripStatus={tripStatus} |
| | | orderId={orderId} |
| | | orderTrunkStatus={orderTrunkStatus} |
| | | doorUnlockedInCurrentPage={doorUnlockedInCurrentPage} |
| | | handleOpenOverRoutePopup={handleOpenOverRoutePopup} |
| | | onDoorUnlockSuccess={onDoorUnlockSuccess} |
| | | /> |
| | |
| | | tripStatus={tripStatus === TripStatus.ArrivedDestination ? TripStatus.ArrivedDestination : -1} |
| | | orderId={orderId} |
| | | orderTrunkStatus={orderTrunkStatus} |
| | | doorUnlockedInCurrentPage={doorUnlockedInCurrentPage} |
| | | handleOpenOverRoutePopup={handleOpenOverRoutePopup} |
| | | onDoorUnlockSuccess={onDoorUnlockSuccess} |
| | | /> |
| | |
| | | type Deps = { |
| | | orderId: string; |
| | | tripStatus: TripStatus; |
| | | onBluetoothDoorUnlockSuccess?: () => void; |
| | | }; |
| | | |
| | | type BluetoothTokenPayload = { |
| | |
| | | /** |
| | | * 蓝牙车控编排:负责凭证获取、蓝牙连接和 RtAuth 鉴权。 |
| | | */ |
| | | export default function useBluetoothCarControl({ orderId, tripStatus }: Deps) { |
| | | export default function useBluetoothCarControl({ orderId, tripStatus, onBluetoothDoorUnlockSuccess }: Deps) { |
| | | const managerRef = useRef<any>(null); // 蓝牙管理器实例引用 |
| | | const lastStartKeyRef = useRef(''); // 最近一次启动鉴权的参数签名 |
| | | const startAuthTaskRef = useRef<Promise<void> | null>(null); // 当前进行中的鉴权任务,防止并发重复请求 |
| | |
| | | }); |
| | | }; // 统一展示蓝牙解锁结果提示 |
| | | latestTripStatusRef.current = tripStatus; |
| | | |
| | | /** |
| | | * 收口蓝牙真实解锁成功后的状态、提示与外部 UI 联动。 |
| | | */ |
| | | const handleBluetoothDoorUnlockSuccess = () => { |
| | | if (unlockFinishedRef.current) { |
| | | return; |
| | | } |
| | | unlockFinishedRef.current = true; |
| | | setAuthError(''); |
| | | onBluetoothDoorUnlockSuccess?.(); |
| | | const storageKey = `ROBOTAXI_UNLOCK_TOAST_SHOWN_${orderId}`; |
| | | if (!Taro.getStorageSync(storageKey)) { |
| | | showUnlockToast(UNLOCK_SUCCESS_MESSAGE); |
| | | Taro.setStorageSync(storageKey, true); |
| | | } |
| | | closeConnectionRef.current(); |
| | | }; |
| | | |
| | | /** |
| | | * 判断当前蓝牙异步回调是否仍属于有效轮次。 |
| | |
| | | if (result?.type === 'pollingUnlock') { |
| | | const isAlreadyUnlocked = Number(result?.data?.result) === 2; // RtNotification 挂载在 data,result=2 表示车辆已解锁 |
| | | if (isAlreadyUnlocked) { |
| | | if (unlockFinishedRef.current) { |
| | | return; |
| | | } |
| | | unlockFinishedRef.current = true; |
| | | setAuthError(''); |
| | | const storageKey = `ROBOTAXI_UNLOCK_TOAST_SHOWN_${orderId}`; |
| | | if (!Taro.getStorageSync(storageKey)) { |
| | | showUnlockToast(UNLOCK_SUCCESS_MESSAGE); |
| | | Taro.setStorageSync(storageKey, true); |
| | | } |
| | | closeConnectionRef.current(); |
| | | handleBluetoothDoorUnlockSuccess(); |
| | | return; |
| | | } |
| | | if (result?.success) { |
| | | if (unlockFinishedRef.current) { |
| | | return; |
| | | } |
| | | unlockFinishedRef.current = true; |
| | | setAuthError(''); |
| | | const storageKey = `ROBOTAXI_UNLOCK_TOAST_SHOWN_${orderId}`; |
| | | if (!Taro.getStorageSync(storageKey)) { |
| | | showUnlockToast(UNLOCK_SUCCESS_MESSAGE); |
| | | Taro.setStorageSync(storageKey, true); |
| | | } |
| | | closeConnectionRef.current(); |
| | | handleBluetoothDoorUnlockSuccess(); |
| | | } else { |
| | | setAuthError(RETRY_UNLOCK_MESSAGE); |
| | | showUnlockToast(RETRY_UNLOCK_MESSAGE); |
| | |
| | | import Taro from '@tarojs/taro'; |
| | | import { useCallback, useEffect, useRef, useState } from 'react'; |
| | | import { TripStatus } from '../../../constants/trip-status'; |
| | | import { getOrderId } from '../../../utils/order'; |
| | | |
| | | /** 已选优惠券缓存键。 */ |
| | | const SELECTED_COUPON_ID_STORAGE_KEY = 'selectedCouponId'; |
| | |
| | | type RequestRouteOptions = { |
| | | startPointId?: string | number; |
| | | force?: boolean; |
| | | orderId?: string; |
| | | syncOrderRouteMetrics?: boolean; |
| | | }; |
| | | |
| | | type Deps = { |
| | | tripStatus: TripStatus; |
| | | orderId?: string; |
| | | isModifyDestinationReviewVisible: boolean; |
| | | hasLocalDestinationOverride: boolean; |
| | | effectiveEndSite: any; |
| | |
| | | destLat: number; |
| | | destLng: number; |
| | | startPointId: string | number; |
| | | orderId: string; |
| | | requestKey: string; |
| | | }; |
| | | |
| | |
| | | if (startPointId === '' || startPointId === null || startPointId === undefined) { |
| | | return null; |
| | | } |
| | | const currentOrderId = getOrderId(deps.orderId); |
| | | return { |
| | | originLat, |
| | | originLng, |
| | | destLat, |
| | | destLng, |
| | | startPointId, |
| | | requestKey: `${deps.tripStatus}|${Number(targetEndSite?.id || 0)}|${startPointId}|${originLng},${originLat}-${destLng},${destLat}` |
| | | orderId: currentOrderId, |
| | | requestKey: `${deps.tripStatus}|${currentOrderId}|${Number(targetEndSite?.id || 0)}|${startPointId}|${originLng},${originLat}-${destLng},${destLat}` |
| | | }; |
| | | }; |
| | | |
| | |
| | | { |
| | | startPointId: routeContext.startPointId, |
| | | force: true, |
| | | orderId: routeContext.orderId, |
| | | syncOrderRouteMetrics: false |
| | | }, |
| | | ); |
| | |
| | | { |
| | | startPointId: routeContext.startPointId, |
| | | force: true, |
| | | orderId: routeContext.orderId, |
| | | syncOrderRouteMetrics: false |
| | | }, |
| | | ); |
| | |
| | | couponId?: string; |
| | | /** 起点站点 ID。 */ |
| | | startPointId?: string | number; |
| | | /** 当前订单 ID。 */ |
| | | orderId?: string; |
| | | /** 是否同步原订单剩余里程/时长指标。 */ |
| | | syncOrderRouteMetrics?: boolean; |
| | | }; |
| | |
| | | const refreshMode = resolvedOptions.refreshMode || 'full'; |
| | | const startPointId = resolvedOptions.startPointId ?? ''; |
| | | const syncOrderRouteMetrics = resolvedOptions.syncOrderRouteMetrics !== false; |
| | | const targetOrderId = String(resolvedOptions.orderId || '').trim(); |
| | | |
| | | const now = Date.now(); |
| | | if (now - lastRouteAndFareRequestAtRef.current < 1000) { |
| | |
| | | if (couponId) { |
| | | requestParams.userCouponId = couponId; |
| | | } |
| | | if (targetOrderId) { |
| | | requestParams.orderId = targetOrderId; |
| | | } |
| | | |
| | | const response = await queryRouteAndFare(requestParams); |
| | | setRouteAndFareError(false); |
| | |
| | | onToggleRouteSelect: (expanded: boolean) => void; |
| | | /** 后备箱状态 */ |
| | | trunkStatus: boolean; |
| | | /** 当前页面内是否已经解锁过车门 */ |
| | | doorUnlockedInCurrentPage: boolean; |
| | | /** 路线选项集合 */ |
| | | routeOptions: any[]; |
| | | /** 选中路线索引 */ |
| | |
| | | routeDurationSeconds={deps.routeDuration} |
| | | onToggleDetailFade={deps.handleToggleDetailFade} |
| | | orderTrunkStatus={deps.trunkStatus} |
| | | doorUnlockedInCurrentPage={deps.doorUnlockedInCurrentPage} |
| | | bluetoothStatusReady={deps.bluetoothStatusReady} |
| | | bluetoothCardDescription={deps.bluetoothCardDescription} |
| | | bluetoothCardActionText={deps.bluetoothCardActionText} |
| | |
| | | const requoteTriggeredRef = useRef(false); // 重新报价触发标记,防止重复触发 |
| | | const [callSubmitting, setCallSubmitting] = useState(false); // 呼叫车辆提交中状态 |
| | | const [orderId, setOrderId] = useState<string>(''); // 订单ID |
| | | const [doorUnlockedInCurrentPage, setDoorUnlockedInCurrentPage] = useState(false); // 当前页面内是否已解锁过车门 |
| | | const [finishCountdownEnabled, setFinishCountdownEnabled] = useState(false); // 完成倒计时是否启用 |
| | | 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 |
| | |
| | | Taro.setStorageSync('tripStatus', TripStatus.PaymentSuccess); |
| | | }, []); |
| | | |
| | | /** |
| | | * 订单切换后重置当前页面内的车门打开态,避免串单。 |
| | | */ |
| | | useEffect(() => { |
| | | setDoorUnlockedInCurrentPage(false); |
| | | }, [orderId]); |
| | | |
| | | /** |
| | | * 蓝牙真实解锁成功后,同步当前页面的车锁打开态。 |
| | | */ |
| | | const handleBluetoothDoorUnlockSuccess = useCallback(() => { |
| | | setDoorUnlockedInCurrentPage(true); |
| | | }, []); |
| | | |
| | | const { queryPaymentResult: queryOrderPaymentResult } = useOrderPaymentResultQuery({ |
| | | orderId, |
| | | onPaySuccess: handlePaymentSuccess |
| | |
| | | markManualDoorUnlock |
| | | } = useBluetoothCarControl({ |
| | | orderId, |
| | | tripStatus |
| | | tripStatus, |
| | | onBluetoothDoorUnlockSuccess: handleBluetoothDoorUnlockSuccess |
| | | }); |
| | | /** |
| | | * 手动点击开门成功后,停止后续蓝牙自动解锁流程。 |
| | | * 手动点击开门成功后,同步车锁打开态并停止后续蓝牙自动解锁流程。 |
| | | */ |
| | | const handleManualDoorUnlockSuccess = useCallback(() => { |
| | | setDoorUnlockedInCurrentPage(true); |
| | | markManualDoorUnlock(); |
| | | }, [markManualDoorUnlock]); |
| | | const { |
| | |
| | | }); |
| | | const { handleConfirmRequoteIfNeeded } = useModifyDestinationPreviewRoute({ |
| | | tripStatus, |
| | | orderId, |
| | | isModifyDestinationReviewVisible, |
| | | hasLocalDestinationOverride, |
| | | effectiveEndSite, |
| | |
| | | showModifyDestination, |
| | | handleShareTrip, |
| | | trunkStatus, |
| | | doorUnlockedInCurrentPage, |
| | | routeOptions, |
| | | selectedRouteIndex, |
| | | handleSelectRoute, |
| | |
| | | <Image onClick={deps.handleCollect} className='collect-icon' src={deps.selectedSite.isCollect === 1 ? deps.activeStar : deps.noStar} /> |
| | | )} |
| | | </View> |
| | | ) : ( |
| | | ) : deps.outOfServiceRange ? ( |
| | | <View className="no-service">超出服务范围,请在运营区内选择</View> |
| | | )} |
| | | ) : <View className="no-service">附近无可选站点</View> |
| | | } |
| | | </View> |
| | | </ScrollView> |
| | | </View> |
| | |
| | | const isInside = fenceRes.status === 'inside'; |
| | | const electronicAreaId = fenceRes?.electronicAreaId || ''; |
| | | |
| | | // 2. 只有在运营区域内才获取附近站点 |
| | | // 2. 只有在运营区域内才获取附近站点 type 传1 地图查询附近站点 不传 查搜索页附近站点 |
| | | let sites: any = { points: [] }; |
| | | if (isInside) { |
| | | sites = await getNearestSite({ latitude: lat, longitude: lng, electronicAreaId, pointOnOff }).catch(e => { |
| | | sites = await getNearestSite({ latitude: lat, longitude: lng, electronicAreaId, pointOnOff, type:1 }).catch(e => { |
| | | return { points: [] }; |
| | | }); |
| | | // 兼容处理 |