feat(begin-route): 新增订单开门状态管理
| | |
| | | * 页面行程状态枚举:与历史 number 值保持 1:1 对齐,避免行为变化。 |
| | | */ |
| | | export enum TripStatus { |
| | | PublishRoute = 1, // 发布行程 |
| | | Publishing = 2, // 发布中 |
| | | CallingCar = 3, // 呼叫车中 |
| | | VehicleApproaching = 4, // 车辆即将到达 |
| | | VehicleArrivedPickup = 5, // 车辆到达上车点 |
| | | InService = 6, // 车辆行驶中 |
| | | ArrivedDestination = 7, // 到达目的地 |
| | | TripFinished = 8, // 行程已结束 |
| | | PaymentSuccess = 9, // 支付成功 |
| | | CancelOnlyPickup = 10, // 取消订单时只展示上车点 |
| | | /** 发布行程 */ |
| | | PublishRoute = 1, |
| | | /** 发布中 */ |
| | | Publishing = 2, |
| | | /** 呼叫车中 */ |
| | | CallingCar = 3, |
| | | /** 车辆即将到达 */ |
| | | VehicleApproaching = 4, |
| | | /** 车辆到达上车点 */ |
| | | VehicleArrivedPickup = 5, |
| | | /** 车辆行驶中 */ |
| | | InService = 6, |
| | | /** 到达目的地 */ |
| | | ArrivedDestination = 7, |
| | | /** 行程已结束 */ |
| | | TripFinished = 8, |
| | | /** 支付成功 */ |
| | | PaymentSuccess = 9, |
| | | /** 取消订单时只展示上车点 */ |
| | | CancelOnlyPickup = 10, |
| | | } |
| | | |
| | | /** |
| | | * socket 订单状态枚举:对应后端推送 status 字段。 |
| | | */ |
| | | export enum SocketOrderStatus { |
| | | Canceled = 0, // 已取消 |
| | | PendingAccept = 1, // 待接单 |
| | | PendingDeparture = 2, // 待出发 |
| | | PickupOnTheWay = 3, // 接驾中 |
| | | WaitingPassenger = 4, // 待客上车 |
| | | PassengerOnboard = 5, // 乘客已上车 |
| | | InProgress = 6, // 服务中 |
| | | WaitingDropoff = 7, // 待客下车 |
| | | PendingPayment = 8, // 待付款 |
| | | PendingComment = 9, // 待评价 |
| | | Completed = 10, // 已完成 |
| | | /** 已取消 */ |
| | | Canceled = 0, |
| | | /** 待接单 */ |
| | | PendingAccept = 1, |
| | | /** 待出发 */ |
| | | PendingDeparture = 2, |
| | | /** 接驾中 */ |
| | | PickupOnTheWay = 3, |
| | | /** 待客上车 */ |
| | | WaitingPassenger = 4, |
| | | /** 乘客已上车 */ |
| | | PassengerOnboard = 5, |
| | | /** 服务中 */ |
| | | InProgress = 6, |
| | | /** 待客下车 */ |
| | | WaitingDropoff = 7, |
| | | /** 待付款 */ |
| | | PendingPayment = 8, |
| | | /** 待评价 */ |
| | | PendingComment = 9, |
| | | /** 已完成 */ |
| | | Completed = 10, |
| | | } |
| | | |
| | | const TRIP_STATUS_SET = new Set<number>(Object.values(TripStatus).filter((value) => typeof value === 'number') as number[]); // 行程状态值集合 |
| | |
| | | handleModifyDestination?: () => void; |
| | | handleShareTrip?: () => void; |
| | | showModifyDestination?: boolean; |
| | | showCancelTrip?: boolean; |
| | | }; |
| | | |
| | | export default function TripActionBar({ |
| | |
| | | handleCancelTrip, |
| | | handleModifyDestination, |
| | | handleShareTrip, |
| | | showModifyDestination = true |
| | | showModifyDestination = true, |
| | | showCancelTrip = tripStatus === TripStatus.VehicleApproaching |
| | | }: Props) { |
| | | // 按行程状态控制按钮集合,避免在 JSX 中堆叠过多条件判断 |
| | | const actionList = [ |
| | |
| | | key: "cancel", |
| | | label: "取消行程", |
| | | icon: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/cancel-trip.png', |
| | | visible: tripStatus === TripStatus.VehicleApproaching, |
| | | visible: showCancelTrip, |
| | | onClick: handleCancelTrip |
| | | }, |
| | | { |
| | |
| | | durationDisplay: { value: string | number; unit: string }; |
| | | isArriveSoon: boolean; |
| | | isPickupCountdownSoon: boolean; |
| | | hasDoorOpenedInCurrentOrder: boolean; |
| | | pickupCountdownSeconds: number | null; |
| | | finishCountdownEnabled: boolean; |
| | | finishCountdownSeconds: number | null; |
| | |
| | | durationDisplay, |
| | | isArriveSoon, |
| | | isPickupCountdownSoon, |
| | | hasDoorOpenedInCurrentOrder, |
| | | pickupCountdownSeconds, |
| | | finishCountdownEnabled, |
| | | finishCountdownSeconds, |
| | |
| | | [TripStatus.VehicleApproaching]: isArriveSoon |
| | | ? <>车辆即将到达,请准备上车</> |
| | | : <>车辆还有<View className="time">{durationDisplay.value}</View>{durationDisplay.unit}到达</>, |
| | | [TripStatus.VehicleArrivedPickup]: <>{isPickupCountdownSoon ? '车辆已到达,请尽快上车' : '车辆已到达上车点'}<View className="time" style={isPickupCountdownSoon ? { color: '#f58900' } : undefined}>{pickupCountdownSeconds !== null ? formatCountdown(pickupCountdownSeconds) : '--:--'}</View></>, |
| | | [TripStatus.VehicleArrivedPickup]: hasDoorOpenedInCurrentOrder |
| | | ? <>车辆已到达,请尽快上车</> |
| | | : <>{isPickupCountdownSoon ? '车辆已到达,请尽快上车' : '车辆已到达上车点'}<View className="time" style={isPickupCountdownSoon ? { color: '#f58900' } : undefined}>{pickupCountdownSeconds !== null ? formatCountdown(pickupCountdownSeconds) : '--:--'}</View></>, |
| | | [TripStatus.InService]: <>预计<View className="time">{inServiceDurationDisplay.value}</View>{inServiceDurationDisplay.unit}抵达目的地</>, |
| | | [TripStatus.ArrivedDestination]: (finishCountdownEnabled || finishCountdownSeconds !== null) |
| | | ? <>已到达目的地<View className="time">{finishCountdownSeconds !== null ? formatCountdown(finishCountdownSeconds) : '--:--'}</View></> |
| | |
| | | const descMap = { |
| | | [TripStatus.CallingCar]: { text: '正全力为您叫车中,请耐心等待...', className: 'calling-car-content-desc' }, |
| | | [TripStatus.VehicleApproaching]: limitMinutes ? { text: `此处限停${limitMinutes}分钟,请尽快前往上车点`, className: 'calling-car-content-warning' } : null, |
| | | [TripStatus.VehicleArrivedPickup]: { text: isPickupCountdownSoon ? `车辆到达后最多等待${limitMinutes}分钟,超时自动取消` : '此处限停,请在倒计时结束前到达上车点', className: 'calling-car-content-desc' }, |
| | | [TripStatus.VehicleArrivedPickup]: hasDoorOpenedInCurrentOrder |
| | | ? { text: '车门已解锁,已停止等待倒计时', className: 'calling-car-content-desc' } |
| | | : { text: isPickupCountdownSoon ? `车辆到达后最多等待${limitMinutes}分钟,超时自动取消` : '此处限停,请在倒计时结束前到达上车点', className: 'calling-car-content-desc' }, |
| | | [TripStatus.InService]: { text: '行程已开始,请全程系好安全带', className: 'calling-car-content-desc' }, |
| | | [TripStatus.ArrivedDestination]: { text: '倒计时结束时车辆将自动驶离,请带好物品安全下车', className: 'calling-car-content-desc' }, |
| | | [TripStatus.TripFinished]: { text: '订单待支付,请核对后完成支付', className: 'calling-car-content-desc' } |
| | |
| | | orderId?: string; |
| | | estimatedWaitMinutes?: number; |
| | | finishCountdownEnabled: boolean; |
| | | hasDoorOpenedInCurrentOrder: boolean; |
| | | handleCancelTimeout?: () => void; |
| | | handleFinishTimeout?: () => void; |
| | | priceUpdateVersion?: number; |
| | |
| | | orderId, |
| | | estimatedWaitMinutes, |
| | | finishCountdownEnabled, |
| | | hasDoorOpenedInCurrentOrder, |
| | | handleCancelTimeout, |
| | | handleFinishTimeout, |
| | | priceUpdateVersion |
| | |
| | | const finishTimerRef = useRef<any>(null); |
| | | const countdownFinishedRef = useRef(false); |
| | | const pickupTripStatusRef = useRef(tripStatus); |
| | | const pickupDoorOpenedRef = useRef(hasDoorOpenedInCurrentOrder); |
| | | const pickupEndTimeRef = useRef<number | null>(null); |
| | | const finishEndTimeRef = useRef<number | null>(null); |
| | | const lastFareRequestKeyRef = useRef<string | null>(null); // 费用明细请求去重键(订单+优惠券) |
| | |
| | | }, [tripStatus]); |
| | | |
| | | useEffect(() => { |
| | | pickupDoorOpenedRef.current = hasDoorOpenedInCurrentOrder; |
| | | }, [hasDoorOpenedInCurrentOrder]); |
| | | |
| | | const stopPickupCountdown = useCallback((clearPersistedEndTime = false) => { |
| | | clearPickupTimer(); |
| | | countdownFinishedRef.current = false; |
| | | pickupEndTimeRef.current = null; |
| | | setPickupCountdownSeconds(null); |
| | | if (clearPersistedEndTime) { |
| | | Taro.removeStorageSync(PICKUP_END_TIME_KEY); |
| | | } |
| | | }, [clearPickupTimer]); |
| | | |
| | | useEffect(() => { |
| | | if (tripStatus !== TripStatus.VehicleArrivedPickup) { |
| | | clearPickupTimer(); |
| | | return; |
| | | } |
| | | if (hasDoorOpenedInCurrentOrder) { |
| | | stopPickupCountdown(true); |
| | | return; |
| | | } |
| | | |
| | |
| | | // 防止状态已切换后,旧定时回调仍触发取消逻辑 |
| | | if (pickupTripStatusRef.current !== TripStatus.VehicleArrivedPickup) { |
| | | clearPickupTimer(); |
| | | return; |
| | | } |
| | | if (pickupDoorOpenedRef.current) { |
| | | stopPickupCountdown(true); |
| | | return; |
| | | } |
| | | const remaining = getRemainingSeconds(pickupEndTimeRef.current); |
| | |
| | | return () => { |
| | | clearPickupTimer(); |
| | | }; |
| | | }, [tripStatus, orderId, handleCancelTimeout, clearPickupTimer]); |
| | | }, [tripStatus, orderId, hasDoorOpenedInCurrentOrder, handleCancelTimeout, clearPickupTimer, stopPickupCountdown]); |
| | | |
| | | useEffect(() => { |
| | | if (tripStatus !== TripStatus.ArrivedDestination) { |
| | |
| | | handlePay?: () => void; |
| | | /** 打开详细评价弹窗 */ |
| | | handleOpenEvaluatePopup?: () => void; |
| | | /** 当前订单是否已开过门 */ |
| | | hasDoorOpenedInCurrentOrder?: boolean; |
| | | /** 取消行程 */ |
| | | handleCancelTrip?: () => void; |
| | | /** 是否展示取消行程 */ |
| | | showCancelTrip?: boolean; |
| | | /** 修改目的地 */ |
| | | handleModifyDestination?: () => void; |
| | | /** 是否展示修改目的地 */ |
| | |
| | | onToggleDetailFade, |
| | | handlePay, |
| | | handleOpenEvaluatePopup, |
| | | hasDoorOpenedInCurrentOrder = false, |
| | | handleCancelTrip, |
| | | showCancelTrip = tripStatus === TripStatus.VehicleApproaching, |
| | | handleModifyDestination, |
| | | showModifyDestination = true, |
| | | handleShareTrip, |
| | |
| | | orderId, |
| | | estimatedWaitMinutes, |
| | | finishCountdownEnabled, |
| | | hasDoorOpenedInCurrentOrder, |
| | | handleCancelTimeout, |
| | | handleFinishTimeout, |
| | | priceUpdateVersion |
| | |
| | | durationDisplay={durationDisplay} |
| | | isArriveSoon={isArriveSoon} |
| | | isPickupCountdownSoon={isPickupCountdownSoon} |
| | | hasDoorOpenedInCurrentOrder={hasDoorOpenedInCurrentOrder} |
| | | pickupCountdownSeconds={pickupCountdownSeconds} |
| | | finishCountdownEnabled={finishCountdownEnabled} |
| | | finishCountdownSeconds={finishCountdownSeconds} |
| | |
| | | <TripActionBar |
| | | tripStatus={tripStatus} |
| | | handleCancelTrip={handleCancelTrip} |
| | | showCancelTrip={showCancelTrip} |
| | | handleModifyDestination={handleModifyDestination} |
| | | showModifyDestination={showModifyDestination} |
| | | handleShareTrip={handleShareTrip} |
| | |
| | | <View className="route-content"> |
| | | <View className="route-content-top"> |
| | | <View className="route-content-txt">预估等待{fareEstimate.waitingTime || '-'}分钟</View> |
| | | <View style={{ margin: '0 4px' }}>|</View> |
| | | <View className="route-content-txt" style={{ margin: '0 4px' }}>|</View> |
| | | <View className="route-content-txt">限乘{fareEstimate.maxPeopleNumber || '-'}人</View> |
| | | </View> |
| | | <View className="route-content-bottom"> |
| New file |
| | |
| | | import Taro from '@tarojs/taro'; |
| | | |
| | | /** 订单开门标记缓存键前缀。 */ |
| | | const ORDER_DOOR_OPENED_STORAGE_KEY_PREFIX = 'ROBOTAXI_ORDER_DOOR_OPENED_'; |
| | | |
| | | /** |
| | | * 生成订单开门标记缓存键。 |
| | | */ |
| | | const getOrderDoorOpenedStorageKey = (orderId?: string) => { |
| | | const normalizedOrderId = String(orderId || '').trim(); |
| | | if (!normalizedOrderId) { |
| | | return ''; |
| | | } |
| | | return `${ORDER_DOOR_OPENED_STORAGE_KEY_PREFIX}${normalizedOrderId}`; |
| | | }; |
| | | |
| | | /** |
| | | * 读取当前订单是否已经开过门。 |
| | | */ |
| | | export const getOrderDoorOpenedFlag = (orderId?: string) => { |
| | | const storageKey = getOrderDoorOpenedStorageKey(orderId); |
| | | if (!storageKey) { |
| | | return false; |
| | | } |
| | | return Boolean(Taro.getStorageSync(storageKey)); |
| | | }; |
| | | |
| | | /** |
| | | * 标记当前订单已经开过门。 |
| | | */ |
| | | export const setOrderDoorOpenedFlag = (orderId?: string) => { |
| | | const storageKey = getOrderDoorOpenedStorageKey(orderId); |
| | | if (!storageKey) { |
| | | return; |
| | | } |
| | | Taro.setStorageSync(storageKey, true); |
| | | }; |
| | | |
| | | /** |
| | | * 清理当前订单的开门标记。 |
| | | */ |
| | | export const clearOrderDoorOpenedFlag = (orderId?: string) => { |
| | | const storageKey = getOrderDoorOpenedStorageKey(orderId); |
| | | if (!storageKey) { |
| | | return; |
| | | } |
| | | Taro.removeStorageSync(storageKey); |
| | | }; |
| | |
| | | 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; |
| | | |
| | | /** |
| | | * 收口蓝牙真实解锁成功后的状态与提示。 |
| | | */ |
| | | 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) { |
| | | 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) { |
| | | 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); |
| | |
| | | createPickupArrivedCancelTripReportPayload |
| | | } from '../../../constants/confirm-ride-report'; |
| | | import { OrderActionsDeps } from './use-order-actions.shared'; |
| | | import { clearOrderDoorOpenedFlag } from './order-door-open-storage'; |
| | | |
| | | /** 等待计时开始时间缓存键。 */ |
| | | const WAITING_TIMER_START_STORAGE_KEY = 'waitingTimerStartAt'; |
| | |
| | | * 清理活跃订单运行时缓存。 |
| | | */ |
| | | const clearActiveOrderRuntime = () => { |
| | | clearOrderDoorOpenedFlag(deps.orderId); |
| | | deps.setOrderId(''); |
| | | deps.setVehicleInfo({}); |
| | | clearOrderTripStorage(); |
| New file |
| | |
| | | import { useCallback, useEffect, useState } from 'react'; |
| | | import { TripStatus } from '../../../constants/trip-status'; |
| | | import { getOrderId } from '../../../utils/order'; |
| | | import { |
| | | clearOrderDoorOpenedFlag, |
| | | getOrderDoorOpenedFlag, |
| | | setOrderDoorOpenedFlag |
| | | } from './order-door-open-storage'; |
| | | |
| | | /** Hook 入参。 */ |
| | | type Deps = { |
| | | /** 当前订单 ID。 */ |
| | | orderId: string; |
| | | /** 当前行程状态。 */ |
| | | tripStatus: TripStatus; |
| | | /** 页面展示版本号。 */ |
| | | pageDidShowVersion: number; |
| | | }; |
| | | |
| | | /** Hook 返回值。 */ |
| | | type UseOrderDoorOpenStateResult = { |
| | | /** 当前订单是否已开过门。 */ |
| | | hasDoorOpenedInCurrentOrder: boolean; |
| | | /** 标记当前订单已开门。 */ |
| | | markCurrentOrderDoorOpened: (targetOrderId?: string) => void; |
| | | /** 清理当前订单开门标记。 */ |
| | | clearCurrentOrderDoorOpened: (targetOrderId?: string) => void; |
| | | }; |
| | | |
| | | /** |
| | | * 管理当前订单“是否已经开过门”的页面级业务状态。 |
| | | */ |
| | | export default function useOrderDoorOpenState({ |
| | | orderId, |
| | | tripStatus, |
| | | pageDidShowVersion |
| | | }: Deps): UseOrderDoorOpenStateResult { |
| | | const [hasDoorOpenedInCurrentOrder, setHasDoorOpenedInCurrentOrder] = useState(false); // 当前订单是否已开过门 |
| | | |
| | | /** |
| | | * 按订单同步当前“已开门”标记,支持退出页面再进入后的状态恢复。 |
| | | */ |
| | | const syncOrderDoorOpenedState = useCallback((targetOrderId?: string) => { |
| | | const resolvedOrderId = getOrderId(targetOrderId || orderId); |
| | | if (!resolvedOrderId) { |
| | | setHasDoorOpenedInCurrentOrder(false); |
| | | return; |
| | | } |
| | | setHasDoorOpenedInCurrentOrder(getOrderDoorOpenedFlag(resolvedOrderId)); |
| | | }, [orderId]); |
| | | |
| | | /** |
| | | * 标记当前订单已经开过门。 |
| | | */ |
| | | const markCurrentOrderDoorOpened = useCallback((targetOrderId?: string) => { |
| | | const resolvedOrderId = getOrderId(targetOrderId || orderId); |
| | | if (!resolvedOrderId) { |
| | | return; |
| | | } |
| | | setOrderDoorOpenedFlag(resolvedOrderId); |
| | | setHasDoorOpenedInCurrentOrder(true); |
| | | }, [orderId]); |
| | | |
| | | /** |
| | | * 清理当前订单的开门标记。 |
| | | */ |
| | | const clearCurrentOrderDoorOpened = useCallback((targetOrderId?: string) => { |
| | | const resolvedOrderId = getOrderId(targetOrderId || orderId); |
| | | if (!resolvedOrderId) { |
| | | setHasDoorOpenedInCurrentOrder(false); |
| | | return; |
| | | } |
| | | clearOrderDoorOpenedFlag(resolvedOrderId); |
| | | setHasDoorOpenedInCurrentOrder(false); |
| | | }, [orderId]); |
| | | |
| | | /** |
| | | * 页面展示或订单切换后,重新同步一次开门标记,避免页面重进丢失。 |
| | | */ |
| | | useEffect(() => { |
| | | syncOrderDoorOpenedState(orderId); |
| | | }, [orderId, pageDidShowVersion, syncOrderDoorOpenedState]); |
| | | |
| | | /** |
| | | * 订单进入终态后,主动清理本单开门标记,避免缓存长期残留。 |
| | | */ |
| | | useEffect(() => { |
| | | if (![TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(tripStatus)) { |
| | | return; |
| | | } |
| | | clearCurrentOrderDoorOpened(orderId); |
| | | }, [tripStatus, orderId, clearCurrentOrderDoorOpened]); |
| | | |
| | | return { |
| | | hasDoorOpenedInCurrentOrder, |
| | | markCurrentOrderDoorOpened, |
| | | clearCurrentOrderDoorOpened |
| | | }; |
| | | } |
| | |
| | | import { TRIP_COMPLETED_PAGE_VIEW_EVENT_ID, TRIP_IN_PROGRESS_PAY_START_BUTTON_EVENT_ID } from '../../../constants/report-events'; |
| | | import { SocketOrderStatus, TripStatus } from '../../../constants/trip-status'; |
| | | import { OrderActionsDeps } from './use-order-actions.shared'; |
| | | import { clearOrderDoorOpenedFlag } from './order-door-open-storage'; |
| | | |
| | | /** 支付 Hook 返回值。 */ |
| | | type UseOrderPaymentResult = { |
| | |
| | | * 同步支付成功态。 |
| | | */ |
| | | const syncPaymentSuccessStatus = () => { |
| | | clearOrderDoorOpenedFlag(deps.orderId); |
| | | deps.setTripStatus(TripStatus.PaymentSuccess); |
| | | Taro.setStorageSync('tripStatus', TripStatus.PaymentSuccess); |
| | | }; |
| | |
| | | * 同步行程结束待支付态。 |
| | | */ |
| | | const syncTripFinishedStatus = () => { |
| | | clearOrderDoorOpenedFlag(deps.orderId); |
| | | deps.setTripStatus(TripStatus.TripFinished); |
| | | Taro.setStorageSync('tripStatus', TripStatus.TripFinished); |
| | | }; |
| | |
| | | force?: boolean; |
| | | }; |
| | | |
| | | type RestoredPageState = { |
| | | orderId: string; |
| | | tripStatus: TripStatus; |
| | | startSite: any; |
| | | endSite: any; |
| | | }; |
| | | |
| | | type Deps = { |
| | | wsHost: string; |
| | | ensureSocketListeners: (wsHost: string) => void; |
| | |
| | | /** |
| | | * 优先使用活跃订单回填上下车点与行程状态。 |
| | | */ |
| | | const syncSitesFromActiveOrder = (cachedTripStatusStorage: any, s: any, e: any) => { |
| | | const syncSitesFromActiveOrder = async ( |
| | | cachedTripStatusStorage: any, |
| | | cachedOrderId: string, |
| | | cachedTripStatus: TripStatus, |
| | | s: any, |
| | | e: any |
| | | ): Promise<RestoredPageState> => { |
| | | const fallbackState: RestoredPageState = { |
| | | orderId: cachedOrderId, |
| | | tripStatus: cachedTripStatus, |
| | | startSite: s, |
| | | endSite: e |
| | | }; |
| | | if (!(cachedTripStatusStorage && cachedTripStatusStorage != 1)) { |
| | | if (s) deps.setStartSite(s); |
| | | if (e) deps.setEndSite(e); |
| | | return; |
| | | return fallbackState; |
| | | } |
| | | getOrderActive() |
| | | .then((res: any) => { |
| | | if (!res) return; |
| | | deps.setTrunkStatus(![0, 2].includes(res.openTrunk)); |
| | | const nextStartSite = { |
| | | id: res.startPointId || '', |
| | | name: res.startPointName || '', |
| | | latitude: res.startLat || 0, |
| | | longitude: res.startLon || 0, |
| | | electronicAreaId: res.xpElectronicFenceAreaId || '', |
| | | time: res.startParkingTimeLimit || 0, |
| | | }; |
| | | const nextEndSite = { |
| | | id: res.endPointId || '', |
| | | name: res.endPointName || '', |
| | | latitude: res.endLat || 0, |
| | | longitude: res.endLon || 0, |
| | | electronicAreaId: res.xpElectronicFenceAreaId || '', |
| | | time: res.endParkingTimeLimit || 0, |
| | | }; |
| | | const nextStatus = toTripStatus(mapOrderStatusToTripStatus(res.status || 1)); |
| | | Taro.setStorageSync('tripStatus', nextStatus); |
| | | deps.setTripStatus(nextStatus); |
| | | Taro.setStorageSync('startSite', nextStartSite); |
| | | Taro.setStorageSync('endSite', nextEndSite); |
| | | deps.setStartSite(nextStartSite); |
| | | deps.setEndSite(nextEndSite); |
| | | }) |
| | | .catch(() => { }); |
| | | |
| | | try { |
| | | const res: any = await getOrderActive(); |
| | | if (!res) { |
| | | if (s) deps.setStartSite(s); |
| | | if (e) deps.setEndSite(e); |
| | | return fallbackState; |
| | | } |
| | | |
| | | deps.setTrunkStatus(![0, 2].includes(res.openTrunk)); |
| | | const nextStartSite = { |
| | | id: res.startPointId || '', |
| | | name: res.startPointName || '', |
| | | latitude: res.startLat || 0, |
| | | longitude: res.startLon || 0, |
| | | electronicAreaId: res.xpElectronicFenceAreaId || '', |
| | | time: res.startParkingTimeLimit || 0, |
| | | }; |
| | | const nextEndSite = { |
| | | id: res.endPointId || '', |
| | | name: res.endPointName || '', |
| | | latitude: res.endLat || 0, |
| | | longitude: res.endLon || 0, |
| | | electronicAreaId: res.xpElectronicFenceAreaId || '', |
| | | time: res.endParkingTimeLimit || 0, |
| | | }; |
| | | const nextStatus = toTripStatus(mapOrderStatusToTripStatus(res.status || 1)); |
| | | const nextOrderId = String(res.id || cachedOrderId || ''); |
| | | Taro.setStorageSync('tripStatus', nextStatus); |
| | | deps.setTripStatus(nextStatus); |
| | | Taro.setStorageSync('startSite', nextStartSite); |
| | | Taro.setStorageSync('endSite', nextEndSite); |
| | | if (nextOrderId) { |
| | | Taro.setStorageSync('orderId', nextOrderId); |
| | | deps.setOrderId(nextOrderId); |
| | | } |
| | | deps.setStartSite(nextStartSite); |
| | | deps.setEndSite(nextEndSite); |
| | | return { |
| | | orderId: nextOrderId, |
| | | tripStatus: nextStatus, |
| | | startSite: nextStartSite, |
| | | endSite: nextEndSite |
| | | }; |
| | | } catch (error) { |
| | | if (s) deps.setStartSite(s); |
| | | if (e) deps.setEndSite(e); |
| | | return fallbackState; |
| | | } |
| | | }; |
| | | |
| | | /** |
| | |
| | | const cachedTripStatusStorage = Taro.getStorageSync('tripStatus'); |
| | | const cachedTripStatus = toTripStatus(cachedTripStatusStorage, deps.getCurrentTripStatus()); |
| | | deps.setTripStatus(cachedTripStatus); |
| | | if ([TripStatus.VehicleApproaching, TripStatus.InService].includes(cachedTripStatus)) { |
| | | deps.setVehicleMoveAlongPoints(readPassedRouteCache(String(cachedOrderId || ''))); |
| | | const restoredPageState = await syncSitesFromActiveOrder( |
| | | cachedTripStatusStorage, |
| | | cachedOrderId, |
| | | cachedTripStatus, |
| | | s, |
| | | e |
| | | ); |
| | | const resolvedOrderId = restoredPageState.orderId; |
| | | const resolvedTripStatus = restoredPageState.tripStatus; |
| | | const resolvedStartSite = restoredPageState.startSite; |
| | | const resolvedEndSite = restoredPageState.endSite; |
| | | if ([TripStatus.VehicleApproaching, TripStatus.InService].includes(resolvedTripStatus)) { |
| | | deps.setVehicleMoveAlongPoints(readPassedRouteCache(String(resolvedOrderId || ''))); |
| | | } else { |
| | | deps.setVehicleMoveAlongPoints([]); |
| | | } |
| | | syncCachedOrderVehicle(cachedOrderId); |
| | | syncSitesFromActiveOrder(cachedTripStatusStorage, s, e); |
| | | const shouldRestorePreviewRoute = isPreviewRouteRestoreActive(cachedOrderId, cachedTripStatus); |
| | | if (!shouldRestorePreviewRoute && cachedOrderId && [TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup, TripStatus.InService, TripStatus.ArrivedDestination, TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(cachedTripStatus)) { |
| | | deps.routeRequestOrderRoute(cachedOrderId, cachedTripStatus); |
| | | syncCachedOrderVehicle(resolvedOrderId); |
| | | const shouldRestorePreviewRoute = isPreviewRouteRestoreActive(resolvedOrderId, resolvedTripStatus); |
| | | if (!shouldRestorePreviewRoute && resolvedOrderId && [TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup, TripStatus.InService, TripStatus.ArrivedDestination, TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(resolvedTripStatus)) { |
| | | deps.routeRequestOrderRoute(resolvedOrderId, resolvedTripStatus); |
| | | } |
| | | await syncLocationAndInitRoute(s, e, cachedOrderId, cachedTripStatus); |
| | | await syncLocationAndInitRoute(resolvedStartSite, resolvedEndSite, resolvedOrderId, resolvedTripStatus); |
| | | }; |
| | | |
| | | /** |
| | |
| | | handlePay: () => void; |
| | | /** 打开评价 */ |
| | | handleOpenEvaluatePopup: () => void; |
| | | /** 是否展示取消行程 */ |
| | | showCancelTrip: boolean; |
| | | /** 当前订单是否已开过门 */ |
| | | hasDoorOpenedInCurrentOrder: boolean; |
| | | /** 详情展开回调 */ |
| | | handleToggleDetailFade: (expanded: boolean, hasFare: boolean) => void; |
| | | /** 修改目的地 */ |
| | |
| | | vehicleInfo={deps.vehicleInfo} |
| | | handleOpenCancelPopup={(displayInfo) => deps.handleOpenCancelPopupByTripStatus(deps.tripStatus, displayInfo)} |
| | | handleCancelTrip={() => deps.handleOpenCancelPopupByTripStatus(deps.tripStatus, true)} |
| | | showCancelTrip={deps.showCancelTrip} |
| | | handleModifyDestination={deps.handleModifyDestination} |
| | | showModifyDestination={deps.showModifyDestination} |
| | | handleShareTrip={deps.handleShareTrip} |
| | |
| | | handlePay={deps.handlePay} |
| | | onOpenCouponSelect={deps.handleOpenCouponSelect} |
| | | handleOpenEvaluatePopup={deps.handleOpenEvaluatePopup} |
| | | hasDoorOpenedInCurrentOrder={deps.hasDoorOpenedInCurrentOrder} |
| | | estimatedWaitMinutes={deps.fareEstimate?.waitingTime} |
| | | routeDurationSeconds={deps.routeDuration} |
| | | onToggleDetailFade={deps.handleToggleDetailFade} |
| | |
| | | import { PENDING_CANCEL_CONTEXT_TTL_MS, type PendingCancelContext } from '../use-order-actions.shared'; |
| | | import type { UseWebSocketEventsDeps } from '../use-web-socket-events'; |
| | | import { isSameOrder, resolveCurrentOrderId, type WebSocketDepsRef } from './web-socket-order-guard'; |
| | | import { clearOrderDoorOpenedFlag } from '../order-door-open-storage'; |
| | | |
| | | /** |
| | | * 解析 payload 中用于同步订单的有效 orderId。 |
| | |
| | | deps.setCancelType(pendingCancelContext?.cancelType || 4); |
| | | deps.pendingCancelContextRef.current = null; |
| | | } else if (changeStatusOld === TripStatus.VehicleApproaching || changeStatusOld === TripStatus.VehicleArrivedPickup) { |
| | | clearOrderDoorOpenedFlag(deps.orderId); |
| | | deps.setOrderId(''); |
| | | deps.setVehicleInfo({}); |
| | | clearOrderTripStorage(); |
| | |
| | | import useBluetoothAccess from './hooks/use-bluetooth-access'; |
| | | // 蓝牙车控连接与鉴权编排 |
| | | import useBluetoothCarControl from './hooks/use-bluetooth-car-control'; |
| | | import useOrderDoorOpenState from './hooks/use-order-door-open-state'; |
| | | import { toTripStatus, TripStatus } from '../../constants/trip-status'; |
| | | import { MODIFY_DESTINATION_SOCKET_STATUS_KEY } from '../../constants/modify-destination'; |
| | | import { getDistance, reportEvent } from '../../../utils/Robotaxi-public-method'; |
| | |
| | | getCurrentLatitude: () => latitude, |
| | | getCurrentVehicleLocation: () => vehicleLocation |
| | | }); // 页面恢复与初始化编排 |
| | | const { |
| | | hasDoorOpenedInCurrentOrder, |
| | | markCurrentOrderDoorOpened, |
| | | clearCurrentOrderDoorOpened |
| | | } = useOrderDoorOpenState({ |
| | | orderId, |
| | | tripStatus, |
| | | pageDidShowVersion |
| | | }); // 当前订单开门状态编排 |
| | | const router = Taro.useRouter(); |
| | | const tripRuntime = useTripRuntime({ |
| | | waitingTimerStartKey: WAITING_TIMER_START_KEY, |
| | |
| | | * 支付成功后的统一收口。 |
| | | */ |
| | | const handlePaymentSuccess = useCallback(() => { |
| | | clearCurrentOrderDoorOpened(orderId); |
| | | setTripStatus(TripStatus.PaymentSuccess); |
| | | Taro.setStorageSync('tripStatus', TripStatus.PaymentSuccess); |
| | | }, []); |
| | | }, [orderId, clearCurrentOrderDoorOpened]); |
| | | |
| | | /** |
| | | * 蓝牙真实解锁成功后,写入当前订单开门标记。 |
| | | */ |
| | | const handleBluetoothDoorUnlockSuccess = useCallback(() => { |
| | | markCurrentOrderDoorOpened(); |
| | | }, [markCurrentOrderDoorOpened]); |
| | | |
| | | const { queryPaymentResult: queryOrderPaymentResult } = useOrderPaymentResultQuery({ |
| | | orderId, |
| | |
| | | markManualDoorUnlock |
| | | } = useBluetoothCarControl({ |
| | | orderId, |
| | | tripStatus |
| | | tripStatus, |
| | | onBluetoothDoorUnlockSuccess: handleBluetoothDoorUnlockSuccess |
| | | }); |
| | | /** |
| | | * 手动点击开门成功后,既要记录本单已开门,也要停止后续蓝牙自动解锁流程。 |
| | | */ |
| | | const handleManualDoorUnlockSuccess = useCallback(() => { |
| | | markCurrentOrderDoorOpened(); |
| | | markManualDoorUnlock(); |
| | | }, [markCurrentOrderDoorOpened, markManualDoorUnlock]); |
| | | const { |
| | | bluetoothStatusReady, |
| | | bluetoothCardDescription, |
| | |
| | | title: `我正在使用小鹏Robotaxi,车牌号[${vehicleInfo?.plateNumber || ''}]`, |
| | | path: `/robotaxi/pages/share-trip/index?orderId=${shareOrderId}` |
| | | }); |
| | | |
| | | /** 接驾阶段取消行程按钮展示态。 */ |
| | | const showCancelTrip = useMemo(() => { |
| | | return [TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup].includes(tripStatus) && !hasDoorOpenedInCurrentOrder; |
| | | }, [tripStatus, hasDoorOpenedInCurrentOrder]); |
| | | |
| | | useShareAppMessage(onShareAppMessage); |
| | | |
| | |
| | | handleOpenOverRoutePopup, |
| | | handlePay, |
| | | handleOpenEvaluatePopup, |
| | | showCancelTrip, |
| | | hasDoorOpenedInCurrentOrder, |
| | | handleToggleDetailFade, |
| | | handleModifyDestination, |
| | | showModifyDestination, |
| | |
| | | handleCloseSuccessAuthPopup, |
| | | handleAuthorizeBluetooth, |
| | | bluetoothAuthSuccess, |
| | | onDoorUnlockSuccess: markManualDoorUnlock, |
| | | onDoorUnlockSuccess: handleManualDoorUnlockSuccess, |
| | | pageDidShowVersion, |
| | | queuePollingTimeSeconds |
| | | }); // 视图渲染委托 |