| | |
| | | import { View, Image } from "@tarojs/components"; |
| | | import { useEffect, useState } from "react"; |
| | | import { formatCountdown } from '../../../../../utils/Robotaxi-public-method'; |
| | | import Taro from '@tarojs/taro'; |
| | | import { View, Image, Text } from '@tarojs/components'; |
| | | import { useEffect, useState } from 'react'; |
| | | import { queryOrderQueueStatus } from '../../../../api/begin-route'; |
| | | import { TripStatus } from '../../../../constants/trip-status'; |
| | | |
| | | type QueueStatus = { |
| | | currentPosition: number; |
| | | estimatedWaitTime: number; |
| | | status: number; |
| | | totalCount: number; |
| | | currentPosition: number; |
| | | estimatedWaitTime: number | null; |
| | | status: number; |
| | | totalCount: number; |
| | | } | null; |
| | | |
| | | type Props = { |
| | | orderId?: string; |
| | | tripStatus: TripStatus; |
| | | waitMinutes: number | null; |
| | | durationDisplay: { value: string | number; unit: string }; |
| | | isArriveSoon: boolean; |
| | | isPickupCountdownSoon: boolean; |
| | | pickupCountdownSeconds: number | null; |
| | | doorUnlockedInCurrentPage: boolean; |
| | | finishCountdownEnabled: boolean; |
| | | finishCountdownSeconds: number | null; |
| | | limitMinutes: number | null; |
| | | handleOpenCancelPopup?: (displayInfo: { durationDisplay: { value: string | number; unit: string }; waitMinutes: number | null }) => void; |
| | | queuePollingTimeSeconds: number; |
| | | orderId?: string; |
| | | tripStatus: TripStatus; |
| | | waitMinutes: number | null; |
| | | durationDisplay: { value: string | number; unit: string }; |
| | | isArriveSoon: boolean; |
| | | isPickupCountdownSoon: boolean; |
| | | pickupCountdownSeconds: number | null; |
| | | doorUnlockedInCurrentPage: boolean; |
| | | finishCountdownEnabled: boolean; |
| | | finishCountdownSeconds: number | null; |
| | | limitMinutes: number | null; |
| | | routeDistance?: { value: number; unit: 'km' | 'm' } | null; |
| | | handleOpenCancelPopup?: (displayInfo: { durationDisplay: { value: string | number; unit: string }; waitMinutes: number | null }) => void; |
| | | queuePollingTimeSeconds: number; |
| | | }; |
| | | |
| | | export default function TripStatusHeader({ |
| | | orderId, |
| | | tripStatus, |
| | | waitMinutes, |
| | | durationDisplay, |
| | | isArriveSoon, |
| | | isPickupCountdownSoon, |
| | | pickupCountdownSeconds, |
| | | doorUnlockedInCurrentPage, |
| | | finishCountdownEnabled, |
| | | finishCountdownSeconds, |
| | | limitMinutes, |
| | | handleOpenCancelPopup, |
| | | queuePollingTimeSeconds |
| | | orderId, |
| | | tripStatus, |
| | | waitMinutes, |
| | | durationDisplay, |
| | | isArriveSoon, |
| | | isPickupCountdownSoon, |
| | | pickupCountdownSeconds, |
| | | doorUnlockedInCurrentPage, |
| | | finishCountdownEnabled, |
| | | finishCountdownSeconds, |
| | | limitMinutes, |
| | | routeDistance = null, |
| | | handleOpenCancelPopup, |
| | | queuePollingTimeSeconds |
| | | }: Props) { |
| | | const [queueStatus, setQueueStatus] = useState<QueueStatus>(null); |
| | | const pollingIntervalMs = Math.max(1000, Number(queuePollingTimeSeconds || 10) * 1000); // 排队位次轮询间隔 |
| | | const inServiceDurationDisplay = durationDisplay.unit === '秒' |
| | | ? { value: 1, unit: '分钟' as const } |
| | | : durationDisplay; // 行程中剩余时间小于 1 分钟时按 1 分钟展示 |
| | | const [queueStatus, setQueueStatus] = useState<QueueStatus>(null); |
| | | const pollingIntervalMs = Math.max(1000, Number(queuePollingTimeSeconds || 10) * 1000); // 排队位次轮询间隔 |
| | | const normalizedDurationDisplay = durationDisplay.unit === '秒' |
| | | ? { value: 1, unit: '分钟' as const } |
| | | : durationDisplay; // 剩余时间小于 1 分钟时按 1 分钟展示,不展示秒 |
| | | const inServiceDurationDisplay = normalizedDurationDisplay; // 行程中沿用统一的归一化时长 |
| | | // 排队接口返回实时预计等待分钟数;无有效值时回退下单时的初始预估。 |
| | | const displayedWaitMinutes = queueStatus?.estimatedWaitTime ?? waitMinutes; |
| | | |
| | | useEffect(() => { |
| | | if (tripStatus !== TripStatus.CallingCar || !orderId) { |
| | | setQueueStatus(null); |
| | | return; |
| | | useEffect(() => { |
| | | if (tripStatus !== TripStatus.CallingCar || !orderId) { |
| | | setQueueStatus(null); |
| | | return; |
| | | } |
| | | |
| | | let timer: ReturnType<typeof setInterval> | null = null; |
| | | let unmounted = false; |
| | | |
| | | // 排队位次是动态变化的,状态为 3 时定时刷新 |
| | | const fetchQueueStatus = async () => { |
| | | try { |
| | | const res = await queryOrderQueueStatus({ orderId }); |
| | | if (unmounted) { |
| | | return; |
| | | } |
| | | const queueData = res?.data ?? res; |
| | | const rawEstimatedWaitTime = queueData?.estimatedWaitTime; |
| | | const estimatedWaitTime = rawEstimatedWaitTime === null || rawEstimatedWaitTime === undefined || rawEstimatedWaitTime === '' |
| | | ? null |
| | | : Number(rawEstimatedWaitTime); |
| | | if ( |
| | | Number(queueData?.currentPosition) > 0 && |
| | | Number(queueData?.totalCount) > 0 |
| | | ) { |
| | | setQueueStatus({ |
| | | currentPosition: Number(queueData.currentPosition), |
| | | estimatedWaitTime: typeof estimatedWaitTime === 'number' && Number.isFinite(estimatedWaitTime) && estimatedWaitTime >= 0 ? estimatedWaitTime : null, |
| | | status: Number(queueData.status) || 0, |
| | | totalCount: Number(queueData.totalCount) |
| | | }); |
| | | return; |
| | | } |
| | | setQueueStatus(null); |
| | | } catch (_) { |
| | | if (!unmounted) { |
| | | setQueueStatus(null); |
| | | } |
| | | } |
| | | }; |
| | | |
| | | let timer: ReturnType<typeof setInterval> | null = null; |
| | | let unmounted = false; |
| | | fetchQueueStatus(); |
| | | timer = setInterval(fetchQueueStatus, pollingIntervalMs); |
| | | |
| | | // 排队位次是动态变化的,状态为 3 时定时刷新 |
| | | const fetchQueueStatus = async () => { |
| | | try { |
| | | const res = await queryOrderQueueStatus({ orderId }); |
| | | if (unmounted) { |
| | | return; |
| | | } |
| | | const queueData = res?.data ?? res; |
| | | if ( |
| | | Number(queueData?.currentPosition) > 0 && |
| | | Number(queueData?.totalCount) > 0 |
| | | ) { |
| | | setQueueStatus({ |
| | | currentPosition: Number(queueData.currentPosition), |
| | | estimatedWaitTime: Number(queueData.estimatedWaitTime) || 0, |
| | | status: Number(queueData.status) || 0, |
| | | totalCount: Number(queueData.totalCount) |
| | | }); |
| | | return; |
| | | } |
| | | setQueueStatus(null); |
| | | } catch (_) { |
| | | if (!unmounted) { |
| | | setQueueStatus(null); |
| | | } |
| | | } |
| | | }; |
| | | return () => { |
| | | unmounted = true; |
| | | if (timer) { |
| | | clearInterval(timer); |
| | | } |
| | | }; |
| | | }, [tripStatus, orderId, pollingIntervalMs]); |
| | | |
| | | fetchQueueStatus(); |
| | | timer = setInterval(fetchQueueStatus, pollingIntervalMs); |
| | | // 标题行文案映射(已改为按状态直接渲染 DOM,保留注释供参考) |
| | | // const titleMap = { |
| | | // [TripStatus.CallingCar]: <>预计<View className='time'>{waitMinutes}</View>分钟内有车应答</>, |
| | | // [TripStatus.VehicleApproaching]: isArriveSoon |
| | | // ? <>车辆即将到达,请准备上车</> |
| | | // : <>车辆还有<View className='time'>{durationDisplay.value}</View>{durationDisplay.unit}到达</>, |
| | | // [TripStatus.VehicleArrivedPickup]: doorUnlockedInCurrentPage |
| | | // ? <>车辆已到达,请尽快上车</> |
| | | // : <>{isPickupCountdownSoon ? '车辆已到达,请尽快上车' : '车辆已到达上车点'}<View className='time'>{pickupCountdownSeconds}</View></>, |
| | | // [TripStatus.InService]: <>预计<View className='time'>{inServiceDurationDisplay.value}</View>{inServiceDurationDisplay.unit}抵达目的地</>, |
| | | // [TripStatus.ArrivedDestination]: (finishCountdownEnabled || finishCountdownSeconds !== null) |
| | | // ? <>已到达目的地<View className='time'>{finishCountdownSeconds}</View></> |
| | | // : <>已到达目的地</>, |
| | | // [TripStatus.TripFinished]: '行程已结束', |
| | | // [TripStatus.PaymentSuccess]: '支付成功' |
| | | // } |
| | | |
| | | return () => { |
| | | unmounted = true; |
| | | if (timer) { |
| | | clearInterval(timer); |
| | | } |
| | | }; |
| | | }, [tripStatus, orderId, pollingIntervalMs]); |
| | | // 说明文案映射(已注释) |
| | | // const descMap = { |
| | | // [TripStatus.CallingCar]: { text: '正全力为您叫车中,请耐心等待...', className: 'calling-car-content-desc' }, |
| | | // [TripStatus.VehicleApproaching]: limitMinutes ? { text: `此处限停${limitMinutes}分钟,请尽快前往上车点`, className: 'calling-car-content-warning' } : null, |
| | | // [TripStatus.VehicleArrivedPickup]: doorUnlockedInCurrentPage |
| | | // ? { 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' } |
| | | // } |
| | | |
| | | // 标题行文案映射 |
| | | const titleMap = { |
| | | [TripStatus.CallingCar]: <>预计<View className="time">{waitMinutes}</View>分钟内有车应答</>, |
| | | [TripStatus.VehicleApproaching]: isArriveSoon |
| | | ? <>车辆即将到达,请准备上车</> |
| | | : <>车辆还有<View className="time">{durationDisplay.value}</View>{durationDisplay.unit}到达</>, |
| | | [TripStatus.VehicleArrivedPickup]: doorUnlockedInCurrentPage |
| | | ? <>车辆已到达,请尽快上车</> |
| | | : <>{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></> |
| | | : <>已到达目的地</>, |
| | | [TripStatus.TripFinished]: '行程已结束', |
| | | [TripStatus.PaymentSuccess]: '支付成功' |
| | | } |
| | | // 站点信息(图二):与 cancel-popup 一致,优先取缓存的起终点 |
| | | const cachedStartSite = Taro.getStorageSync('startSite') || {}; |
| | | const cachedEndSite = Taro.getStorageSync('endSite') || {}; |
| | | const startSiteName = cachedStartSite?.name || cachedStartSite?.address || '-'; |
| | | const endSiteName = cachedEndSite?.name || cachedEndSite?.address || '-'; |
| | | |
| | | // 说明文案映射 |
| | | const descMap = { |
| | | [TripStatus.CallingCar]: { text: '正全力为您叫车中,请耐心等待...', className: 'calling-car-content-desc' }, |
| | | [TripStatus.VehicleApproaching]: limitMinutes ? { text: `此处限停${limitMinutes}分钟,请尽快前往上车点`, className: 'calling-car-content-warning' } : null, |
| | | [TripStatus.VehicleArrivedPickup]: doorUnlockedInCurrentPage |
| | | ? { 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' } |
| | | } |
| | | // 状态栏右侧的距离/时间文案 |
| | | const distanceText = routeDistance ? `${routeDistance.value}${routeDistance.unit === 'm' ? 'M' : 'KM'}` : ''; |
| | | const approachingTimeText = `${normalizedDurationDisplay.value}MIN`; |
| | | const inServiceTimeText = `${normalizedDurationDisplay.value}MIN`; |
| | | |
| | | return ( |
| | | <View className='calling-car-content'> |
| | | <View> |
| | | {titleMap[tripStatus] && ( |
| | | <View className='calling-car-content-title'>{titleMap[tripStatus]}</View> |
| | | return ( |
| | | <> |
| | | {/* 呼叫车中:DOM 结构与样式保持不变(图一 + 站点卡片) */} |
| | | {tripStatus === TripStatus.CallingCar && ( |
| | | <> |
| | | <View className='calling-car-content'> |
| | | <View className='calling-car-content-main'> |
| | | <View className='calling-car-content-title calling-car-content-title--strong'> |
| | | 预计{displayedWaitMinutes ?? '-'}分钟内有车应答 |
| | | </View> |
| | | <View className='calling-car-content-desc'> |
| | | 正全力为您叫车中... |
| | | {queueStatus && ( |
| | | <> |
| | | <Text className='calling-car-content-desc-divider'>|</Text> |
| | | 第{queueStatus.currentPosition}位/共{queueStatus.totalCount}位 |
| | | </> |
| | | )} |
| | | {descMap[tripStatus] && ( |
| | | <View style={{ paddingBottom: tripStatus === TripStatus.CallingCar && !queueStatus ? '20px' : '0px' }} className={descMap[tripStatus]?.className}>{descMap[tripStatus]?.text}</View> |
| | | )} |
| | | {tripStatus === TripStatus.CallingCar && queueStatus && ( |
| | | <View className='calling-car-content-desc'> |
| | | 第{queueStatus.currentPosition}位/共{queueStatus.totalCount}位 |
| | | </View> |
| | | )} |
| | | </View> |
| | | </View> |
| | | { |
| | | [TripStatus.CallingCar].includes(tripStatus) && |
| | | <View className="cancel-btn"> |
| | | <Image onClick={() => handleOpenCancelPopup?.({ durationDisplay, waitMinutes })} src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/cancel-btn.png' className="cancel" /> |
| | | </View> |
| | | } |
| | | <View |
| | | className='calling-car-cancel-btn' |
| | | onClick={() => handleOpenCancelPopup?.({ durationDisplay, waitMinutes: displayedWaitMinutes })} |
| | | > |
| | | 取消行程 |
| | | </View> |
| | | </View> |
| | | <View className='calling-car-site-box'> |
| | | <View className='calling-car-site-box-icon'> |
| | | <Image className='calling-car-site-check'src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/route-state-icon-golden-20260702.png' ></Image> |
| | | <Image className='dotted-line' src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/dotted-line.png' /> |
| | | <Image className='calling-car-site-check' src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/route-state-icon-black-20260702.png'></Image> |
| | | </View> |
| | | <View className='calling-car-site-box-content'> |
| | | <View>{startSiteName}</View> |
| | | <View className='calling-car-site-box-divider'></View> |
| | | <View>{endSiteName}</View> |
| | | </View> |
| | | </View> |
| | | </> |
| | | )} |
| | | |
| | | {/* 车辆即将到达:左右布局状态栏(图一) */} |
| | | {tripStatus === TripStatus.VehicleApproaching && ( |
| | | <View className='calling-car-status-box'> |
| | | <Image className='calling-car-status-box-come-icon' mode='widthFix' src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/come-icon-20260728.webp'></Image> |
| | | <View className='calling-car-status-box-left'>{isArriveSoon ? '即将抵达' : '正在赶来'}</View> |
| | | <View className='calling-car-status-box-right'> |
| | | {distanceText && <View className='distance'>{distanceText}</View>} |
| | | <View className='time'>{approachingTimeText}</View> |
| | | </View> |
| | | </View> |
| | | ); |
| | | )} |
| | | |
| | | {/* 车辆到达上车点:直接展示图片 */} |
| | | {tripStatus === TripStatus.VehicleArrivedPickup && ( |
| | | <View className='calling-car-status-box calling-car-arrived-img'> |
| | | <View className='calling-car-status-box-left'>车辆已到达上车点,倒计时结束将自动取消订单</View> |
| | | </View> |
| | | )} |
| | | |
| | | {/* 行程中:左右布局状态栏 */} |
| | | {tripStatus === TripStatus.InService && ( |
| | | <View className='calling-car-status-box'> |
| | | <Image className='calling-car-status-box-come-icon' mode='widthFix' src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/come-icon-20260728.webp'></Image> |
| | | <View className='calling-car-status-box-left'>正在前往目的地</View> |
| | | <View className='calling-car-status-box-right'> |
| | | {distanceText && <View className='distance'>{distanceText}</View>} |
| | | <View className='time'>{inServiceTimeText}</View> |
| | | </View> |
| | | </View> |
| | | )} |
| | | </> |
| | | ); |
| | | } |