feat: 引入行程状态枚举以替代魔术数字
- 新增 TripStatus 和 SocketOrderStatus 枚举,统一管理行程状态
- 替换多处硬编码的状态数字为枚举引用,提升代码可读性和维护性
- 提供类型安全的转换函数 toTripStatus 和 toSocketOrderStatus
- 更新相关组件、钩子和工具函数以使用新枚举类型
| | |
| | | import { View, Image } from '@tarojs/components'; |
| | | import { safeParse } from '../../../../../utils/Robotaxi-public-method'; |
| | | import { TripStatus } from '../../../../../robotaxi/pages/begin-route/constants/trip-status'; |
| | | |
| | | type HomeTripStatus = TripStatus | 0; |
| | | |
| | | type Props = { |
| | | /** 行程状态 */ |
| | | tripStatus: number; |
| | | tripStatus: HomeTripStatus; |
| | | /** 订单信息 */ |
| | | orderData: any; |
| | | /** 查看详情回调 */ |
| | | onViewDetail: () => void; |
| | | /** 状态文案映射 */ |
| | | orderStatusMap: Record<number, string>; |
| | | orderStatusMap: Partial<Record<TripStatus, string>>; |
| | | }; |
| | | |
| | | /** |
| | | * 订单状态卡片。 |
| | | */ |
| | | export default function OrderStatusCard({ tripStatus, orderData, onViewDetail, orderStatusMap }: Props) { |
| | | if (![3, 4, 5, 6, 7, 8].includes(tripStatus)) { |
| | | if (![TripStatus.CallingCar, TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup, TripStatus.InService, TripStatus.ArrivedDestination, TripStatus.TripFinished].includes(tripStatus as TripStatus)) { |
| | | return null; |
| | | } |
| | | |
| | | return ( |
| | | <View className="order-status"> |
| | | { |
| | | [3].includes(tripStatus) && |
| | | [TripStatus.CallingCar].includes(tripStatus as TripStatus) && |
| | | <View className="order-status-left"> |
| | | <View className="order-status-left-item"> |
| | | <Image className="order-status-left-item-icon" src={require('../../../../../robotaxi/assets/pointer.png')} /> |
| | |
| | | </View> |
| | | } |
| | | { |
| | | [4, 5, 6, 7, 8].includes(tripStatus) && |
| | | [TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup, TripStatus.InService, TripStatus.ArrivedDestination, TripStatus.TripFinished].includes(tripStatus as TripStatus) && |
| | | <View className="order-status-left"> |
| | | { |
| | | orderData.vehiclePictures |
| | |
| | | } |
| | | <View className="order-status-left-detail"> |
| | | <View>{orderData.plateNumber}</View> |
| | | <View>{orderStatusMap[tripStatus]}</View> |
| | | <View>{orderStatusMap[tripStatus as TripStatus]}</View> |
| | | </View> |
| | | </View> |
| | | } |
| | |
| | | import Taro from '@tarojs/taro'; |
| | | import { useEffect, useRef, useState } from 'react'; |
| | | import { TripStatus } from '../../../../robotaxi/pages/begin-route/constants/trip-status'; |
| | | |
| | | type HomeTripStatus = TripStatus | 0; |
| | | |
| | | type Deps = { |
| | | /** 轮播列表 */ |
| | | bannerList: any[]; |
| | | /** 行程状态 */ |
| | | tripStatus: number; |
| | | tripStatus: HomeTripStatus; |
| | | }; |
| | | |
| | | /** |
| | |
| | | import Taro from '@tarojs/taro'; |
| | | import wsManager from '../../../../utils/websocket'; |
| | | import { mapOrderStatusToTripStatus } from '../../../../utils/Robotaxi-public-method'; |
| | | import { TripStatus, toTripStatus } from '../../../../robotaxi/pages/begin-route/constants/trip-status'; |
| | | |
| | | type HomeTripStatus = TripStatus | 0; |
| | | |
| | | type Deps = { |
| | | /** ws 服务地址 */ |
| | |
| | | /** 设置吸附状态 */ |
| | | setIsAdsorbed: (value: boolean) => void; |
| | | /** 设置行程状态 */ |
| | | setTripStatus: (value: number) => void; |
| | | setTripStatus: (value: HomeTripStatus) => void; |
| | | /** 认证校验 */ |
| | | checkOtp: (silent?: boolean) => Promise<any>; |
| | | /** 设置登录态 */ |
| | |
| | | * 首页启动流程编排。 |
| | | */ |
| | | export default function useHomeBootstrap(deps: Deps) { |
| | | const toHomeTripStatus = (value: unknown): HomeTripStatus => { |
| | | const numericValue = Number(value); // 统一转数值后做状态归一 |
| | | return numericValue === 0 ? 0 : toTripStatus(numericValue); |
| | | }; |
| | | |
| | | /** |
| | | * 同步登录、协议与活跃订单。 |
| | | */ |
| | |
| | | return; |
| | | } |
| | | deps.setOrderData(activeOrder || {}); |
| | | const mappedStatus = mapOrderStatusToTripStatus(activeOrder.status || 0); |
| | | const mappedStatus = toHomeTripStatus(mapOrderStatusToTripStatus(activeOrder.status || 0)); |
| | | deps.setTripStatus(mappedStatus); |
| | | Taro.setStorageSync('tripStatus', mappedStatus); |
| | | Taro.setStorageSync('orderId', activeOrder.id || ''); |
| | |
| | | setCurrentAddress: deps.setCurrentAddress, |
| | | setIsAdsorbed: deps.setIsAdsorbed |
| | | }); |
| | | deps.setTripStatus(Number(Taro.getStorageSync('tripStatus')) || 0); |
| | | deps.setTripStatus(toHomeTripStatus(Taro.getStorageSync('tripStatus'))); |
| | | syncAuthAndOrder(); |
| | | Taro.getSetting({ |
| | | success: (res) => { |
| | |
| | | // 首页启动流程编排 |
| | | import useHomeBootstrap from './hooks/use-home-bootstrap'; |
| | | import { DEFAULT_LATITUDE, DEFAULT_LONGITUDE, FIRST_ENTER_KEY } from '../../../robotaxi/constants/map'; |
| | | import { TripStatus, toTripStatus } from '../../../robotaxi/pages/begin-route/constants/trip-status'; |
| | | import { |
| | | getGeofence, |
| | | getBannerList, |
| | |
| | | import { getServicePhone } from '../../../robotaxi/api/customer-service'; |
| | | import { getOrderDistance } from '../../../robotaxi/api/select-map-site'; |
| | | import { checkOtp } from '../../../utils/tools'; |
| | | |
| | | type HomeTripStatus = TripStatus | 0; |
| | | |
| | | export default function Home() { |
| | | const renderTailPreserveText = (text: string, tailCount = 3) => { |
| | |
| | | const [isMaintain, setIsMaintain] = useState(false); // 是否维护中 |
| | | const [maintainBounce, setMaintainBounce] = useState(false); |
| | | const [hotfixEndText, setHotfixEndText] = useState(''); |
| | | const [tripStatus, setTripStatus] = useState<number>(0); // 行程状态 |
| | | const [tripStatus, setTripStatus] = useState<HomeTripStatus>(0); // 行程状态(0 表示无进行中行程) |
| | | const [bannerList, setBannerList] = useState<any[]>([]); // 轮播图列表 |
| | | const [weatherData, setWeatherData] = useState<any>(null); // 天气数据 |
| | | const [orderData, setOrderData] = useState<any>({}); // 订单数据 |
| | |
| | | } |
| | | }, []); |
| | | |
| | | const ORDER_STATUS = { |
| | | 4: '车辆正在赶来', |
| | | 5: '车辆已到达上车点,请尽快前往', |
| | | 6: '正在前往目的地', |
| | | 7: '车辆已到达目的地,请尽快结束行程', |
| | | 8: '行程已结束,请尽快支付' |
| | | } |
| | | const ORDER_STATUS: Partial<Record<TripStatus, string>> = { |
| | | [TripStatus.VehicleApproaching]: '车辆正在赶来', |
| | | [TripStatus.VehicleArrivedPickup]: '车辆已到达上车点,请尽快前往', |
| | | [TripStatus.InService]: '正在前往目的地', |
| | | [TripStatus.ArrivedDestination]: '车辆已到达目的地,请尽快结束行程', |
| | | [TripStatus.TripFinished]: '行程已结束,请尽快支付' |
| | | }; // 订单状态文案映射 |
| | | |
| | | useEffect(() => { |
| | | let timer: any = null; |
| | |
| | | // 监听订单状态变化 |
| | | useEffect(() => { |
| | | const orderStatusHandler = (payload) => { |
| | | const mappedTripStatus = payload?.tripStatus; |
| | | const mappedTripStatus = payload?.tripStatus; // ws 推送的行程状态 |
| | | if (typeof mappedTripStatus === 'number') { |
| | | setTripStatus(mappedTripStatus); |
| | | setTripStatus(mappedTripStatus === 0 ? 0 : toTripStatus(mappedTripStatus)); |
| | | } |
| | | }; |
| | | let openHandler: (() => void) | null = null; |
| | |
| | | import EvaluatePopup from './evaluate-popup'; |
| | | import TipPopup from '../../select-map-site/components/tip-popup'; |
| | | import AuthBluetoothPopup from './auth-bluetooth-popup'; |
| | | import { TripStatus } from '../constants/trip-status'; |
| | | |
| | | type Props = { |
| | | mapLongitude: number; |
| | |
| | | startSite: any; |
| | | endSite: any; |
| | | fareEstimate: any; |
| | | tripStatus: number; |
| | | tripStatus: TripStatus; |
| | | routeDuration: number | null; |
| | | handleCloseCancelPopup: () => void; |
| | | handleConfirmCancel: () => void; |
| | |
| | | <View className='container-inner'> |
| | | {shouldShowOrigin && ( |
| | | <View className='origin-box' style={{ opacity: originBoxHidden ? 0 : originOpacity, transition: 'opacity 0.2s ease-out', pointerEvents: originBoxHidden ? 'none' : 'auto' }}> |
| | | {[4, 5].includes(tripStatus) && ( |
| | | {[TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup].includes(tripStatus) && ( |
| | | <Image onClick={handleWalkNavigation} style={{ marginBottom: '16px' }} className='origin-icon' src={walkIcon} /> |
| | | )} |
| | | <Image onClick={toOrigin} className='origin-icon' src={originIcon} /> |
| | |
| | | import { View, Image } from "@tarojs/components"; |
| | | import { useState, useRef, useEffect } from "react"; |
| | | import { unlockVehicle, queryVehicleTrunkOpenStatus } from '../../../../api/begin-route'; |
| | | import { TripStatus } from '../../constants/trip-status'; |
| | | |
| | | type Props = { |
| | | tripStatus: number; |
| | | tripStatus: TripStatus; |
| | | orderId?: string; |
| | | orderTrunkStatus?: boolean; |
| | | handleOpenOverRoutePopup?: () => void; |
| | |
| | | }); |
| | | }; |
| | | |
| | | if (tripStatus === 5) { |
| | | if (tripStatus === TripStatus.VehicleArrivedPickup) { |
| | | return ( |
| | | <View className="car-card-arrive"> |
| | | <View className={`car-card-arrive-item ${trunkStatus && 'close-btn-item'}`} onClick={() => handleUnlock('trunk', () => setTrunkStatus(!trunkStatus))}> |
| | |
| | | ); |
| | | } |
| | | |
| | | if (tripStatus === 7) { |
| | | if (tripStatus === TripStatus.ArrivedDestination) { |
| | | return ( |
| | | <View className="car-card-reach"> |
| | | <View className="car-card-reach-text">点击按钮开车门</View> |
| | |
| | | import activeGood from '../../../../assets/active-good.png' |
| | | import activeBad from '../../../../assets/active-bad.png' |
| | | import activeNormal from '../../../../assets/active-normal.png' |
| | | import { TripStatus } from '../../constants/trip-status'; |
| | | |
| | | type Props = { |
| | | tripStatus: number; |
| | | tripStatus: TripStatus; |
| | | orderId?: string; |
| | | handleOpenEvaluatePopup?: () => void; |
| | | evaluateSubmitVersion?: number; |
| | |
| | | }); |
| | | }; |
| | | |
| | | if (tripStatus !== 9) { |
| | | if (tripStatus !== TripStatus.PaymentSuccess) { |
| | | return null; |
| | | } |
| | | |
| | |
| | | import { View, Image, Text } from "@tarojs/components"; |
| | | import { useState, useEffect } from "react"; |
| | | import { formatMoney, getTypeLabel, formatTripDistance, formatNumber, formatTripDurationFromSeconds } from '../../../../../utils/Robotaxi-public-method'; |
| | | import { TripStatus } from '../../constants/trip-status'; |
| | | |
| | | type Props = { |
| | | tripStatus: number; |
| | | tripStatus: TripStatus; |
| | | fareBreakdown: any; |
| | | orderId?: string; |
| | | resetDetailVersion?: number; |
| | |
| | | } |
| | | }; |
| | | |
| | | if (![8, 9].includes(tripStatus)) { |
| | | if (![TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(tripStatus)) { |
| | | return null; |
| | | } |
| | | |
| | | return ( |
| | | <View> |
| | | <View className="car-card-end" style={{ paddingBottom: tripStatus === 8 ? '145px' : '44px' }}> |
| | | <View className="car-card-end" style={{ paddingBottom: tripStatus === TripStatus.TripFinished ? '145px' : '44px' }}> |
| | | <View className="car-card-line" style="margin:24px 0;"></View> |
| | | <View className="car-card-end-particulars"> |
| | | <View className="car-card-end-particulars-label"> |
| | |
| | | } |
| | | </View> |
| | | { |
| | | tripStatus === 8 && |
| | | tripStatus === TripStatus.TripFinished && |
| | | <View className="pay-box"> |
| | | { |
| | | fareBreakdown.operateFree != 1 ? |
| | |
| | | import { View, Image } from "@tarojs/components"; |
| | | import { useState, useEffect, useRef } from "react"; |
| | | import { queryTemperatureByOrderId, setTemperature } from '../../../../api/begin-route'; |
| | | import { TripStatus } from '../../constants/trip-status'; |
| | | |
| | | type Props = { |
| | | tripStatus: number; |
| | | tripStatus: TripStatus; |
| | | orderId?: string; |
| | | }; |
| | | |
| | |
| | | const TEMP_MIN = 22; |
| | | const TEMP_MAX = 28; |
| | | |
| | | // 状态 4 时拉取车辆当前温度 |
| | | // 车辆即将到达状态时拉取车辆当前温度 |
| | | useEffect(() => { |
| | | if (tripStatus === 4) { |
| | | if (tripStatus === TripStatus.VehicleApproaching) { |
| | | const targetOrderId = orderId || Taro.getStorageSync('orderId'); |
| | | if (targetOrderId) { |
| | | queryTemperatureByOrderId({ orderId: targetOrderId }).then((res: any) => { |
| | |
| | | }); |
| | | }; |
| | | |
| | | // if (tripStatus !== 4 || typeof tempStatus !== 'number') { |
| | | // if (tripStatus !== TripStatus.VehicleApproaching || typeof tempStatus !== 'number') { |
| | | // return null; |
| | | // } |
| | | |
| | |
| | | import changeDestinationIcon from "../../../../assets/change-destination.png"; |
| | | import shareIcon from "../../../../assets/share-trip.png"; |
| | | import cancelIcon from "../../../../assets/cancel-trip.png"; |
| | | import { TripStatus } from '../../constants/trip-status'; |
| | | |
| | | type Props = { |
| | | tripStatus: number; |
| | | tripStatus: TripStatus; |
| | | handleCancelTrip?: () => void; |
| | | handleModifyDestination?: () => void; |
| | | handleShareTrip?: () => void; |
| | |
| | | key: "cancel", |
| | | label: "取消行程", |
| | | icon: cancelIcon, |
| | | visible: tripStatus === 4, |
| | | visible: tripStatus === TripStatus.VehicleApproaching, |
| | | onClick: handleCancelTrip |
| | | }, |
| | | { |
| | | key: "destination", |
| | | label: "修改目的地", |
| | | icon: changeDestinationIcon, |
| | | visible: [4, 5, 6].includes(tripStatus), |
| | | visible: [TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup, TripStatus.InService].includes(tripStatus), |
| | | onClick: handleModifyDestination |
| | | }, |
| | | { |
| | | key: "share", |
| | | label: "分享行程", |
| | | icon: shareIcon, |
| | | visible: [4, 5, 6].includes(tripStatus), |
| | | visible: [TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup, TripStatus.InService].includes(tripStatus), |
| | | onClick: handleShareTrip |
| | | } |
| | | ].filter(item => item.visible); |
| | |
| | | import { useEffect, useState } from "react"; |
| | | import { formatCountdown } from '../../../../../utils/Robotaxi-public-method'; |
| | | import { queryOrderQueueStatus } from '../../../../api/begin-route'; |
| | | import { TripStatus } from '../../constants/trip-status'; |
| | | |
| | | type QueueStatus = { |
| | | currentPosition: number; |
| | |
| | | |
| | | type Props = { |
| | | orderId?: string; |
| | | tripStatus: number; |
| | | tripStatus: TripStatus; |
| | | waitMinutes: number | null; |
| | | durationDisplay: { value: string | number; unit: string }; |
| | | isArriveSoon: boolean; |
| | |
| | | const [queueStatus, setQueueStatus] = useState<QueueStatus>(null); |
| | | |
| | | useEffect(() => { |
| | | if (tripStatus !== 3 || !orderId) { |
| | | if (tripStatus !== TripStatus.CallingCar || !orderId) { |
| | | setQueueStatus(null); |
| | | return; |
| | | } |
| | |
| | | |
| | | // 标题行文案映射 |
| | | const titleMap = { |
| | | 3: <>预计<View className="time">{waitMinutes}</View>分钟内有车应答</>, |
| | | 4: <>车辆还有<View className="time">{durationDisplay.value}</View>{durationDisplay.unit}到达</>, |
| | | 5: <>车辆已到达上车点<View className="time" style={isPickupCountdownSoon ? { color: '#E03434' } : undefined}>{pickupCountdownSeconds !== null ? formatCountdown(pickupCountdownSeconds) : '--:--'}</View></>, |
| | | 6: <>预计<View className="time">{durationDisplay.value}</View>{durationDisplay.unit}抵达目的地</>, |
| | | 7: (finishCountdownEnabled || finishCountdownSeconds !== null) |
| | | [TripStatus.CallingCar]: <>预计<View className="time">{waitMinutes}</View>分钟内有车应答</>, |
| | | [TripStatus.VehicleApproaching]: <>车辆还有<View className="time">{durationDisplay.value}</View>{durationDisplay.unit}到达</>, |
| | | [TripStatus.VehicleArrivedPickup]: <>车辆已到达上车点<View className="time" style={isPickupCountdownSoon ? { color: '#E03434' } : undefined}>{pickupCountdownSeconds !== null ? formatCountdown(pickupCountdownSeconds) : '--:--'}</View></>, |
| | | [TripStatus.InService]: <>预计<View className="time">{durationDisplay.value}</View>{durationDisplay.unit}抵达目的地</>, |
| | | [TripStatus.ArrivedDestination]: (finishCountdownEnabled || finishCountdownSeconds !== null) |
| | | ? <>已到达目的地<View className="time">{finishCountdownSeconds !== null ? formatCountdown(finishCountdownSeconds) : '--:--'}</View></> |
| | | : <>已到达目的地</>, |
| | | 8: '行程已结束', |
| | | 9: '支付成功' |
| | | [TripStatus.TripFinished]: '行程已结束', |
| | | [TripStatus.PaymentSuccess]: '支付成功' |
| | | } |
| | | |
| | | // 说明文案映射 |
| | | const descMap = { |
| | | 3: { text: '正全力为您叫车中,请耐心等待...', className: 'calling-car-content-desc' }, |
| | | 4: limitMinutes ? { text: `此处限停${limitMinutes}分钟,请尽快前往上车点`, className: isArriveSoon ? 'calling-car-content-desc-red' : 'calling-car-content-warning' } : null, |
| | | 5: { text: '此处限停,请在倒计时结束前到达上车点', className: 'calling-car-content-desc' }, |
| | | 6: { text: '行程已开始,请全程系好安全带', className: 'calling-car-content-desc' }, |
| | | 7: { text: '倒计时结束时车辆将自动驶离,请带好物品安全下车', className: 'calling-car-content-desc' }, |
| | | 8: { text: '订单待支付,请核对后完成支付', className: 'calling-car-content-desc' } |
| | | [TripStatus.CallingCar]: { text: '正全力为您叫车中,请耐心等待...', className: 'calling-car-content-desc' }, |
| | | [TripStatus.VehicleApproaching]: limitMinutes ? { text: `此处限停${limitMinutes}分钟,请尽快前往上车点`, className: isArriveSoon ? 'calling-car-content-desc-red' : 'calling-car-content-warning' } : null, |
| | | [TripStatus.VehicleArrivedPickup]: { text: '此处限停,请在倒计时结束前到达上车点', 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' } |
| | | } |
| | | |
| | | return ( |
| | |
| | | <View className='calling-car-content-title'>{titleMap[tripStatus]}</View> |
| | | )} |
| | | {descMap[tripStatus] && ( |
| | | // <View style={{ paddingBottom: tripStatus === 3 ? '20px' : '0px' }} className={descMap[tripStatus]?.className}>{descMap[tripStatus]?.text}</View> |
| | | // <View style={{ paddingBottom: tripStatus === TripStatus.CallingCar ? '20px' : '0px' }} className={descMap[tripStatus]?.className}>{descMap[tripStatus]?.text}</View> |
| | | <View className={descMap[tripStatus]?.className}>{descMap[tripStatus]?.text}</View> |
| | | )} |
| | | {tripStatus === 3 && queueStatus && ( |
| | | {tripStatus === TripStatus.CallingCar && queueStatus && ( |
| | | <View className='calling-car-content-desc'> |
| | | 第{queueStatus.currentPosition}位/共{queueStatus.totalCount}位 |
| | | </View> |
| | | )} |
| | | </View> |
| | | { |
| | | [3].includes(tripStatus) && |
| | | [TripStatus.CallingCar].includes(tripStatus) && |
| | | <View className="cancel-btn"> |
| | | <Image onClick={() => handleOpenCancelPopup?.(durationDisplay)} src={require('../../../../assets/cancel-btn.png')} className="cancel" /> |
| | | </View> |
| | |
| | | import { useState, useEffect, useRef, useCallback } from 'react'; |
| | | import { getRemainingSeconds, parseFlexibleEndTimeMs } from '../../../../../utils/Robotaxi-public-method'; |
| | | import { getWaitTime, queryOrderFinishTime, cancelOrder, queryFareBreakdown } from '../../../../api/begin-route'; |
| | | import { TripStatus } from '../../constants/trip-status'; |
| | | |
| | | type Props = { |
| | | tripStatus: number; |
| | | tripStatus: TripStatus; |
| | | orderId?: string; |
| | | estimatedWaitMinutes?: number; |
| | | finishCountdownEnabled: boolean; |
| | |
| | | }; |
| | | |
| | | useEffect(() => { |
| | | if (typeof priceUpdateVersion === 'number' && priceUpdateVersion > 0 && [8, 9].includes(tripStatus)) { |
| | | if (typeof priceUpdateVersion === 'number' && priceUpdateVersion > 0 && [TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(tripStatus)) { |
| | | requestFareBreakdown(true); |
| | | } |
| | | }, [priceUpdateVersion, tripStatus, requestFareBreakdown]); |
| | | |
| | | useEffect(() => { |
| | | let active = true; |
| | | if (tripStatus !== 3) return () => { active = false; }; |
| | | if (tripStatus !== TripStatus.CallingCar) return () => { active = false; }; |
| | | if (typeof estimatedWaitMinutes === 'number' && !Number.isNaN(estimatedWaitMinutes)) { |
| | | setWaitMinutes(estimatedWaitMinutes); |
| | | return () => { active = false; }; |
| | |
| | | }, [tripStatus, estimatedWaitMinutes]); |
| | | |
| | | useEffect(() => { |
| | | if (tripStatus !== 4) return; |
| | | if (tripStatus !== TripStatus.VehicleApproaching) return; |
| | | const startSite = Taro.getStorageSync('startSite'); |
| | | const time = Number(startSite?.time); |
| | | if (!Number.isNaN(time)) { |
| | |
| | | }, [tripStatus]); |
| | | |
| | | useEffect(() => { |
| | | if (tripStatus !== 5) { |
| | | if (tripStatus !== TripStatus.VehicleArrivedPickup) { |
| | | clearPickupTimer(); |
| | | return; |
| | | } |
| | | |
| | | const runPickupTick = () => { |
| | | // 防止状态已切换后,旧定时回调仍触发取消逻辑 |
| | | if (pickupTripStatusRef.current !== 5) { |
| | | if (pickupTripStatusRef.current !== TripStatus.VehicleArrivedPickup) { |
| | | clearPickupTimer(); |
| | | return; |
| | | } |
| | |
| | | }, [tripStatus, orderId, handleCancelTimeout, clearPickupTimer]); |
| | | |
| | | useEffect(() => { |
| | | if (tripStatus !== 7) { |
| | | if (tripStatus !== TripStatus.ArrivedDestination) { |
| | | clearFinishTimer(); |
| | | return; |
| | | } |
| | |
| | | }, [tripStatus, handleFinishTimeout, finishCountdownEnabled, clearFinishTimer]); |
| | | |
| | | useEffect(() => { |
| | | if ([8, 9].includes(tripStatus)) { |
| | | if ([TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(tripStatus)) { |
| | | Taro.removeStorageSync(FINISH_END_TIME_KEY); |
| | | requestFareBreakdown(); |
| | | } |
| | |
| | | import { View, Image } from "@tarojs/components"; |
| | | import './css-folder/calling-car.less' |
| | | import { formatDurationDisplay } from '../../../../utils/Robotaxi-public-method'; |
| | | import { TripStatus } from '../constants/trip-status'; |
| | | |
| | | import useTripCountdown from './calling-car-modules/use-trip-countdown'; // 行程倒计时 |
| | | import TripStatusHeader from './calling-car-modules/trip-status-header'; // 行程状态头 |
| | |
| | | |
| | | type Props = { |
| | | /** 行程状态:3呼叫中、4即将到达、5已到达上车点、6行程中、7到达终点、8待支付、9已支付 */ |
| | | tripStatus?: number; |
| | | tripStatus?: TripStatus; |
| | | /** 打开取消弹窗,入参为预计到达展示文案 */ |
| | | handleOpenCancelPopup?: (durationDisplay: any) => void; |
| | | /** 上车点超时后的回调 */ |
| | |
| | | } |
| | | |
| | | export default function CallingCar({ |
| | | tripStatus = 3, |
| | | tripStatus = TripStatus.CallingCar, |
| | | handleOpenCancelPopup, |
| | | handleCancelTimeout, |
| | | handleFinishTimeout, |
| | |
| | | /** 预计到达时长展示(秒/分钟) */ |
| | | const durationDisplay = formatDurationDisplay(routeDurationSeconds); |
| | | /** 车辆即将到达上车点(<=60秒) */ |
| | | const isArriveSoon = tripStatus === 4 && (typeof routeDurationSeconds === 'number' && routeDurationSeconds <= 60); |
| | | const isArriveSoon = tripStatus === TripStatus.VehicleApproaching && (typeof routeDurationSeconds === 'number' && routeDurationSeconds <= 60); |
| | | /** 上车倒计时即将结束(<60秒) */ |
| | | const isPickupCountdownSoon = tripStatus === 5 && (typeof pickupCountdownSeconds === 'number' && pickupCountdownSeconds < 60); |
| | | const isPickupCountdownSoon = tripStatus === TripStatus.VehicleArrivedPickup && (typeof pickupCountdownSeconds === 'number' && pickupCountdownSeconds < 60); |
| | | |
| | | // 按状态组合渲染行程卡片模块 |
| | | return ( |
| | |
| | | /> |
| | | |
| | | { |
| | | [4, 5].includes(tripStatus) && bluetoothStatusReady && |
| | | [TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup].includes(tripStatus) && bluetoothStatusReady && |
| | | <BluetoothOperateCard |
| | | description={bluetoothCardDescription} |
| | | actionText={bluetoothCardActionText} |
| | |
| | | } |
| | | |
| | | { |
| | | [4, 5].includes(tripStatus) && |
| | | [TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup].includes(tripStatus) && |
| | | <AuthBluetoothPopup |
| | | show={showAuthBluetoothPopup} |
| | | onClose={handleCloseAuthBluetoothPopup} |
| | |
| | | } |
| | | |
| | | { |
| | | [4, 5].includes(tripStatus) && |
| | | [TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup].includes(tripStatus) && |
| | | <SuccessAuthPopup |
| | | show={showSuccessAuthPopup} |
| | | onClose={handleCloseSuccessAuthPopup || (() => { })} |
| | |
| | | } |
| | | |
| | | { |
| | | [4, 5, 6, 8, 9].includes(tripStatus) && |
| | | [TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup, TripStatus.InService, TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(tripStatus) && |
| | | <View className="car-card"> |
| | | <VehicleInfoCard vehicleInfo={vehicleInfo} /> |
| | | |
| | |
| | | } |
| | | |
| | | <DoorTrunkControl |
| | | tripStatus={tripStatus === 7 ? 7 : -1} |
| | | tripStatus={tripStatus === TripStatus.ArrivedDestination ? TripStatus.ArrivedDestination : -1} |
| | | orderId={orderId} |
| | | orderTrunkStatus={orderTrunkStatus} |
| | | handleOpenOverRoutePopup={handleOpenOverRoutePopup} |
| | |
| | | clearOrderTripStorage, |
| | | formatDurationDisplay |
| | | } from '../../../../utils/Robotaxi-public-method' |
| | | import { TripStatus } from '../constants/trip-status'; |
| | | |
| | | |
| | | type Props = { |
| | |
| | | startSite?: any; |
| | | endSite?: any; |
| | | waitMinutes?: number; |
| | | tripStatus?: number; |
| | | tripStatus?: TripStatus; |
| | | routeDurationSeconds?: number | null; |
| | | discountAmount?: number; |
| | | onClose?: () => void; |
| | |
| | | tip: discountAmountValue > 0 ? `本单限时优惠${discountAmountValue}元,重新叫车可能等待更久` : '', |
| | | }, |
| | | 2: { |
| | | title: tripStatus === 4 |
| | | title: tripStatus === TripStatus.VehicleApproaching |
| | | ? `车辆还有${durationDisplay.value}${durationDisplay.unit}到达,确定取消吗` |
| | | : `车辆还有${waitMinutesValue}分钟到达,确定取消吗`, |
| | | tip: discountAmountValue > 0 ? `本单限时优惠${discountAmountValue}元,重新叫车可能等待更久` : '', |
| New file |
| | |
| | | /** |
| | | * 页面行程状态枚举:与历史 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, // 取消订单时只展示上车点 |
| | | } |
| | | |
| | | /** |
| | | * 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, // 已完成 |
| | | } |
| | | |
| | | const TRIP_STATUS_SET = new Set<number>(Object.values(TripStatus).filter((value) => typeof value === 'number') as number[]); // 行程状态值集合 |
| | | const SOCKET_STATUS_SET = new Set<number>(Object.values(SocketOrderStatus).filter((value) => typeof value === 'number') as number[]); // socket 状态值集合 |
| | | |
| | | export const DEFAULT_TRIP_STATUS = TripStatus.PublishRoute; // 默认行程状态 |
| | | |
| | | /** |
| | | * 将未知值安全转为行程状态,异常值回落到 fallback。 |
| | | */ |
| | | export function toTripStatus(value: unknown, fallback: TripStatus = DEFAULT_TRIP_STATUS): TripStatus { |
| | | const numericValue = Number(value); // 统一转数值 |
| | | if (!Number.isFinite(numericValue)) { |
| | | return fallback; |
| | | } |
| | | if (!TRIP_STATUS_SET.has(numericValue)) { |
| | | return fallback; |
| | | } |
| | | return numericValue as TripStatus; |
| | | } |
| | | |
| | | /** |
| | | * 将未知值安全转为 socket 状态,异常值回落到 fallback。 |
| | | */ |
| | | export function toSocketOrderStatus(value: unknown, fallback: SocketOrderStatus = SocketOrderStatus.PendingAccept): SocketOrderStatus { |
| | | const numericValue = Number(value); // 统一转数值 |
| | | if (!Number.isFinite(numericValue)) { |
| | | return fallback; |
| | | } |
| | | if (!SOCKET_STATUS_SET.has(numericValue)) { |
| | | return fallback; |
| | | } |
| | | return numericValue as SocketOrderStatus; |
| | | } |
| | |
| | | import Taro from '@tarojs/taro'; |
| | | import { useEffect, useRef, useState } from 'react'; |
| | | import { TripStatus } from '../constants/trip-status'; |
| | | |
| | | type Deps = { |
| | | shouldShowWaitingRadar: boolean; |
| | | isAndroid: boolean; |
| | | tripStatus: number; |
| | | tripStatus: TripStatus; |
| | | mapScale: number; |
| | | startSite: any; |
| | | endSite: any; |
| | |
| | | |
| | | // 更新车辆气泡位置 |
| | | const updateCarCalloutPosition = async () => { |
| | | if (!deps.isAndroid || !deps.vehicleLocation || ![4, 6].includes(deps.tripStatus)) { |
| | | if (!deps.isAndroid || !deps.vehicleLocation || ![TripStatus.VehicleApproaching, TripStatus.InService].includes(deps.tripStatus)) { |
| | | setCarCalloutVisible(false); |
| | | return; |
| | | } |
| | |
| | | |
| | | // 更新到达气泡位置 |
| | | const updateArriveCalloutPosition = async () => { |
| | | if (!deps.isAndroid || ![7].includes(deps.tripStatus)) { |
| | | if (!deps.isAndroid || ![TripStatus.ArrivedDestination].includes(deps.tripStatus)) { |
| | | setArriveCalloutVisible(false); |
| | | return; |
| | | } |
| | |
| | | }, [deps.isAndroid, deps.tripStatus, deps.endSite?.latitude, deps.endSite?.longitude]); |
| | | |
| | | useEffect(() => { |
| | | if (!deps.isAndroid || deps.tripStatus !== 7) return; |
| | | if (!deps.isAndroid || deps.tripStatus !== TripStatus.ArrivedDestination) return; |
| | | if (deps.mapScale < 20) return; |
| | | const timer = setTimeout(() => { |
| | | updateArriveCalloutPosition(); |
| | |
| | | import Taro, { useDidShow } from '@tarojs/taro'; |
| | | import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; |
| | | import { TripStatus } from '../constants/trip-status'; |
| | | |
| | | type Deps = { |
| | | tripStatus: number; |
| | | tripStatus: TripStatus; |
| | | bluetoothAuthSuccess: boolean; |
| | | startBluetoothAuth: () => void; |
| | | }; |
| | |
| | | |
| | | // 仅在 4/5 状态维护蓝牙 UI;其他状态强制收起弹窗 |
| | | useEffect(() => { |
| | | if ([4, 5].includes(tripStatus)) { |
| | | if ([TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup].includes(tripStatus)) { |
| | | setStatusReady(false); |
| | | refreshBluetoothState(); |
| | | startBluetoothStateWatch(); |
| | |
| | | |
| | | // 页面回前台后同步蓝牙状态,避免设置页返回后状态滞后 |
| | | useDidShow(() => { |
| | | if ([4, 5].includes(tripStatus)) { |
| | | if ([TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup].includes(tripStatus)) { |
| | | refreshBluetoothState(); |
| | | } |
| | | }); |
| | |
| | | import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; |
| | | import RtAuthBluetoothManager from '../../../utils/car-control/RtAuthBluetoothManager'; |
| | | import { getBluetoothUnlockToken } from '../../../api/car-control'; |
| | | import { TripStatus } from '../constants/trip-status'; |
| | | |
| | | type Deps = { |
| | | orderId: string; |
| | | tripStatus: number; |
| | | tripStatus: TripStatus; |
| | | vehicleInfo: any; |
| | | }; |
| | | |
| | |
| | | */ |
| | | const startAuth = useCallback(async () => { |
| | | console.log('[蓝牙] 开始鉴权入参:', { tripStatus, orderId, bluetoothMac, isAuthorizing, authSuccess }); |
| | | if (![4, 5].includes(tripStatus)) return; |
| | | if (![TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup].includes(tripStatus)) return; |
| | | if (!orderId || !bluetoothMac) return; |
| | | |
| | | const startKey = `${orderId}|${bluetoothMac}`; |
| | |
| | | }, [startAuth]); |
| | | |
| | | useEffect(() => { |
| | | if (![4, 5].includes(tripStatus)) { |
| | | if (![TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup].includes(tripStatus)) { |
| | | console.log('[蓝牙] 当前状态非4/5,断开连接:', tripStatus); |
| | | closeConnection(); |
| | | lastStartKeyRef.current = ''; |
| | |
| | | import Taro from '@tarojs/taro'; |
| | | import { useEffect, useRef, useState } from 'react'; |
| | | import { TripStatus } from '../constants/trip-status'; |
| | | |
| | | type Deps = { |
| | | tripStatus: number; |
| | | tripStatus: TripStatus; |
| | | statusRule: any; |
| | | isAndroid: boolean; |
| | | showTrip1StyleEndpoint: boolean; |
| | |
| | | }; |
| | | |
| | | const defaultDeps: Deps = { |
| | | tripStatus: 1, |
| | | tripStatus: TripStatus.PublishRoute, |
| | | statusRule: { showPickup: true, showDropoff: true, showCar: false }, |
| | | isAndroid: false, |
| | | showTrip1StyleEndpoint: false, |
| | |
| | | ...(useAndroidNativeTrip1Callout |
| | | ? { |
| | | callout: { |
| | | content: (deps.tripStatus === 1 || deps.showTrip1StyleEndpoint) ? `${pickupSite?.name || '上车点'} >` : (pickupSite?.name || '上车点'), |
| | | content: (deps.tripStatus === TripStatus.PublishRoute || deps.showTrip1StyleEndpoint) ? `${pickupSite?.name || '上车点'} >` : (pickupSite?.name || '上车点'), |
| | | color: '#000000', |
| | | fontSize: 12, |
| | | bgColor: '#FFFFFF', |
| | |
| | | }, |
| | | } |
| | | : { |
| | | customCallout: { anchorY: [2, 3].includes(deps.tripStatus) ? 150 : -5, anchorX: 0, display: 'ALWAYS' }, |
| | | customCallout: { anchorY: [TripStatus.Publishing, TripStatus.CallingCar].includes(deps.tripStatus) ? 150 : -5, anchorX: 0, display: 'ALWAYS' }, |
| | | }) |
| | | }; |
| | | }; |
| | |
| | | // 构建下车点 marker |
| | | const createDropoffMarker = (dropoffSite: any, useAndroidNativeTrip1Callout: boolean) => { |
| | | if (!(dropoffSite?.latitude && dropoffSite?.longitude)) return null; |
| | | const dropoffName = [7].includes(deps.tripStatus) ? '已到达' : (dropoffSite.name || '目的地'); |
| | | const dropoffName = [TripStatus.ArrivedDestination].includes(deps.tripStatus) ? '已到达' : (dropoffSite.name || '目的地'); |
| | | return { |
| | | id: 2, |
| | | latitude: dropoffSite.latitude, |
| | |
| | | anchor: { x: 0.5, y: 1 }, |
| | | zIndex: 1, |
| | | name: dropoffName, |
| | | ...(useAndroidNativeTrip1Callout || deps.tripStatus == 6 |
| | | ...(useAndroidNativeTrip1Callout || deps.tripStatus === TripStatus.InService |
| | | ? { |
| | | callout: { |
| | | content: (deps.tripStatus === 1 || deps.showTrip1StyleEndpoint) ? `${dropoffName} >` : dropoffName, |
| | | content: (deps.tripStatus === TripStatus.PublishRoute || deps.showTrip1StyleEndpoint) ? `${dropoffName} >` : dropoffName, |
| | | color: '#000000', |
| | | fontSize: 12, |
| | | bgColor: '#FFFFFF', |
| | |
| | | }; |
| | | }; |
| | | |
| | | // 应用 marker 状态并处理状态6刷新标记 |
| | | // 应用 marker 状态并处理“行驶中”阶段的刷新标记 |
| | | const applyMarkerState = (nextMarkers: any[], nextMarkersSignature: string, shouldSkipUpdate: boolean, carMarker: any) => { |
| | | if (!shouldSkipUpdate && markersSignatureRef.current !== nextMarkersSignature) { |
| | | markersSignatureRef.current = nextMarkersSignature; |
| | | setMarkers(nextMarkers); |
| | | } |
| | | if (!deps.isAndroid && [4, 6].includes(deps.tripStatus) && deps.statusRule.showCar && !!carMarker && !tripStatus6MarkerRefreshedRef.current) { |
| | | if (!deps.isAndroid && [TripStatus.VehicleApproaching, TripStatus.InService].includes(deps.tripStatus) && deps.statusRule.showCar && !!carMarker && !tripStatus6MarkerRefreshedRef.current) { |
| | | tripStatus6MarkerRefreshedRef.current = true; |
| | | } |
| | | carMarkerInitialized.current = deps.statusRule.showCar && !!carMarker; |
| | |
| | | useEffect(() => { |
| | | const pickupSite = deps.startSite?.latitude && deps.startSite?.longitude ? deps.startSite : Taro.getStorageSync('startSite'); |
| | | const dropoffSite = deps.endSite?.latitude && deps.endSite?.longitude ? deps.endSite : Taro.getStorageSync('endSite'); |
| | | const useAndroidNativeTrip1Callout = deps.isAndroid && ([1, 5, 8, 9, 10].includes(deps.tripStatus) || deps.showTrip1StyleEndpoint); |
| | | const useAndroidNativeTrip1Callout = deps.isAndroid && ([TripStatus.PublishRoute, TripStatus.VehicleArrivedPickup, TripStatus.TripFinished, TripStatus.PaymentSuccess, TripStatus.CancelOnlyPickup].includes(deps.tripStatus) || deps.showTrip1StyleEndpoint); |
| | | const pickupMarker = createPickupMarker(pickupSite, useAndroidNativeTrip1Callout); |
| | | const dropoffMarker = createDropoffMarker(dropoffSite, useAndroidNativeTrip1Callout); |
| | | const shouldWaitArrivedDirection = deps.tripStatus === 7 && !deps.arrivedMarkerReady; |
| | | const shouldWaitArrivedDirection = deps.tripStatus === TripStatus.ArrivedDestination && !deps.arrivedMarkerReady; |
| | | const effectiveShowCar = deps.statusRule.showCar && !shouldWaitArrivedDirection; |
| | | const carMarker = createCarMarker(); |
| | | const nextMarkers: any[] = []; |
| | |
| | | ].join('|')) |
| | | .join(';'); |
| | | const hasDropoffMarker = markersRef.current.some((m: any) => m.id === 2); |
| | | const shouldHoldMarkerByTranslate = [4, 6].includes(deps.tripStatus) && carMarkerInitialized.current && deps.statusRule.showCar && (!deps.statusRule.showDropoff || hasDropoffMarker); |
| | | const shouldHoldMarkerByTranslate = [TripStatus.VehicleApproaching, TripStatus.InService].includes(deps.tripStatus) && carMarkerInitialized.current && deps.statusRule.showCar && (!deps.statusRule.showDropoff || hasDropoffMarker); |
| | | const shouldSkipUpdate = shouldHoldMarkerByTranslate && (deps.isAndroid || tripStatus6MarkerRefreshedRef.current); |
| | | applyMarkerState(nextMarkers, nextMarkersSignature, shouldSkipUpdate, carMarker); |
| | | }, [deps.tripStatus, deps.startSite?.name, deps.startSite?.latitude, deps.startSite?.longitude, deps.endSite?.name, deps.endSite?.latitude, deps.endSite?.longitude, deps.vehicleLocation, deps.vehicleDirection, deps.vehicleMarkerId, deps.isAndroid, deps.statusRule.showPickup, deps.statusRule.showCar, deps.statusRule.showDropoff]); |
| | |
| | | import Taro from '@tarojs/taro'; |
| | | import { useState, useRef, useCallback, useMemo, useEffect } from 'react'; |
| | | import { computeAdaptiveBottomPadding } from '../../../../utils/Robotaxi-public-method'; |
| | | import { TripStatus } from '../constants/trip-status'; |
| | | |
| | | type Deps = { |
| | | tripStatus: number; |
| | | tripStatus: TripStatus; |
| | | statusRule: any; |
| | | routePoints: any[]; |
| | | walkingPoints: any[]; |
| | |
| | | }; |
| | | |
| | | const defaultDeps: Deps = { |
| | | tripStatus: 1, |
| | | tripStatus: TripStatus.PublishRoute, |
| | | statusRule: { viewport: 'pickup' }, |
| | | routePoints: [], |
| | | walkingPoints: [], |
| | |
| | | // 按视口类型构建 includePoints 入参 |
| | | const buildViewportPayload = (viewportType: string, pickupSite: any, dropoffSite: any) => { |
| | | const points: any[] = []; |
| | | const keyParts: any[] = [viewportType === 'order_detail' ? 8 : deps.tripStatus, viewportType]; |
| | | const keyParts: any[] = [viewportType === 'order_detail' ? TripStatus.TripFinished : deps.tripStatus, viewportType]; |
| | | if (viewportType === 'route_walking') { |
| | | if (deps.routePoints.length > 0) points.push(...deps.routePoints); |
| | | if (deps.walkingPoints.length > 0) points.push(...deps.walkingPoints); |
| | |
| | | |
| | | const toOrigin = () => { |
| | | const mapCtx = Taro.createMapContext('map'); |
| | | if (deps.tripStatus === 6 && deps.routePoints.length > 0) { |
| | | if (deps.tripStatus === TripStatus.InService && deps.routePoints.length > 0) { |
| | | const bottomPadding = computeBottomPadding(deps.routePoints, 'route'); |
| | | const topPadding = Math.max(24, resolveSafeTopInset() + 32); |
| | | mapCtx.includePoints({ |
| | |
| | | const getLocationInfo = () => { |
| | | Taro.getLocation({ type: 'gcj02' }) |
| | | .then((res) => { |
| | | if (deps.tripStatus === 7) { |
| | | if (deps.tripStatus === TripStatus.ArrivedDestination) { |
| | | return; |
| | | } |
| | | deps.setLongitude(res.longitude); |
| | | deps.setLatitude(res.latitude); |
| | | if (deps.tripStatus === 6 && deps.routePoints.length > 0) { |
| | | if (deps.tripStatus === TripStatus.InService && deps.routePoints.length > 0) { |
| | | const bottomPadding = computeBottomPadding(deps.routePoints, 'route'); |
| | | const topPadding = Math.max(24, resolveSafeTopInset() + 32); |
| | | Taro.createMapContext('map').includePoints({ |
| | |
| | | }; |
| | | |
| | | useEffect(() => { |
| | | if ([6, 7].includes(deps.tripStatus)) { |
| | | if ([TripStatus.InService, TripStatus.ArrivedDestination].includes(deps.tripStatus)) { |
| | | setShowLocationFlag(false); |
| | | return; |
| | | } |
| | |
| | | }, [deps.tripStatus, deps.statusRule?.viewport, deps.endSite?.latitude, deps.endSite?.longitude, mapScale]); |
| | | |
| | | useEffect(() => { |
| | | if (![8, 9].includes(deps.tripStatus)) { |
| | | if (![TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(deps.tripStatus)) { |
| | | orderDetailViewportLockedRef.current = false; |
| | | orderDetailRouteUpdatedAfterEnterRef.current = false; |
| | | orderDetailEnterSignatureRef.current = ''; |
| | |
| | | }, [deps.tripStatus]); |
| | | |
| | | useEffect(() => { |
| | | if (![8, 9].includes(deps.tripStatus)) return; |
| | | if (![TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(deps.tripStatus)) return; |
| | | const { pickupSite, dropoffSite } = resolveViewportSites(); |
| | | const currentSignature = buildOrderDetailRouteSignature(pickupSite, dropoffSite); |
| | | if (!orderDetailEnterSignatureRef.current) { |
| | |
| | | const { points, keyParts } = buildViewportPayload(viewportType, pickupSite, dropoffSite); |
| | | if (points.length === 0) return; |
| | | if ( |
| | | [8, 9].includes(deps.tripStatus) && |
| | | [TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(deps.tripStatus) && |
| | | viewportType === 'order_detail' && |
| | | orderDetailViewportLockedRef.current && |
| | | orderDetailRouteUpdatedAfterEnterRef.current |
| | | ) { |
| | | return; |
| | | } |
| | | if (deps.tripStatus === 6 && viewportType === 'route' && tripStatus6ViewportLockedRef.current) return; |
| | | if (deps.tripStatus === TripStatus.InService && viewportType === 'route' && tripStatus6ViewportLockedRef.current) return; |
| | | const nextKey = keyParts.join('|'); |
| | | if (nextKey === viewportKeyRef.current) return; |
| | | viewportKeyRef.current = nextKey; |
| | | updateViewportByPayload(viewportType, points); |
| | | if (deps.tripStatus === 6 && viewportType === 'route') { |
| | | if (deps.tripStatus === TripStatus.InService && viewportType === 'route') { |
| | | tripStatus6ViewportLockedRef.current = true; |
| | | } |
| | | if ([8, 9].includes(deps.tripStatus) && viewportType === 'order_detail') { |
| | | if ([TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(deps.tripStatus) && viewportType === 'order_detail') { |
| | | const hasOrderDetailRouteAfterEnter = orderDetailRouteUpdatedAfterEnterRef.current && deps.routePoints.length > 0; |
| | | orderDetailViewportLockedRef.current = hasOrderDetailRouteAfterEnter; |
| | | } |
| | |
| | | import { useEffect, useMemo, useState } from 'react'; |
| | | import { orderChange } from '../../../api/begin-route'; |
| | | import { getOrderActive } from '../../../api/home'; |
| | | import { TripStatus } from '../constants/trip-status'; |
| | | import { |
| | | MODIFY_DESTINATION_CANDIDATE_KEY, |
| | | MODIFY_DESTINATION_OVERRIDE_KEY, |
| | |
| | | } from '../constants/modify-destination'; |
| | | |
| | | type Deps = { |
| | | tripStatus: number; |
| | | tripStatus: TripStatus; |
| | | orderId: string; |
| | | endSite: any; |
| | | setEndSite: (site: any) => void; |
| | |
| | | return isValidSite(cached) ? cached : null; |
| | | }); |
| | | |
| | | const MODIFIABLE_TRIP_STATUSES: TripStatus[] = [ |
| | | TripStatus.VehicleApproaching, |
| | | TripStatus.VehicleArrivedPickup, |
| | | TripStatus.InService, |
| | | TripStatus.ArrivedDestination, |
| | | TripStatus.TripFinished, |
| | | TripStatus.PaymentSuccess |
| | | ]; // 可修改目的地的行程状态 |
| | | |
| | | useEffect(() => { |
| | | if (!deps.orderId || ![4, 5, 6, 7, 8, 9].includes(deps.tripStatus)) { |
| | | if (!deps.orderId || !MODIFIABLE_TRIP_STATUSES.includes(deps.tripStatus)) { |
| | | setPreviewEndSite(null); |
| | | setPreviousEndSite(null); |
| | | setPreviousFareEstimate(null); |
| | | setPreviousRoutePoints([]); |
| | | if (![4, 5, 6, 7, 8, 9].includes(deps.tripStatus)) { |
| | | if (!MODIFIABLE_TRIP_STATUSES.includes(deps.tripStatus)) { |
| | | clearModifyDestinationSession(); |
| | | Taro.removeStorageSync(MODIFY_DESTINATION_OVERRIDE_KEY); |
| | | setOverrideEndSite(null); |
| | |
| | | import { saveOrder, getOrderPayment, cancelOrder, endOrder } from '../../../api/begin-route'; |
| | | import { checkOtp } from '../../../../utils/tools'; |
| | | import { jumpTo } from '../../../../utils/jump'; |
| | | import { TripStatus } from '../constants/trip-status'; |
| | | |
| | | type Deps = { |
| | | /** 行程状态 */ |
| | | tripStatus: number; |
| | | tripStatus: TripStatus; |
| | | /** 订单 ID */ |
| | | orderId: string; |
| | | /** 结束点 */ |
| | |
| | | /** 取消类型 */ |
| | | cancelType: number; |
| | | /** 状态设置 */ |
| | | setTripStatus: (value: number) => void; |
| | | setTripStatus: (value: TripStatus) => void; |
| | | /** 订单设置 */ |
| | | setOrderId: (value: string) => void; |
| | | /** 下单提交设置 */ |
| | |
| | | return; |
| | | } |
| | | deps.setOrderId(nextOrderId); |
| | | deps.setTripStatus(2); |
| | | deps.setTripStatus(TripStatus.Publishing); |
| | | deps.bindSocketListeners(); |
| | | } catch (error) { |
| | | stopCallSubmitting(); |
| | |
| | | */ |
| | | const restartPlanningRoute = () => { |
| | | deps.setShowCancelPopup(false); |
| | | deps.setTripStatus(1); |
| | | deps.setTripStatus(TripStatus.PublishRoute); |
| | | deps.routeUpdateWalkingRouteForStart(deps.startSite); |
| | | const s = Taro.getStorageSync('startSite'); |
| | | const e = Taro.getStorageSync('endSite'); |
| | |
| | | await cancelOrder(targetOrderId); |
| | | Taro.showToast({ title: '行程已取消', icon: 'success' }); |
| | | clearActiveOrderRuntime(); |
| | | deps.setTripStatus(10); |
| | | deps.setTripStatus(TripStatus.CancelOnlyPickup); |
| | | deps.setCancelType(4); |
| | | return; |
| | | } catch (error) { |
| | |
| | | /** |
| | | * 根据状态打开取消弹窗。 |
| | | */ |
| | | const handleOpenCancelPopupByTripStatus = (status: number, durationDisplay?: any) => { |
| | | const handleOpenCancelPopupByTripStatus = (status: TripStatus, durationDisplay?: any) => { |
| | | if (durationDisplay && ![3, 5].includes(status)) { |
| | | deps.setCancelType(2); |
| | | deps.setShowCancelPopup(true); |
| | | return; |
| | | } |
| | | if ([2, 3].includes(status)) deps.setCancelType(1); |
| | | if (status == 4) deps.setCancelType(2); |
| | | if (status == 5) deps.setCancelType(3); |
| | | if ([TripStatus.Publishing, TripStatus.CallingCar].includes(status)) deps.setCancelType(1); |
| | | if (status === TripStatus.VehicleApproaching) deps.setCancelType(2); |
| | | if (status === TripStatus.VehicleArrivedPickup) deps.setCancelType(3); |
| | | deps.setShowCancelPopup(true); |
| | | }; |
| | | |
| | |
| | | */ |
| | | const handleConfirmOverRoute = () => { |
| | | deps.setShowOverRoutePopup(false); |
| | | deps.setTripStatus(8); |
| | | deps.setTripStatus(TripStatus.TripFinished); |
| | | }; |
| | | |
| | | /** |
| | |
| | | await endOrder(targetOrderId); |
| | | } catch (error) { |
| | | } |
| | | deps.setTripStatus(8); |
| | | deps.setTripStatus(TripStatus.TripFinished); |
| | | }; |
| | | |
| | | /** |
| | |
| | | try { |
| | | await getOrderPayment({ orderId: targetOrderId }); |
| | | Taro.showToast({ title: '支付成功', icon: 'none' }); |
| | | deps.setTripStatus(9); |
| | | Taro.setStorageSync('tripStatus', 9); |
| | | deps.setTripStatus(TripStatus.PaymentSuccess); |
| | | Taro.setStorageSync('tripStatus', TripStatus.PaymentSuccess); |
| | | } catch (error) { |
| | | Taro.showToast({ title: '支付失败,请重试', icon: 'none' }); |
| | | return; |
| | |
| | | import Taro from '@tarojs/taro'; |
| | | import { clearOrderTripStorage } from '../../../../utils/Robotaxi-public-method'; |
| | | import { TripStatus } from '../constants/trip-status'; |
| | | |
| | | type Deps = { |
| | | tripStatus: number; |
| | | tripStatus: TripStatus; |
| | | startSite: any; |
| | | getCheckedEnter: () => boolean; |
| | | setCheckedEnter: (checked: boolean) => void; |
| | |
| | | * 返回首页并清理必要缓存。 |
| | | */ |
| | | const toBack = () => { |
| | | if (deps.tripStatus == 9 || deps.tripStatus == 1) { |
| | | if (deps.tripStatus === TripStatus.PaymentSuccess || deps.tripStatus === TripStatus.PublishRoute) { |
| | | clearOrderTripStorage(); |
| | | Taro.removeStorageSync('endSite'); |
| | | } |
| | |
| | | deps.setCheckedEnter(false); |
| | | return; |
| | | } |
| | | if (deps.tripStatus === 1) { |
| | | if (deps.tripStatus === TripStatus.PublishRoute) { |
| | | const markerId = e.detail.markerId; |
| | | toSelectSite(markerId === 1 ? 'start' : 'end'); |
| | | } |
| | |
| | | import { mapOrderStatusToTripStatus } from '../../../../utils/Robotaxi-public-method'; |
| | | import { getOrderActive, } from '../../../api/home'; |
| | | import { queryVehicleByOrderId } from '../../../api/begin-route'; |
| | | import { toTripStatus, TripStatus } from '../constants/trip-status'; |
| | | |
| | | type Deps = { |
| | | wsHost: string; |
| | | ensureSocketListeners: (wsHost: string) => void; |
| | | setOrderId: (value: string) => void; |
| | | setVehicleInfo: (value: any) => void; |
| | | setTripStatus: (value: number) => void; |
| | | setTripStatus: (value: TripStatus) => void; |
| | | setStartSite: (value: any) => void; |
| | | setEndSite: (value: any) => void; |
| | | setTrunkStatus: (value: boolean) => void; |
| | |
| | | setLongitude: (value: number | null) => void; |
| | | getLocationInfo: () => void; |
| | | routeRequestRouteAndFare: (originLat: number, originLng: number, destLat: number, destLng: number, force?: boolean) => void; |
| | | routeRequestOrderRoute: (orderId: string, status: number) => void; |
| | | routeRequestOrderRoute: (orderId: string, status: TripStatus) => void; |
| | | routeUpdateWalkingRouteForStart: (startSite: any) => void; |
| | | getCurrentTripStatus: () => number; |
| | | getCurrentTripStatus: () => TripStatus; |
| | | getCurrentLongitude: () => number | null; |
| | | getCurrentLatitude: () => number | null; |
| | | }; |
| | |
| | | electronicAreaId: res.xpElectronicFenceAreaId || '', |
| | | time: res.endParkingTimeLimit || 0, |
| | | }; |
| | | const nextStatus = mapOrderStatusToTripStatus(res.status || 1); |
| | | const nextStatus = toTripStatus(mapOrderStatusToTripStatus(res.status || 1)); |
| | | Taro.setStorageSync('tripStatus', nextStatus); |
| | | deps.setTripStatus(nextStatus); |
| | | Taro.setStorageSync('startSite', nextStartSite); |
| | |
| | | /** |
| | | * 同步地图定位并按状态初始化路线。 |
| | | */ |
| | | const syncLocationAndInitRoute = (s: any, e: any, cachedTripStatus: number) => { |
| | | // 状态 7 直接以下车点为中心,避免 getLocationInfo 抢占视口中心 |
| | | if (cachedTripStatus === 7) { |
| | | const syncLocationAndInitRoute = (s: any, e: any, cachedTripStatus: TripStatus) => { |
| | | // 到达目的地阶段直接以下车点为中心,避免 getLocationInfo 抢占视口中心 |
| | | if (cachedTripStatus === TripStatus.ArrivedDestination) { |
| | | const dropoffLat = Number(e?.latitude); |
| | | const dropoffLng = Number(e?.longitude); |
| | | if (Number.isFinite(dropoffLat) && Number.isFinite(dropoffLng)) { |
| | |
| | | } |
| | | const originLng = s?.longitude || deps.getCurrentLongitude(); |
| | | const originLat = s?.latitude || deps.getCurrentLatitude(); |
| | | if (cachedTripStatus === 1 && originLng && originLat && e?.longitude && e?.latitude) { |
| | | if (cachedTripStatus === TripStatus.PublishRoute && originLng && originLat && e?.longitude && e?.latitude) { |
| | | deps.routeRequestRouteAndFare(originLat, originLng, e.latitude, e.longitude, true); |
| | | } |
| | | if (deps.getCurrentTripStatus() === 1 && s?.latitude && s?.longitude) { |
| | | if (deps.getCurrentTripStatus() === TripStatus.PublishRoute && s?.latitude && s?.longitude) { |
| | | deps.routeUpdateWalkingRouteForStart(s); |
| | | } |
| | | }; |
| | |
| | | const e = Taro.getStorageSync('endSite'); |
| | | const cachedOrderId = Taro.getStorageSync('orderId'); |
| | | const cachedTripStatusStorage = Taro.getStorageSync('tripStatus'); |
| | | const cachedTripStatus = Number(cachedTripStatusStorage) || deps.getCurrentTripStatus(); |
| | | const cachedTripStatus = toTripStatus(cachedTripStatusStorage, deps.getCurrentTripStatus()); |
| | | deps.setTripStatus(cachedTripStatus); |
| | | syncCachedOrderVehicle(cachedOrderId); |
| | | syncSitesFromActiveOrder(cachedTripStatusStorage, s, e); |
| | | if (cachedOrderId && [4, 5, 6, 7, 8, 9].includes(cachedTripStatus)) { |
| | | if (cachedOrderId && [TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup, TripStatus.InService, TripStatus.ArrivedDestination, TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(cachedTripStatus)) { |
| | | deps.routeRequestOrderRoute(cachedOrderId, cachedTripStatus); |
| | | } |
| | | syncLocationAndInitRoute(s, e, cachedTripStatus); |
| | |
| | | import { queryOrderRoute, queryOrderTrajectory, queryRouteAndFare } from '../../../api/begin-route'; |
| | | import { normalizeDistanceKm, normalizeDurationSeconds } from '../../../utils/format'; |
| | | import { ensureSelectedCityByLocation } from '../../../utils/city'; |
| | | import { TripStatus } from '../constants/trip-status'; |
| | | |
| | | type Deps = { |
| | | isAndroid: boolean; |
| | | tripStatus: number; |
| | | tripStatusRef: React.MutableRefObject<number>; |
| | | tripStatus: TripStatus; |
| | | tripStatusRef: React.MutableRefObject<TripStatus>; |
| | | statusRule: any; |
| | | shouldShowRoute: boolean; |
| | | allowSelectableRoutes: boolean; |
| | |
| | | |
| | | const defaultDeps: Deps = { |
| | | isAndroid: false, |
| | | tripStatus: 1, |
| | | tripStatusRef: { current: 1 }, |
| | | tripStatus: TripStatus.PublishRoute, |
| | | tripStatusRef: { current: TripStatus.PublishRoute }, |
| | | statusRule: { showWalking: false }, |
| | | shouldShowRoute: false, |
| | | allowSelectableRoutes: false, |
| | |
| | | newEndLng = routeData.destination.longitude ?? newEndLng; |
| | | } |
| | | |
| | | if (deps.tripStatusRef.current === 1 && Number.isFinite(newStartLat) && Number.isFinite(newStartLng) && typeof deps.setStartSite === 'function') { |
| | | if (deps.tripStatusRef.current === TripStatus.PublishRoute && Number.isFinite(newStartLat) && Number.isFinite(newStartLng) && typeof deps.setStartSite === 'function') { |
| | | deps.setStartSite({ |
| | | ...deps.startSite, |
| | | latitude: newStartLat, |
| | |
| | | }); |
| | | } |
| | | |
| | | if (deps.tripStatusRef.current === 1 && Number.isFinite(newEndLat) && Number.isFinite(newEndLng) && typeof deps.setEndSite === 'function') { |
| | | if (deps.tripStatusRef.current === TripStatus.PublishRoute && Number.isFinite(newEndLat) && Number.isFinite(newEndLng) && typeof deps.setEndSite === 'function') { |
| | | deps.setEndSite({ |
| | | ...deps.endSite, |
| | | latitude: newEndLat, |
| | |
| | | applyRoutePoints(points); |
| | | // 如果允许选择路线(当前业务下未发单及修改目的地确认态时 allowSelectableRoutes 为 true) |
| | | if (deps.allowSelectableRoutes) { |
| | | // 在 tripStatus === 1 或 修改目的地确认态 时,我们需要包含所有路线的点,以便自适应视野 |
| | | // 在发布行程阶段或修改目的地确认态时,需要包含所有路线点以便自适应视野 |
| | | const allRoutesPoints: any[] = []; |
| | | allRoutePoints.forEach(pts => { |
| | | if (Array.isArray(pts)) { |
| | |
| | | }; |
| | | |
| | | // 请求订单实时路线/轨迹 |
| | | const requestOrderRoute = (targetOrderId: string, status: number) => { |
| | | const routeType = status <= 5 ? 1 : 2; |
| | | const requestRoute = [8, 9].includes(status) ? queryOrderTrajectory : queryOrderRoute; |
| | | const requestOrderRoute = (targetOrderId: string, status: TripStatus) => { |
| | | const routeType = status <= TripStatus.VehicleArrivedPickup ? 1 : 2; |
| | | const requestRoute = [TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(status) ? queryOrderTrajectory : queryOrderRoute; |
| | | requestRoute({ orderId: targetOrderId, routeType }) |
| | | .then((res: any) => { |
| | | const rawPoints = Array.isArray(res?.polyline) ? res.polyline : []; |
| | |
| | | })) |
| | | .filter((item: any) => Number.isFinite(item.longitude) && Number.isFinite(item.latitude)); |
| | | if (points.length > 0) { |
| | | if (status === 5) { |
| | | if (status === TripStatus.VehicleArrivedPickup) { |
| | | const last = points[points.length - 1]; |
| | | deps.updateVehicleLocationThrottled(last.latitude, last.longitude, true); |
| | | } else { |
| | | applyRoutePoints(points); |
| | | const targetPoint = status === 7 |
| | | const targetPoint = status === TripStatus.ArrivedDestination |
| | | ? points[points.length - 1] |
| | | : points[0]; |
| | | deps.updateVehicleLocationThrottled(targetPoint.latitude, targetPoint.longitude, true); |
| | | if ([4, 6].includes(status)) { |
| | | if ([TripStatus.VehicleApproaching, TripStatus.InService].includes(status)) { |
| | | currentFullRouteRef.current = [...points]; |
| | | lastErasedIndexRef.current = 0; |
| | | orderRouteInitializedRef.current = true; |
| | | } |
| | | } |
| | | } else if (status === 7) { |
| | | } else if (status === TripStatus.ArrivedDestination) { |
| | | const cachedEndSite = Taro.getStorageSync('endSite') || {}; |
| | | const fallbackLat = Number(cachedEndSite?.latitude); |
| | | const fallbackLng = Number(cachedEndSite?.longitude); |
| | |
| | | |
| | | // 获取当前用于渲染的路线点 |
| | | const getDisplayRoutePoints = () => { |
| | | const isEraseMode = [4, 6].includes(deps.tripStatus); |
| | | const isEraseMode = [TripStatus.VehicleApproaching, TripStatus.InService].includes(deps.tripStatus); |
| | | return isEraseMode ? routePoints : reducePoints(routePoints, deps.isAndroid ? 120 : 200); |
| | | }; |
| | | |
| | |
| | | lastPolylineUpdateAtRef.current = Date.now(); |
| | | return; |
| | | } |
| | | if (![8, 9].includes(deps.tripStatus)) { |
| | | if (![TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(deps.tripStatus)) { |
| | | setPolyline([]); |
| | | } |
| | | lastPolylineUpdateAtRef.current = 0; |
| | |
| | | import { useEffect } from 'react'; |
| | | import { preloadImageToTemp } from '../../../../utils/Robotaxi-public-method'; |
| | | import { getOrderDetail } from '../../../api/order'; |
| | | import { TripStatus } from '../constants/trip-status'; |
| | | |
| | | type Deps = { |
| | | /** 等待计时开始 key */ |
| | |
| | | /** 重新报价过期秒数 */ |
| | | requoteExpireSeconds: number; |
| | | /** 行程状态 */ |
| | | tripStatus: number; |
| | | tripStatus: TripStatus; |
| | | /** 行程状态引用 */ |
| | | tripStatusRef: React.MutableRefObject<number>; |
| | | tripStatusRef: React.MutableRefObject<TripStatus>; |
| | | /** 上一行程状态引用 */ |
| | | lastTripStatusRef: React.MutableRefObject<number>; |
| | | lastTripStatusRef: React.MutableRefObject<TripStatus>; |
| | | /** 行程同步引用 */ |
| | | tripStatusForMapSyncRef: React.MutableRefObject<number>; |
| | | tripStatusForMapSyncRef: React.MutableRefObject<TripStatus>; |
| | | /** 重新报价触发引用 */ |
| | | requoteTriggeredRef: React.MutableRefObject<boolean>; |
| | | /** 路线是否初始化 */ |
| | |
| | | /** 设置 polyline */ |
| | | setPolyline: (value: any[]) => void; |
| | | /** 行程状态设置 */ |
| | | setTripStatus: (value: number) => void; |
| | | setTripStatus: (value: TripStatus) => void; |
| | | /** 订单状态重置版本 */ |
| | | setResetDetailVersion: (updater: (v: number) => number) => void; |
| | | /** 是否提交中 */ |
| | |
| | | /** |
| | | * 刷新完成态轨迹。 |
| | | */ |
| | | const refreshOrderDetailRoute = (prevTripStatus: number) => { |
| | | const shouldSkipOrderDetailRefresh = prevTripStatus === 8 && deps.tripStatus === 9; |
| | | const refreshOrderDetailRoute = (prevTripStatus: TripStatus) => { |
| | | const shouldSkipOrderDetailRefresh = prevTripStatus === TripStatus.TripFinished && deps.tripStatus === TripStatus.PaymentSuccess; |
| | | if (shouldSkipOrderDetailRefresh) return; |
| | | deps.orderDetailRouteRequestIdRef.current += 1; |
| | | const currentRequestId = deps.orderDetailRouteRequestIdRef.current; |
| | | getOrderDetail(Taro.getStorageSync('orderId')).then((res: any) => { |
| | | if (currentRequestId !== deps.orderDetailRouteRequestIdRef.current) return; |
| | | if (![8, 9].includes(deps.tripStatusRef.current)) return; |
| | | if (![TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(deps.tripStatusRef.current)) return; |
| | | const mapCtx = Taro.createMapContext('map'); |
| | | if (!res.trajectories) { |
| | | mapCtx.moveToLocation({ latitude: 39.916527, longitude: 116.397128 }); |
| | |
| | | }; |
| | | |
| | | useEffect(() => { |
| | | const wasHold = [4, 6].includes(deps.lastTripStatusRef.current); |
| | | const isHold = [4, 6].includes(deps.tripStatus); |
| | | if (deps.tripStatus != 5) Taro.removeStorageSync('pickupEndTime'); |
| | | if (deps.tripStatus != 7) Taro.removeStorageSync('finishEndTime'); |
| | | const wasHold = [TripStatus.VehicleApproaching, TripStatus.InService].includes(deps.lastTripStatusRef.current); |
| | | const isHold = [TripStatus.VehicleApproaching, TripStatus.InService].includes(deps.tripStatus); |
| | | if (deps.tripStatus !== TripStatus.VehicleArrivedPickup) Taro.removeStorageSync('pickupEndTime'); |
| | | if (deps.tripStatus !== TripStatus.ArrivedDestination) Taro.removeStorageSync('finishEndTime'); |
| | | if (!wasHold && isHold) { |
| | | deps.orderRouteInitializedRef.current = false; |
| | | deps.tripStatus6ViewportLockedRef.current = false; |
| | |
| | | }, [deps.vehicleInfo?.id]); |
| | | |
| | | useEffect(() => { |
| | | if (deps.tripStatus === 1) checkAndShowPassengerNotice(); |
| | | if (![2, 3].includes(deps.tripStatus)) return; |
| | | if (deps.tripStatus === TripStatus.PublishRoute) checkAndShowPassengerNotice(); |
| | | if (![TripStatus.Publishing, TripStatus.CallingCar].includes(deps.tripStatus)) return; |
| | | let cancelled = false; |
| | | const load = async (url: string, setter: (value: string) => void) => { |
| | | const local = await preloadImageToTemp(url); |
| | |
| | | const prevTripStatus = deps.tripStatusForMapSyncRef.current; |
| | | let timer: any = null; |
| | | let requoteTimer: any = null; |
| | | if (deps.tripStatus == 1) { |
| | | if (deps.tripStatus === TripStatus.PublishRoute) { |
| | | deps.setCallSubmitting(false); |
| | | requoteTimer = startRequoteTimer(); |
| | | } else if (deps.tripStatus == 2 || deps.tripStatus == 3) { |
| | | } else if (deps.tripStatus === TripStatus.Publishing || deps.tripStatus === TripStatus.CallingCar) { |
| | | deps.requoteTriggeredRef.current = false; |
| | | deps.setWalkingPoints([]); |
| | | deps.setWalkingTime(0); |
| | | timer = startWaitingTimer(); |
| | | } else if ([4, 5].includes(deps.tripStatus)) { |
| | | } else if ([TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup].includes(deps.tripStatus)) { |
| | | deps.requoteTriggeredRef.current = false; |
| | | const cachedStartSite = Taro.getStorageSync('startSite') || {}; |
| | | if (cachedStartSite?.latitude && cachedStartSite?.longitude) { |
| | |
| | | } |
| | | deps.setElapsedSeconds(0); |
| | | clearWaitingTimerStorage(); |
| | | } else if (deps.tripStatus == 7) { |
| | | } else if (deps.tripStatus === TripStatus.ArrivedDestination) { |
| | | deps.requoteTriggeredRef.current = false; |
| | | deps.setWalkingPoints([]); |
| | | deps.setWalkingTime(0); |
| | |
| | | } |
| | | deps.setElapsedSeconds(0); |
| | | clearWaitingTimerStorage(); |
| | | } else if ([8, 9].includes(deps.tripStatus)) { |
| | | } else if ([TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(deps.tripStatus)) { |
| | | deps.requoteTriggeredRef.current = false; |
| | | deps.setWalkingPoints([]); |
| | | deps.setWalkingTime(0); |
| | |
| | | useDidHide(() => { |
| | | deps.setResetDetailVersion((v: number) => v + 1); |
| | | const currentTripStatus = deps.tripStatusRef.current; |
| | | if (currentTripStatus === 2 || currentTripStatus === 3) { |
| | | if (currentTripStatus === TripStatus.Publishing || currentTripStatus === TripStatus.CallingCar) { |
| | | const startAt = Number(Taro.getStorageSync(deps.waitingTimerStartKey)); |
| | | if (Number.isFinite(startAt) && startAt > 0) { |
| | | const currentElapsed = Math.max(0, Math.floor((Date.now() - startAt) / 1000)); |
| | |
| | | useEffect(() => { |
| | | const originLng = deps.startSite?.longitude || deps.longitude; |
| | | const originLat = deps.startSite?.latitude || deps.latitude; |
| | | if (deps.tripStatus === 1 && originLng && originLat && deps.endSite?.longitude && deps.endSite?.latitude) { |
| | | if (deps.tripStatus === TripStatus.PublishRoute && originLng && originLat && deps.endSite?.longitude && deps.endSite?.latitude) { |
| | | deps.routeRequestRouteAndFare(originLat, originLng, deps.endSite.latitude, deps.endSite.longitude); |
| | | } |
| | | if (deps.tripStatus === 1 && deps.startSite?.latitude && deps.startSite?.longitude) { |
| | | if (deps.tripStatus === TripStatus.PublishRoute && deps.startSite?.latitude && deps.startSite?.longitude) { |
| | | deps.routeUpdateWalkingRouteForStart(deps.startSite); |
| | | } |
| | | }, [deps.startSite?.longitude, deps.startSite?.latitude, deps.endSite?.longitude, deps.endSite?.latitude, deps.longitude, deps.latitude]); |
| | |
| | | import CallingCar from '../components/calling-car'; |
| | | import ModifyDestinationReview from '../components/modify-destination-review'; |
| | | import { formatDurationDisplay } from '../../../../utils/Robotaxi-public-method'; |
| | | import { TripStatus } from '../constants/trip-status'; |
| | | |
| | | type Deps = { |
| | | /** 是否安卓 */ |
| | | isAndroid: boolean; |
| | | /** 行程状态 */ |
| | | tripStatus: number; |
| | | tripStatus: TripStatus; |
| | | /** 地图缩放等级 */ |
| | | mapScale: number; |
| | | /** 地图 markers */ |
| | |
| | | /** 发单禁用 */ |
| | | isCallDisabled: boolean; |
| | | /** 打开取消弹窗 */ |
| | | handleOpenCancelPopupByTripStatus: (status: number, durationDisplay?: any) => void; |
| | | handleOpenCancelPopupByTripStatus: (status: TripStatus, durationDisplay?: any) => void; |
| | | /** 取消超时 */ |
| | | handleOpenCancelPopupWithType: (type: number) => void; |
| | | /** 行程状态设置 */ |
| | | setTripStatus: (value: number) => void; |
| | | setTripStatus: (value: TripStatus) => void; |
| | | /** 完成倒计时启用 */ |
| | | finishCountdownEnabled: boolean; |
| | | /** 订单 ID */ |
| | |
| | | ) : null |
| | | ) : ( |
| | | <> |
| | | {(item.name !== '车辆位置' && !(item.id === 2 && [7].includes(deps.tripStatus))) && ( |
| | | {(item.name !== '车辆位置' && !(item.id === 2 && [TripStatus.ArrivedDestination].includes(deps.tripStatus))) && ( |
| | | <CoverView className={`custom-callout-content ${deps.isAndroid && 'txt-center '}`} style={{ zIndex: 1 }}> |
| | | <CoverView style={{ lineHeight: '22px', display: 'flex', alignItems: 'center' }}>{item.name}</CoverView> |
| | | {deps.isAndroid && <CoverView style={{ width: '2px' }}></CoverView>} |
| | | {(deps.tripStatus == 1 || deps.showTrip1StyleEndpoint) && <CoverImage className='custom-callout-icon' src={deps.customCalloutIconSrc} />} |
| | | {(deps.tripStatus === TripStatus.PublishRoute || deps.showTrip1StyleEndpoint) && <CoverImage className='custom-callout-icon' src={deps.customCalloutIconSrc} />} |
| | | </CoverView> |
| | | )} |
| | | {(item.name === '车辆位置' && [4, 6].includes(deps.tripStatus) && !deps.isAndroid) && ( |
| | | {(item.name === '车辆位置' && [TripStatus.VehicleApproaching, TripStatus.InService].includes(deps.tripStatus) && !deps.isAndroid) && ( |
| | | <CoverView className='custom-callout-content' style={{ padding: '6px 10px', minWidth: '40px', justifyContent: 'center', zIndex: 2 }}> |
| | | <CoverView className='car-time'> |
| | | <CoverView className='car-time-num'>{deps.routeDistance?.value ?? 0}</CoverView> |
| | |
| | | </CoverView> |
| | | </CoverView> |
| | | )} |
| | | {(item.id === 2 && [7].includes(deps.tripStatus) && !deps.isAndroid && deps.arrivedTooltipReady) && ( |
| | | {(item.id === 2 && [TripStatus.ArrivedDestination].includes(deps.tripStatus) && !deps.isAndroid && deps.arrivedTooltipReady) && ( |
| | | <CoverView className='custom-callout-content'> |
| | | 已到达 |
| | | </CoverView> |
| | |
| | | const renderMarkerCallout = (item: any, index: number) => { |
| | | const calloutKey = `callout-${item.id}-${item.name || ''}`; |
| | | if (item.isRouteSummaryBubble) return renderDefaultCallout(item, calloutKey); |
| | | const isWaiting = [2, 3].includes(deps.tripStatus) && index === 0; |
| | | const isWaiting = [TripStatus.Publishing, TripStatus.CallingCar].includes(deps.tripStatus) && index === 0; |
| | | if (isWaiting) return renderWaitingCallout(item, calloutKey); |
| | | if (item.name === '车辆位置' && deps.isAndroid && [4, 6].includes(deps.tripStatus)) return null; |
| | | if (item.name === '车辆位置' && deps.isAndroid && [TripStatus.VehicleApproaching, TripStatus.InService].includes(deps.tripStatus)) return null; |
| | | return renderDefaultCallout(item, calloutKey); |
| | | }; |
| | | |
| | |
| | | */ |
| | | const renderMapOverlays = () => ( |
| | | <> |
| | | {!(deps.isAndroid && [1, 5, 8, 9, 10].includes(deps.tripStatus)) && deps.markers.map(renderMarkerCallout)} |
| | | {!(deps.isAndroid && [TripStatus.PublishRoute, TripStatus.VehicleArrivedPickup, TripStatus.TripFinished, TripStatus.PaymentSuccess, TripStatus.CancelOnlyPickup].includes(deps.tripStatus)) && deps.markers.map(renderMarkerCallout)} |
| | | {deps.shouldShowWaitingRadar && deps.waitingRadarVisible && ( |
| | | <Image className="waiting-radar-img" src={deps.waitingRadarSrc} webp={true} style={deps.waitingRadarStyle} /> |
| | | )} |
| | |
| | | * 渲染底部行程卡片。 |
| | | */ |
| | | const renderTripCard = () => { |
| | | if (deps.tripStatus == 1 && deps.startSite && deps.endSite) { |
| | | if (deps.tripStatus === TripStatus.PublishRoute && deps.startSite && deps.endSite) { |
| | | return ( |
| | | <ReleaseRoute |
| | | startSite={deps.startSite} |
| | |
| | | /> |
| | | ); |
| | | } |
| | | if (deps.tripStatus == 2) { |
| | | if (deps.tripStatus === TripStatus.Publishing) { |
| | | return ( |
| | | <ReleaseBeBeing |
| | | handleOpenCancelPopup={() => deps.handleOpenCancelPopupByTripStatus(2)} |
| | | handleOpenCancelPopup={() => deps.handleOpenCancelPopupByTripStatus(TripStatus.Publishing)} |
| | | onFinish={() => { |
| | | deps.setTripStatus(3); |
| | | Taro.setStorageSync('tripStatus', 3); |
| | | deps.setTripStatus(TripStatus.CallingCar); |
| | | Taro.setStorageSync('tripStatus', TripStatus.CallingCar); |
| | | }} |
| | | /> |
| | | ); |
| | | } |
| | | if (deps.tripStatus >= 3 && deps.tripStatus !== 10) { |
| | | if (deps.tripStatus >= TripStatus.CallingCar && deps.tripStatus !== TripStatus.CancelOnlyPickup) { |
| | | return ( |
| | | <CallingCar |
| | | key={deps.tripStatus} |
| | |
| | | import { queryVehicleByOrderId } from '../../../api/begin-route'; |
| | | import { clearOrderTripStorage, safeParse } from '../../../../utils/Robotaxi-public-method'; |
| | | import { normalizeDistanceKm, normalizeDurationSeconds } from '../../../../robotaxi/utils/format'; |
| | | import { toTripStatus, TripStatus } from '../constants/trip-status'; |
| | | |
| | | type UseWebSocketEventsDeps = { |
| | | tripStatusRef: any; |
| | | tripStatusRef: React.MutableRefObject<TripStatus>; |
| | | orderId: string; |
| | | setOrderId: (orderId: string) => void; |
| | | setTripStatus: (status: number) => void; |
| | | setTripStatus: (status: TripStatus) => void; |
| | | setVehicleInfo: (vehicle: any) => void; |
| | | vehicleInfo: any; |
| | | setPriceUpdateVersion: (updater: (v: number) => number) => void; |
| | |
| | | }; |
| | | |
| | | const syncTripStatus = (payload: any, deps: UseWebSocketEventsDeps) => { |
| | | const mappedTripStatus = payload?.tripStatus; |
| | | if (typeof mappedTripStatus !== 'number') { |
| | | const incomingTripStatus = Number(payload?.tripStatus); |
| | | if (!Number.isFinite(incomingTripStatus)) { |
| | | return null; |
| | | } |
| | | const currentTripStatus = Number(deps.tripStatusRef.current); |
| | | const currentTripStatus = deps.tripStatusRef.current; |
| | | const mappedTripStatus = toTripStatus(incomingTripStatus, currentTripStatus); |
| | | // 行程进行关键态不允许被回退,避免 4/6 被延迟状态覆盖导致路线链路重置 |
| | | if ([4, 6].includes(currentTripStatus) && mappedTripStatus < currentTripStatus) { |
| | | if ([TripStatus.VehicleApproaching, TripStatus.InService].includes(currentTripStatus) && mappedTripStatus < currentTripStatus) { |
| | | return currentTripStatus; |
| | | } |
| | | if (currentTripStatus === mappedTripStatus) { |
| | |
| | | .catch(() => { }); |
| | | }; |
| | | |
| | | const handleCancelPopupPush = (payload: any, changeStatusOld: number, deps: UseWebSocketEventsDeps) => { |
| | | const handleCancelPopupPush = (payload: any, changeStatusOld: TripStatus, deps: UseWebSocketEventsDeps) => { |
| | | if (!payload?.showCancelPopup) return; |
| | | if (changeStatusOld === 4 || changeStatusOld === 5) { |
| | | if (changeStatusOld === TripStatus.VehicleApproaching || changeStatusOld === TripStatus.VehicleArrivedPickup) { |
| | | deps.setOrderId(''); |
| | | clearOrderTripStorage(); |
| | | Taro.removeStorageSync('pickupEndTime'); |
| | |
| | | } else { |
| | | deps.setCancelType(5); |
| | | } |
| | | deps.setTripStatus(10); |
| | | deps.setTripStatus(TripStatus.CancelOnlyPickup); |
| | | deps.setShowCancelPopup(true); |
| | | }; |
| | | |
| | | const handleStatusSideEffects = (mappedTripStatus: number | null, changeStatusOld: number, deps: UseWebSocketEventsDeps) => { |
| | | if (mappedTripStatus === 6) { |
| | | const handleStatusSideEffects = (mappedTripStatus: TripStatus | null, changeStatusOld: TripStatus, deps: UseWebSocketEventsDeps) => { |
| | | if (mappedTripStatus === TripStatus.InService) { |
| | | deps.setShowAnnouncementsPopup(false); |
| | | } |
| | | if (mappedTripStatus === 5 && changeStatusOld !== 5) { |
| | | if (mappedTripStatus === TripStatus.VehicleArrivedPickup && changeStatusOld !== TripStatus.VehicleArrivedPickup) { |
| | | resetRouteRuntimeState(deps); |
| | | } |
| | | if (mappedTripStatus === 4 && changeStatusOld !== 4) { |
| | | if (mappedTripStatus === TripStatus.VehicleApproaching && changeStatusOld !== TripStatus.VehicleApproaching) { |
| | | resetRouteRuntimeState(deps); |
| | | } |
| | | }; |
| | |
| | | if (!targetPoint) { |
| | | return; |
| | | } |
| | | const currentTripStatus = Number(deps.tripStatusRef.current); |
| | | if (currentTripStatus === 7) { |
| | | const currentTripStatus = deps.tripStatusRef.current; |
| | | if (currentTripStatus === TripStatus.ArrivedDestination) { |
| | | const lastPoint = deps.lastVehiclePointRef.current; |
| | | const latDiff = Math.abs(Number(lastPoint?.latitude || 0) - targetPoint.latitude); |
| | | const lngDiff = Math.abs(Number(lastPoint?.longitude || 0) - targetPoint.longitude); |
| | |
| | | Taro.nextTick(() => { |
| | | const updated = deps.applyRoutePoints(points); |
| | | if (!updated) return; |
| | | if (![4, 5, 6, 7].includes(deps.tripStatusRef.current)) return; |
| | | if (![TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup, TripStatus.InService, TripStatus.ArrivedDestination].includes(deps.tripStatusRef.current)) return; |
| | | syncVehiclePointWithRoute(points, deps, servedPoints); |
| | | }); |
| | | }; |
| | |
| | | const points = payload?.points || []; |
| | | const servedPoints = Array.isArray(payload?.servedPoints) ? payload.servedPoints : []; |
| | | const forceFullRedraw = payload?.forceFullRedraw === true; |
| | | const currentTripStatus = Number(deps.tripStatusRef.current); |
| | | const currentTripStatus = deps.tripStatusRef.current; |
| | | updateRouteSummary(payload, deps); |
| | | // 行程结束态仅更新概要信息,不再消费路线点,避免视口反复重算造成跳动 |
| | | if ([8, 9].includes(currentTripStatus)) { |
| | | if ([TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(currentTripStatus)) { |
| | | return; |
| | | } |
| | | if ([4, 6].includes(currentTripStatus)) { |
| | | if ([TripStatus.VehicleApproaching, TripStatus.InService].includes(currentTripStatus)) { |
| | | deps.setVehicleMoveAlongPoints(servedPoints); |
| | | } |
| | | if (forceFullRedraw) { |
| | | handleForceFullRouteRedraw(points, deps, servedPoints); |
| | | return; |
| | | } |
| | | const isEraseStatus = ![7, 8, 9].includes(currentTripStatus); |
| | | const isEraseStatus = ![TripStatus.ArrivedDestination, TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(currentTripStatus); |
| | | if (isEraseStatus) { |
| | | handleEraseRouteStatus(points, servedPoints, deps); |
| | | return; |
| | |
| | | import useBluetoothAccess from './hooks/use-bluetooth-access'; |
| | | // 蓝牙车控连接与鉴权编排 |
| | | import useBluetoothCarControl from './hooks/use-bluetooth-car-control'; |
| | | import { toTripStatus, TripStatus } from './constants/trip-status'; |
| | | |
| | | import backIcon from '../../../robotaxi/assets/back-icon.png' |
| | | import personalCenter from '../../../robotaxi/assets/personal-center.png' |
| | |
| | | import { _ROBOTAXI_WS_HOST } from '../../../constants/api'; |
| | | |
| | | const ROUTE_SUMMARY_MARKER_ID = 999001; |
| | | type StatusRule = { |
| | | showPickup: boolean; |
| | | showDropoff: boolean; |
| | | showCar: boolean; |
| | | showRoute: boolean; |
| | | showWalking: boolean; |
| | | viewport: 'route_walking' | 'pickup' | 'route' | 'car_user' | 'order_detail'; |
| | | translateCar: boolean; |
| | | }; |
| | | |
| | | export default function BeginRoute() { |
| | | const WAITING_TIMER_START_KEY = 'waitingTimerStartAt'; // 等待计时开始时间 |
| | |
| | | const [latitude, setLatitude] = useState<number | null>(null); // 纬度 |
| | | const [startSite, setStartSite] = useState<any>(null); // 上车点 |
| | | const [endSite, setEndSite] = useState<any>({}); // 下车点 |
| | | const [tripStatus, setTripStatus] = useState<number>(Number(Taro.getStorageSync('tripStatus')) || 1); // 行程状态 1:发布行程 2:发布中 3:呼叫车中 4:车辆即将到达 5:车辆到达上车点 6:车辆行驶中 7:到达目的地 8:行程已结束 9:支付成功 10:取消订单时页面只展示一个上车点 |
| | | const [tripStatus, setTripStatus] = useState<TripStatus>(toTripStatus(Taro.getStorageSync('tripStatus'))); // 当前行程状态 |
| | | const [elapsedSeconds, setElapsedSeconds] = useState(0); // 等待计时秒数 |
| | | const requoteTriggeredRef = useRef(false); // 重新报价触发标记,防止重复触发 |
| | | const [callSubmitting, setCallSubmitting] = useState(false); // 呼叫车辆提交中状态 |
| | |
| | | Taro.setStorageSync('tripStatus', tripStatus); |
| | | }, [tripStatus]); |
| | | // 行程状态显示规则配置:showPickup-显示上车点,showDropoff-显示下车点,showCar-显示车辆,showRoute-显示路线,showWalking-显示步行路线,viewport-视口类型,translateCar-车辆是否平移 |
| | | const STATUS_RULES: Record<number, any> = { |
| | | 1: { showPickup: true, showDropoff: true, showCar: false, showRoute: true, showWalking: true, viewport: 'route_walking', translateCar: false }, |
| | | 2: { showPickup: true, showDropoff: false, showCar: false, showRoute: false, showWalking: false, viewport: 'pickup', translateCar: false }, |
| | | 3: { showPickup: true, showDropoff: false, showCar: false, showRoute: false, showWalking: false, viewport: 'pickup', translateCar: false }, |
| | | 4: { showPickup: true, showDropoff: false, showCar: true, showRoute: true, showWalking: true, viewport: 'route', translateCar: true }, |
| | | 5: { showPickup: true, showDropoff: false, showCar: true, showRoute: false, showWalking: true, viewport: 'car_user', translateCar: true }, |
| | | 6: { showPickup: false, showDropoff: true, showCar: true, showRoute: true, showWalking: false, viewport: 'route', translateCar: true }, |
| | | 7: { showPickup: false, showDropoff: true, showCar: true, showRoute: false, showWalking: false, viewport: 'car_user', translateCar: true }, |
| | | 8: { showPickup: true, showDropoff: true, showCar: false, showRoute: true, showWalking: false, viewport: 'order_detail', translateCar: false }, |
| | | 9: { showPickup: true, showDropoff: true, showCar: false, showRoute: true, showWalking: false, viewport: 'order_detail', translateCar: false }, |
| | | 10: { showPickup: true, showDropoff: false, showCar: false, showRoute: false, showWalking: false, viewport: 'pickup', translateCar: false }, |
| | | const STATUS_RULES: Record<TripStatus, StatusRule> = { |
| | | [TripStatus.PublishRoute]: { showPickup: true, showDropoff: true, showCar: false, showRoute: true, showWalking: true, viewport: 'route_walking', translateCar: false }, |
| | | [TripStatus.Publishing]: { showPickup: true, showDropoff: false, showCar: false, showRoute: false, showWalking: false, viewport: 'pickup', translateCar: false }, |
| | | [TripStatus.CallingCar]: { showPickup: true, showDropoff: false, showCar: false, showRoute: false, showWalking: false, viewport: 'pickup', translateCar: false }, |
| | | [TripStatus.VehicleApproaching]: { showPickup: true, showDropoff: false, showCar: true, showRoute: true, showWalking: true, viewport: 'route', translateCar: true }, |
| | | [TripStatus.VehicleArrivedPickup]: { showPickup: true, showDropoff: false, showCar: true, showRoute: false, showWalking: true, viewport: 'car_user', translateCar: true }, |
| | | [TripStatus.InService]: { showPickup: false, showDropoff: true, showCar: true, showRoute: true, showWalking: false, viewport: 'route', translateCar: true }, |
| | | [TripStatus.ArrivedDestination]: { showPickup: false, showDropoff: true, showCar: true, showRoute: false, showWalking: false, viewport: 'car_user', translateCar: true }, |
| | | [TripStatus.TripFinished]: { showPickup: true, showDropoff: true, showCar: false, showRoute: true, showWalking: false, viewport: 'order_detail', translateCar: false }, |
| | | [TripStatus.PaymentSuccess]: { showPickup: true, showDropoff: true, showCar: false, showRoute: true, showWalking: false, viewport: 'order_detail', translateCar: false }, |
| | | [TripStatus.CancelOnlyPickup]: { showPickup: true, showDropoff: false, showCar: false, showRoute: false, showWalking: false, viewport: 'pickup', translateCar: false }, |
| | | }; |
| | | const orderDetailRouteRequestIdRef = useRef(0); // 订单详情路线请求ID |
| | | const isCheckedEnter = useRef(false); // 是否已检查页面进入状态 |
| | |
| | | return false; |
| | | } |
| | | }, []); |
| | | const shouldShowWaitingRadar = isAndroid && [2, 3].includes(tripStatus); // 是否显示等待雷达动画(Android且状态2/3) |
| | | const vehicleMarkerId = !isAndroid && tripStatus === 6 ? 30 : 3; // 车辆标记ID,iOS状态6使用不同ID |
| | | const shouldShowWaitingRadar = isAndroid && [TripStatus.Publishing, TripStatus.CallingCar].includes(tripStatus); // 是否显示等待雷达动画(Android 且处于发布中/呼叫车中) |
| | | const vehicleMarkerId = !isAndroid && tripStatus === TripStatus.InService ? 30 : 3; // 车辆标记 ID,iOS 行驶中阶段使用不同 ID |
| | | const { |
| | | openService, |
| | | toCoupon, |
| | |
| | | } |
| | | }); |
| | | const statusRule = useMemo(() => { |
| | | const baseRule = STATUS_RULES[tripStatus] || STATUS_RULES[1]; |
| | | const baseRule = STATUS_RULES[tripStatus] || STATUS_RULES[TripStatus.PublishRoute]; |
| | | if (isModifyDestinationReviewVisible || hasLocalDestinationOverride) { |
| | | return { |
| | | ...baseRule, |
| | |
| | | return baseRule; |
| | | }, [tripStatus, isModifyDestinationReviewVisible, hasLocalDestinationOverride]); |
| | | const shouldShowRoute = statusRule.showRoute; // 当前状态是否显示路线 |
| | | const allowSelectableRoutes = tripStatus === 1 || isModifyDestinationReviewVisible || hasLocalDestinationOverride; |
| | | const allowSelectableRoutes = tripStatus === TripStatus.PublishRoute || isModifyDestinationReviewVisible || hasLocalDestinationOverride; |
| | | const showTrip1StyleEndpoint = isModifyDestinationReviewVisible || hasLocalDestinationOverride; |
| | | const vehicleState = useVehicleTracking({ |
| | | statusRule, |
| | |
| | | toOrigin, |
| | | getLocationInfo |
| | | } = viewportState; // 视口状态解构 |
| | | const arrivedMarkerReady = tripStatus !== 7 || typeof vehicleDirection === 'number'; |
| | | const arrivedTooltipReady = tripStatus !== 7 || (arrivedMarkerReady && mapScale >= 20); |
| | | const arrivedMarkerReady = tripStatus !== TripStatus.ArrivedDestination || typeof vehicleDirection === 'number'; |
| | | const arrivedTooltipReady = tripStatus !== TripStatus.ArrivedDestination || (arrivedMarkerReady && mapScale >= 20); |
| | | const markerState = useMapMarkers({ |
| | | tripStatus, |
| | | statusRule, |
| | |
| | | const destLat = Number(targetEndSite?.latitude); |
| | | const destLng = Number(targetEndSite?.longitude); |
| | | |
| | | if (![4, 5, 6].includes(tripStatus) || !Number.isFinite(destLat) || !Number.isFinite(destLng)) { |
| | | if (![TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup, TripStatus.InService].includes(tripStatus) || !Number.isFinite(destLat) || !Number.isFinite(destLng)) { |
| | | modifyPreviewRouteKeyRef.current = ''; |
| | | return; |
| | | } |
| | |
| | | let originLat = Number(startSite?.latitude ?? cachedStartSite?.latitude); |
| | | let originLng = Number(startSite?.longitude ?? cachedStartSite?.longitude); |
| | | |
| | | if ([4, 5].includes(tripStatus)) { |
| | | if ([TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup].includes(tripStatus)) { |
| | | if (!Number.isFinite(originLat) || !Number.isFinite(originLng)) { |
| | | return; |
| | | } |
| | | } else if (tripStatus === 6 && Number.isFinite(Number(vehicleLocation?.latitude)) && Number.isFinite(Number(vehicleLocation?.longitude))) { |
| | | } else if (tripStatus === TripStatus.InService && Number.isFinite(Number(vehicleLocation?.latitude)) && Number.isFinite(Number(vehicleLocation?.longitude))) { |
| | | originLat = Number(vehicleLocation?.latitude); |
| | | originLng = Number(vehicleLocation?.longitude); |
| | | } else { |
| | |
| | | const mapLatitude = latitude ?? 39.916527; |
| | | const canToggleRouteSelect = allowSelectableRoutes && routeOptions.length > 1; |
| | | |
| | | const shouldShowOrigin = ![7, 8, 9].includes(tripStatus); |
| | | const shouldShowOrigin = ![TripStatus.ArrivedDestination, TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(tripStatus); |
| | | const handleServiceEntry = () => { |
| | | isCheckedEnter.current = true; |
| | | openService(); |
| | |
| | | toOrder(); |
| | | }; |
| | | const handleWalkNavigation = () => { |
| | | if (![4, 5].includes(tripStatus) || !startSite?.latitude || !startSite?.longitude) { |
| | | if (![TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup].includes(tripStatus) || !startSite?.latitude || !startSite?.longitude) { |
| | | return; |
| | | } |
| | | |
| New file |
| | |
| | | import { View, Image, Map, CoverView, CoverImage } from '@tarojs/components'; |
| | | import { formatDurationDisplay, safeParse } from '../../../../utils/Robotaxi-public-method'; |
| | | import { TripStatus } from '../../begin-route/constants/trip-status'; |
| | | import homeIcon from '../../../assets/back-home.png'; |
| | | import shareContactIcon from '../../../assets/share-contact.png'; |
| | | import contactServiceIcon from '../../../assets/contact-service.png'; |
| | | import shareLine from '../../../assets/share-line.png'; |
| | | |
| | | type Props = { |
| | | isExpired: boolean; |
| | | showReminder: boolean; |
| | | setShowReminder: (value: boolean) => void; |
| | | tripData: any; |
| | | mapLongitude: number; |
| | | mapLatitude: number; |
| | | mapScale: number; |
| | | markers: any[]; |
| | | polyline: any[]; |
| | | routeDuration: number | null; |
| | | renderMapOverlays: () => React.ReactNode; |
| | | goHome: () => void; |
| | | handleContactService: () => void; |
| | | }; |
| | | |
| | | /** |
| | | * 分享页视图组件:仅负责渲染,不承载业务副作用。 |
| | | */ |
| | | export default function ShareTripView({ |
| | | isExpired, |
| | | showReminder, |
| | | setShowReminder, |
| | | tripData, |
| | | mapLongitude, |
| | | mapLatitude, |
| | | mapScale, |
| | | markers, |
| | | polyline, |
| | | routeDuration, |
| | | renderMapOverlays, |
| | | goHome, |
| | | handleContactService, |
| | | }: Props) { |
| | | const durationDisplay = formatDurationDisplay(routeDuration); // 预计耗时展示文案 |
| | | |
| | | if (isExpired) { |
| | | return ( |
| | | <View className='share-trip-expired'> |
| | | <View className='expired-content'> |
| | | <Image className='expired-img' src={shareContactIcon} /> |
| | | <View className='expired-text'>行程结束已超过24小时</View> |
| | | <View className='expired-sub-text'>查看行程信息,请联系客服</View> |
| | | <View className='expired-btn' onClick={handleContactService}> |
| | | 联系客服 |
| | | </View> |
| | | </View> |
| | | </View> |
| | | ); |
| | | } |
| | | |
| | | return ( |
| | | <View className='home-container'> |
| | | <Map |
| | | id='map' |
| | | longitude={mapLongitude} |
| | | latitude={mapLatitude} |
| | | scale={mapScale} |
| | | markers={markers} |
| | | polyline={polyline} |
| | | style={{ width: '100%', height: '87vh' }} |
| | | > |
| | | {renderMapOverlays()} |
| | | <View onClick={goHome} className='select-city'> |
| | | <Image className='select-city-icon' src={homeIcon} /> |
| | | </View> |
| | | <View className='container'> |
| | | <CoverView |
| | | className={`share-contact ${showReminder ? 'has-tip' : ''}`} |
| | | onClick={handleContactService} |
| | | > |
| | | <CoverImage className='share-contact-icon' src={contactServiceIcon} /> |
| | | </CoverView> |
| | | {showReminder && ( |
| | | <View className='tip-box'> |
| | | <View className='tip-box-content'> |
| | | <View className='tip-box-text'>开启提醒后,亲友行程动态秒通知</View> |
| | | <View className='tip-box-btn' onClick={() => setShowReminder(false)}> |
| | | 去开启 |
| | | </View> |
| | | </View> |
| | | </View> |
| | | )} |
| | | <View className='container-inner'> |
| | | <View catchMove onClick={(event) => event.stopPropagation()}> |
| | | <View className='calling-car'> |
| | | <Image |
| | | className='calling-car-bg' |
| | | src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/site-top-bg.png' |
| | | /> |
| | | <View className='calling-car-head'> |
| | | {tripData?.tripStatus === TripStatus.VehicleApproaching && ( |
| | | <View className='calling-car-head-title'> |
| | | 车辆还有 |
| | | <View className='calling-car-head-title-eta'>{durationDisplay.value}</View> |
| | | {durationDisplay.unit}到达上车点 |
| | | </View> |
| | | )} |
| | | {tripData?.tripStatus === TripStatus.VehicleArrivedPickup && ( |
| | | <View className='calling-car-head-title'>车辆已到达上车点</View> |
| | | )} |
| | | {tripData?.tripStatus === TripStatus.InService && ( |
| | | <View className='calling-car-head-title'> |
| | | 预计 |
| | | <View className='calling-car-head-title-eta'>{durationDisplay.value}</View> |
| | | {durationDisplay.unit}抵达目的地 |
| | | </View> |
| | | )} |
| | | {tripData?.tripStatus > TripStatus.ArrivedDestination && ( |
| | | <View className='calling-car-head-title'>已到达目的地</View> |
| | | )} |
| | | </View> |
| | | <View className='car-card'> |
| | | <View className='car-card-detail'> |
| | | <View> |
| | | <View className='car-card-detail-num'>{tripData?.plateNumber || '--'}</View> |
| | | <View className='car-card-detail-color'> |
| | | {tripData?.vehicleColor || '--'} |
| | | <View className='car-card-detail-color-line'></View> |
| | | 车身编码: {tripData?.vehicleCode || '--'} |
| | | </View> |
| | | </View> |
| | | {tripData?.vehiclePictures && ( |
| | | <Image |
| | | className='car-card-detail-img' |
| | | src={ |
| | | safeParse(tripData?.vehiclePictures, {})?.url |
| | | || 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/vehicle-default.png' |
| | | } |
| | | /> |
| | | )} |
| | | </View> |
| | | <View className='car-card-line'></View> |
| | | <View className='car-card-temp'> |
| | | <View className='car-card-temp-list'> |
| | | <Image className='car-card-temp-img' src={shareLine} /> |
| | | <View className='car-card-temp-row'> |
| | | <View className='car-card-temp-row-left'> |
| | | <View className='dot start'></View> |
| | | <View |
| | | className={`car-card-temp-title ${ |
| | | tripData?.tripStatus === TripStatus.InService |
| | | || tripData?.tripStatus === TripStatus.ArrivedDestination |
| | | ? 'disabled-text' |
| | | : '' |
| | | }`} |
| | | > |
| | | 起点:{tripData?.startPointName} |
| | | </View> |
| | | </View> |
| | | {(tripData?.tripStatus === TripStatus.InService |
| | | || tripData?.tripStatus === TripStatus.ArrivedDestination) |
| | | && tripData?.boardingTime && ( |
| | | <View className='car-card-temp-time disabled-text'> |
| | | {tripData.boardingTime} 上车 |
| | | </View> |
| | | )} |
| | | </View> |
| | | <View className='car-card-temp-row' style={{ marginTop: '12px' }}> |
| | | <View className='car-card-temp-row-left'> |
| | | <View className='dot end'></View> |
| | | <View |
| | | className={`car-card-temp-title ${ |
| | | tripData?.tripStatus === TripStatus.ArrivedDestination |
| | | ? 'disabled-text' |
| | | : '' |
| | | }`} |
| | | > |
| | | 终点:{tripData?.endPointName} |
| | | </View> |
| | | </View> |
| | | {tripData?.tripStatus === TripStatus.ArrivedDestination |
| | | && tripData?.getoffTime && ( |
| | | <View className='car-card-temp-time disabled-text'> |
| | | {tripData.getoffTime} 到达 |
| | | </View> |
| | | )} |
| | | </View> |
| | | </View> |
| | | </View> |
| | | </View> |
| | | </View> |
| | | </View> |
| | | </View> |
| | | </View> |
| | | </Map> |
| | | </View> |
| | | ); |
| | | } |
| | |
| | | import Taro from "@tarojs/taro"; |
| | | import { useEffect, useRef, useState } from "react"; |
| | | import { TripStatus } from "../../begin-route/constants/trip-status"; |
| | | |
| | | type Deps = { |
| | | tripStatus: number; |
| | | tripStatus: TripStatus; |
| | | statusRule: any; |
| | | isAndroid: boolean; |
| | | startSite: any; |
| | |
| | | }; |
| | | |
| | | const defaultDeps: Deps = { |
| | | tripStatus: 1, |
| | | tripStatus: TripStatus.PublishRoute, |
| | | statusRule: { showPickup: true, showDropoff: true, showCar: false }, |
| | | isAndroid: false, |
| | | startSite: null, |
| | |
| | | ? { |
| | | callout: { |
| | | content: |
| | | deps.tripStatus === 1 |
| | | deps.tripStatus === TripStatus.PublishRoute |
| | | ? `${pickupSite?.name || "上车点"} >` |
| | | : pickupSite?.name || "上车点", |
| | | color: "#000000", |
| | |
| | | } |
| | | : { |
| | | customCallout: { |
| | | anchorY: [2, 3].includes(deps.tripStatus) ? 150 : -5, |
| | | anchorY: [TripStatus.Publishing, TripStatus.CallingCar].includes(deps.tripStatus) ? 150 : -5, |
| | | anchorX: 0, |
| | | display: "ALWAYS" |
| | | } |
| | |
| | | ) => { |
| | | if (!(dropoffSite?.latitude && dropoffSite?.longitude)) return null; |
| | | const dropoffName = |
| | | [7].includes(deps.tripStatus) ? "已到达" : dropoffSite.name || "目的地"; |
| | | [TripStatus.ArrivedDestination].includes(deps.tripStatus) ? "已到达" : dropoffSite.name || "目的地"; |
| | | return { |
| | | id: 2, |
| | | latitude: dropoffSite.latitude, |
| | |
| | | anchor: { x: 0.5, y: 1 }, |
| | | zIndex: 1, |
| | | name: dropoffName, |
| | | ...(useAndroidNativeTrip1Callout || deps.tripStatus == 6 |
| | | ...(useAndroidNativeTrip1Callout || deps.tripStatus === TripStatus.InService |
| | | ? { |
| | | callout: { |
| | | content: |
| | | deps.tripStatus === 1 |
| | | deps.tripStatus === TripStatus.PublishRoute |
| | | ? `${dropoffName} >` |
| | | : dropoffName, |
| | | color: "#000000", |
| | |
| | | } |
| | | if ( |
| | | !deps.isAndroid && |
| | | [4, 6].includes(deps.tripStatus) && |
| | | [TripStatus.VehicleApproaching, TripStatus.InService].includes(deps.tripStatus) && |
| | | deps.statusRule.showCar && |
| | | !!carMarker && |
| | | !tripStatus6MarkerRefreshedRef.current |
| | |
| | | ? deps.endSite |
| | | : Taro.getStorageSync("endSite"); |
| | | const useAndroidNativeTrip1Callout = |
| | | deps.isAndroid && [1, 5, 8, 9, 10].includes(deps.tripStatus); |
| | | deps.isAndroid && [TripStatus.PublishRoute, TripStatus.VehicleArrivedPickup, TripStatus.TripFinished, TripStatus.PaymentSuccess, TripStatus.CancelOnlyPickup].includes(deps.tripStatus); |
| | | const pickupMarker = createPickupMarker( |
| | | pickupSite, |
| | | useAndroidNativeTrip1Callout |
| | |
| | | currentCarMarker.rotate !== carMarker.rotate |
| | | ); |
| | | const shouldHoldMarkerByTranslate = |
| | | [4, 6].includes(deps.tripStatus) && |
| | | [TripStatus.VehicleApproaching, TripStatus.InService].includes(deps.tripStatus) && |
| | | carMarkerInitialized.current && |
| | | deps.statusRule.showCar && |
| | | (!deps.statusRule.showDropoff || hasDropoffMarker); |
| | | const shouldHoldMarkerWhenArrived = |
| | | deps.tripStatus === 7 && |
| | | deps.tripStatus === TripStatus.ArrivedDestination && |
| | | carMarkerInitialized.current && |
| | | deps.statusRule.showCar && |
| | | (!deps.statusRule.showDropoff || hasDropoffMarker); |
| | |
| | | import Taro from "@tarojs/taro"; |
| | | import { useCallback, useEffect, useRef, useState } from "react"; |
| | | import { computeAdaptiveBottomPadding } from "../../../../utils/Robotaxi-public-method"; |
| | | import { TripStatus } from "../../begin-route/constants/trip-status"; |
| | | |
| | | type Deps = { |
| | | tripStatus: number; |
| | | tripStatus: TripStatus; |
| | | statusRule: any; |
| | | routePoints: any[]; |
| | | walkingPoints: any[]; |
| | |
| | | }; |
| | | |
| | | const defaultDeps: Deps = { |
| | | tripStatus: 1, |
| | | tripStatus: TripStatus.PublishRoute, |
| | | statusRule: { viewport: "pickup" }, |
| | | routePoints: [], |
| | | walkingPoints: [], |
| | |
| | | ) => { |
| | | const points: any[] = []; |
| | | const keyParts: any[] = [ |
| | | viewportType === "order_detail" ? 8 : deps.tripStatus, |
| | | viewportType === "order_detail" ? TripStatus.TripFinished : deps.tripStatus, |
| | | viewportType |
| | | ]; |
| | | |
| | |
| | | ); |
| | | if (points.length === 0) return; |
| | | if ( |
| | | deps.tripStatus === 6 && |
| | | deps.tripStatus === TripStatus.InService && |
| | | viewportType === "route" && |
| | | tripStatus6ViewportLockedRef.current |
| | | ) { |
| | |
| | | if (nextKey === viewportKeyRef.current) return; |
| | | viewportKeyRef.current = nextKey; |
| | | updateViewportByPayload(viewportType, points); |
| | | if (deps.tripStatus === 6 && viewportType === "route") { |
| | | if (deps.tripStatus === TripStatus.InService && viewportType === "route") { |
| | | tripStatus6ViewportLockedRef.current = true; |
| | | } |
| | | }, [ |
| | |
| | | import { useEffect, useRef, useState } from "react"; |
| | | import { TripStatus } from "../../begin-route/constants/trip-status"; |
| | | |
| | | type Deps = { |
| | | isAndroid: boolean; |
| | | tripStatus: number; |
| | | tripStatus: TripStatus; |
| | | statusRule: any; |
| | | shouldShowRoute: boolean; |
| | | }; |
| | | |
| | | const defaultDeps: Deps = { |
| | | isAndroid: false, |
| | | tripStatus: 1, |
| | | tripStatus: TripStatus.PublishRoute, |
| | | statusRule: { showWalking: false }, |
| | | shouldShowRoute: false |
| | | }; |
| | |
| | | }; |
| | | |
| | | const getDisplayRoutePoints = () => { |
| | | const isTranslateMode = [4, 6].includes(deps.tripStatus); |
| | | const isTranslateMode = [TripStatus.VehicleApproaching, TripStatus.InService].includes(deps.tripStatus); |
| | | return isTranslateMode |
| | | ? routePoints |
| | | : reducePoints(routePoints, deps.isAndroid ? 120 : 200); |
| | |
| | | return; |
| | | } |
| | | |
| | | if (![8, 9].includes(deps.tripStatus)) { |
| | | if (![TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(deps.tripStatus)) { |
| | | setPolyline([]); |
| | | } |
| | | }, [ |
| New file |
| | |
| | | import Taro from '@tarojs/taro'; |
| | | import { useEffect, useRef } from 'react'; |
| | | import { mapOrderStatusToTripStatus } from '../../../../utils/Robotaxi-public-method'; |
| | | import { shareQueryOrderInfo, shareQueryOrderRoute } from '../../../api/begin-route'; |
| | | import { normalizeDistanceKm, normalizeDurationSeconds } from '../../../utils/format'; |
| | | import { TripStatus } from '../../begin-route/constants/trip-status'; |
| | | |
| | | type LocationPoint = { latitude: number; longitude: number }; |
| | | |
| | | type RouteDistance = { value: number; unit: 'km' | 'm' } | null; |
| | | |
| | | type Deps = { |
| | | orderId: string; |
| | | tripStatus: TripStatus; |
| | | startSite: any; |
| | | endSite: any; |
| | | setTripStatus: (value: TripStatus) => void; |
| | | setTripData: (value: any) => void; |
| | | setStartSite: (value: any) => void; |
| | | setEndSite: (value: any) => void; |
| | | setIsExpired: (value: boolean) => void; |
| | | setShowReminder: (value: boolean) => void; |
| | | setLongitude: (value: number) => void; |
| | | setLatitude: (value: number) => void; |
| | | setRouteDistance: (value: RouteDistance) => void; |
| | | setRouteDuration: (value: number | null) => void; |
| | | applyRoutePoints: (value: any[]) => void; |
| | | updateVehicleLocationThrottled: (lat: number, lng: number, force?: boolean) => void; |
| | | setVehicleDirection: (value: number) => void; |
| | | }; |
| | | |
| | | /** |
| | | * 分享页运行时编排:负责订单拉取、状态同步、轮询与地图数据联动。 |
| | | */ |
| | | export default function useShareTripRuntime(deps: Deps) { |
| | | const tripStatusRef = useRef<TripStatus>(deps.tripStatus); // 行程状态引用 |
| | | const startSiteRef = useRef<any>(deps.startSite); // 上车点引用 |
| | | const endSiteRef = useRef<any>(deps.endSite); // 下车点引用 |
| | | const infoStatusRef = useRef<TripStatus | null>(null); // 最新详情状态引用 |
| | | const errorToastShownRef = useRef(false); // 错误提示节流标记 |
| | | const applyRoutePointsRef = useRef(deps.applyRoutePoints); // 路线落图函数引用 |
| | | const updateVehicleLocationRef = useRef(deps.updateVehicleLocationThrottled); // 车辆位置更新函数引用 |
| | | const setVehicleDirectionRef = useRef(deps.setVehicleDirection); // 车辆朝向更新函数引用 |
| | | |
| | | /** |
| | | * 计算两点方向角(0-360)。 |
| | | */ |
| | | const getDirectionByPoints = (from: LocationPoint, to?: LocationPoint) => { |
| | | if (!from || !to) return 0; |
| | | const deltaLng = to.longitude - from.longitude; |
| | | const deltaLat = to.latitude - from.latitude; |
| | | const angle = Math.atan2(deltaLng, deltaLat) * (180 / Math.PI); |
| | | return ((angle % 360) + 360) % 360; |
| | | }; |
| | | |
| | | /** |
| | | * 朝向值归一化到 0-360,非法值返回 null。 |
| | | */ |
| | | const normalizeDirection = (value: any) => { |
| | | const direction = Number(value); |
| | | if (!Number.isFinite(direction)) return null; |
| | | return ((direction % 360) + 360) % 360; |
| | | }; |
| | | |
| | | /** |
| | | * 安全转换数值,非法值返回 null。 |
| | | */ |
| | | const toFiniteNumber = (value: any) => { |
| | | const num = Number(value); |
| | | return Number.isFinite(num) ? num : null; |
| | | }; |
| | | |
| | | /** |
| | | * 归一化坐标点结构,兼容字符串和对象格式。 |
| | | */ |
| | | const normalizePoint = (point: any) => { |
| | | if (!point) return null; |
| | | if (typeof point === 'string') { |
| | | const [longitudeValue, latitudeValue] = point.split(','); |
| | | const longitudeNum = toFiniteNumber(longitudeValue); |
| | | const latitudeNum = toFiniteNumber(latitudeValue); |
| | | if (longitudeNum === null || latitudeNum === null) return null; |
| | | return { longitude: longitudeNum, latitude: latitudeNum }; |
| | | } |
| | | const longitudeNum = toFiniteNumber(point.longitude ?? point.lon ?? point.lng); |
| | | const latitudeNum = toFiniteNumber(point.latitude ?? point.lat); |
| | | if (longitudeNum === null || latitudeNum === null) return null; |
| | | return { longitude: longitudeNum, latitude: latitudeNum }; |
| | | }; |
| | | |
| | | /** |
| | | * 获取对象中首个有效字段值。 |
| | | */ |
| | | const getFieldValue = (source: any, keys: string[]) => { |
| | | for (const key of keys) { |
| | | const value = source?.[key]; |
| | | if (value !== undefined && value !== null && value !== '') { |
| | | return value; |
| | | } |
| | | } |
| | | return null; |
| | | }; |
| | | |
| | | /** |
| | | * 构建上/下车点结构。 |
| | | */ |
| | | const buildSite = (source: any, type: 'start' | 'end') => { |
| | | const isStart = type === 'start'; |
| | | const latitudeValue = getFieldValue( |
| | | source, |
| | | isStart |
| | | ? ['startLat', 'startLatitude', 'upLat', 'originLat', 'originLatitude'] |
| | | : ['endLat', 'endLatitude', 'downLat', 'destinationLat', 'destinationLatitude'], |
| | | ); |
| | | const longitudeValue = getFieldValue( |
| | | source, |
| | | isStart |
| | | ? ['startLng', 'startLon', 'startLongitude', 'upLng', 'upLon', 'originLng', 'originLon', 'originLongitude'] |
| | | : ['endLng', 'endLon', 'endLongitude', 'downLng', 'downLon', 'destinationLng', 'destinationLon', 'destinationLongitude'], |
| | | ); |
| | | const latitudeNum = toFiniteNumber(latitudeValue); |
| | | const longitudeNum = toFiniteNumber(longitudeValue); |
| | | if (latitudeNum === null || longitudeNum === null) return null; |
| | | return { |
| | | latitude: latitudeNum, |
| | | longitude: longitudeNum, |
| | | name: getFieldValue( |
| | | source, |
| | | isStart ? ['startName', 'startSiteName', 'upName', 'originName'] : ['endName', 'endSiteName', 'downName', 'destinationName'], |
| | | ) || (isStart ? '上车点' : '终点'), |
| | | address: getFieldValue( |
| | | source, |
| | | isStart ? ['startAddress', 'startSiteAddress', 'upAddress', 'originAddress'] : ['endAddress', 'endSiteAddress', 'downAddress', 'destinationAddress'], |
| | | ) || '', |
| | | }; |
| | | }; |
| | | |
| | | /** |
| | | * 格式化时间展示文案。 |
| | | */ |
| | | const formatTimeText = (value: any) => { |
| | | if (!value) return null; |
| | | const text = String(value); |
| | | if (/^\d{2}:\d{2}/.test(text)) return text.slice(0, 5); |
| | | const date = new Date(text); |
| | | if (Number.isNaN(date.getTime())) return text; |
| | | const hours = `${date.getHours()}`.padStart(2, '0'); |
| | | const minutes = `${date.getMinutes()}`.padStart(2, '0'); |
| | | return `${hours}:${minutes}`; |
| | | }; |
| | | |
| | | /** |
| | | * 解析时间为毫秒时间戳。 |
| | | */ |
| | | const parseTimeToTimestamp = (value: any) => { |
| | | if (value === undefined || value === null || value === '') return null; |
| | | if (typeof value === 'number') { |
| | | if (!Number.isFinite(value) || value <= 0) return null; |
| | | return value < 1e12 ? value * 1000 : value; |
| | | } |
| | | const normalizedValue = String(value).trim(); |
| | | if (!normalizedValue) return null; |
| | | if (/^\d+$/.test(normalizedValue)) { |
| | | const numericValue = Number(normalizedValue); |
| | | if (!Number.isFinite(numericValue) || numericValue <= 0) return null; |
| | | return numericValue < 1e12 ? numericValue * 1000 : numericValue; |
| | | } |
| | | const parsedTime = new Date(normalizedValue.replace(/-/g, '/')).getTime(); |
| | | return Number.isFinite(parsedTime) && parsedTime > 0 ? parsedTime : null; |
| | | }; |
| | | |
| | | /** |
| | | * 根据详情与状态判断分享页是否过期。 |
| | | */ |
| | | const resolveExpiredState = (info: any, mappedStatus: TripStatus) => { |
| | | if (info?.expired === true || info?.isExpired === true) return true; |
| | | if (mappedStatus < TripStatus.ArrivedDestination) return false; |
| | | const getoffTimestamp = parseTimeToTimestamp(info?.getoffTime); |
| | | if (typeof getoffTimestamp !== 'number') return false; |
| | | return Date.now() - getoffTimestamp > 24 * 60 * 60 * 1000; |
| | | }; |
| | | |
| | | /** |
| | | * 获取点集合中心点。 |
| | | */ |
| | | const getCenterPoint = (points: Array<{ longitude: number; latitude: number }>) => { |
| | | if (!points.length) return null; |
| | | const init = { |
| | | minLng: points[0].longitude, |
| | | maxLng: points[0].longitude, |
| | | minLat: points[0].latitude, |
| | | maxLat: points[0].latitude, |
| | | }; |
| | | const bounds = points.reduce( |
| | | (acc, point) => ({ |
| | | minLng: Math.min(acc.minLng, point.longitude), |
| | | maxLng: Math.max(acc.maxLng, point.longitude), |
| | | minLat: Math.min(acc.minLat, point.latitude), |
| | | maxLat: Math.max(acc.maxLat, point.latitude), |
| | | }), |
| | | init, |
| | | ); |
| | | return { |
| | | longitude: (bounds.minLng + bounds.maxLng) / 2, |
| | | latitude: (bounds.minLat + bounds.maxLat) / 2, |
| | | }; |
| | | }; |
| | | |
| | | /** |
| | | * 提取路线点。 |
| | | */ |
| | | const extractRoutePoints = (routeResponse: any) => { |
| | | const candidates = [ |
| | | routeResponse?.points, |
| | | routeResponse?.route?.polyline, |
| | | routeResponse?.polyline, |
| | | routeResponse?.content?.points, |
| | | ]; |
| | | for (const candidate of candidates) { |
| | | if (Array.isArray(candidate) && candidate.length > 0) { |
| | | return candidate.map(normalizePoint).filter(Boolean) as Array<{ longitude: number; latitude: number }>; |
| | | } |
| | | } |
| | | return []; |
| | | }; |
| | | |
| | | /** |
| | | * 提取已服务轨迹点。 |
| | | */ |
| | | const extractServedPoints = (routeResponse: any) => { |
| | | if (!Array.isArray(routeResponse?.servedPoints)) return []; |
| | | return routeResponse.servedPoints.map(normalizePoint).filter(Boolean) as Array<{ longitude: number; latitude: number }>; |
| | | }; |
| | | |
| | | /** |
| | | * 解析车辆应展示位置。 |
| | | */ |
| | | const resolveVehiclePoint = (points: Array<{ longitude: number; latitude: number }>, servedPoints: Array<{ longitude: number; latitude: number }> = []) => { |
| | | const servedLastPoint = Array.isArray(servedPoints) && servedPoints.length > 0 ? servedPoints[servedPoints.length - 1] : null; |
| | | if (servedLastPoint) return servedLastPoint; |
| | | return points[0] || null; |
| | | }; |
| | | |
| | | /** |
| | | * 将分享路线数据同步到地图与车辆状态。 |
| | | */ |
| | | const syncShareRoute = (routeResponse: any, mappedStatus: TripStatus, nextStartSite: any, nextEndSite: any) => { |
| | | const extractedRoutePoints = extractRoutePoints(routeResponse); |
| | | const servedPoints = extractServedPoints(routeResponse); |
| | | const routeMeta = routeResponse?.content?.route || routeResponse?.route || routeResponse || {}; |
| | | const distance = normalizeDistanceKm(Number(routeMeta?.distance)); |
| | | const duration = normalizeDurationSeconds(Number(routeMeta?.duration)); |
| | | deps.setRouteDistance(distance); |
| | | deps.setRouteDuration(duration); |
| | | applyRoutePointsRef.current(extractedRoutePoints); |
| | | |
| | | const direction = normalizeDirection( |
| | | routeResponse?.direction ?? |
| | | routeResponse?.content?.direction ?? |
| | | routeMeta?.direction ?? |
| | | routeResponse?.vehicleDirection, |
| | | ); |
| | | const vehiclePoint = resolveVehiclePoint(extractedRoutePoints, servedPoints); |
| | | if (vehiclePoint && [TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup, TripStatus.InService].includes(mappedStatus)) { |
| | | updateVehicleLocationRef.current(vehiclePoint.latitude, vehiclePoint.longitude, true); |
| | | if (typeof direction === 'number') { |
| | | setVehicleDirectionRef.current(direction); |
| | | } else { |
| | | setVehicleDirectionRef.current(getDirectionByPoints(vehiclePoint, extractedRoutePoints[1])); |
| | | } |
| | | } |
| | | |
| | | const center = getCenterPoint([ |
| | | ...extractedRoutePoints, |
| | | ...(nextStartSite ? [nextStartSite] : []), |
| | | ...(nextEndSite ? [nextEndSite] : []), |
| | | ]); |
| | | if (center) { |
| | | deps.setLongitude(center.longitude); |
| | | deps.setLatitude(center.latitude); |
| | | } |
| | | }; |
| | | |
| | | useEffect(() => { |
| | | tripStatusRef.current = deps.tripStatus; |
| | | }, [deps.tripStatus]); |
| | | |
| | | useEffect(() => { |
| | | startSiteRef.current = deps.startSite; |
| | | }, [deps.startSite]); |
| | | |
| | | useEffect(() => { |
| | | endSiteRef.current = deps.endSite; |
| | | }, [deps.endSite]); |
| | | |
| | | useEffect(() => { |
| | | applyRoutePointsRef.current = deps.applyRoutePoints; |
| | | updateVehicleLocationRef.current = deps.updateVehicleLocationThrottled; |
| | | setVehicleDirectionRef.current = deps.setVehicleDirection; |
| | | }, [deps.applyRoutePoints, deps.updateVehicleLocationThrottled, deps.setVehicleDirection]); |
| | | |
| | | useEffect(() => { |
| | | if (!deps.orderId) { |
| | | Taro.showToast({ title: '分享订单不存在', icon: 'none' }); |
| | | return; |
| | | } |
| | | |
| | | let cancelled = false; |
| | | let timer: ReturnType<typeof setInterval> | null = null; |
| | | |
| | | const showFetchError = (error: unknown) => { |
| | | console.error('获取分享行程失败', error); |
| | | if (!errorToastShownRef.current) { |
| | | errorToastShownRef.current = true; |
| | | Taro.showToast({ title: '获取分享行程失败', icon: 'none' }); |
| | | } |
| | | }; |
| | | |
| | | const stopPolling = () => { |
| | | if (timer) { |
| | | clearInterval(timer); |
| | | timer = null; |
| | | } |
| | | }; |
| | | |
| | | const applyShareInfo = (info: any) => { |
| | | const mappedStatus = mapOrderStatusToTripStatus(info?.status) as TripStatus; |
| | | const nextStartSite = buildSite(info, 'start'); |
| | | const nextEndSite = buildSite(info, 'end'); |
| | | const etaMinutes = Math.max( |
| | | 0, |
| | | Number(info?.etaMinutes ?? info?.estimatedWaitTime ?? info?.arrivalMinutes ?? info?.routeDurationMinutes ?? 0) || 0, |
| | | ); |
| | | const nextTripData = { |
| | | ...info, |
| | | tripStatus: mappedStatus, |
| | | etaMinutes, |
| | | plateNumber: info?.plateNumber || info?.vehiclePlateNumber || '', |
| | | vehicleColor: info?.vehicleColor || '', |
| | | vehicleCode: info?.vehicleCode || '', |
| | | vehiclePictures: info?.vehiclePictures || info?.vehicleImage || '', |
| | | startPointName: info?.startPointName || nextStartSite?.name || '上车点', |
| | | endPointName: info?.endPointName || nextEndSite?.name || '终点', |
| | | boardingTime: formatTimeText(info?.boardingTime || info?.pickupTime), |
| | | getoffTime: formatTimeText(info?.getoffTime || info?.finishTime || info?.orderFinishTime), |
| | | customerServicePhone: info?.customerServicePhone || info?.servicePhone || '400-000-0000', |
| | | }; |
| | | |
| | | infoStatusRef.current = mappedStatus; |
| | | tripStatusRef.current = mappedStatus; |
| | | startSiteRef.current = nextStartSite; |
| | | endSiteRef.current = nextEndSite || {}; |
| | | |
| | | deps.setTripStatus(mappedStatus); |
| | | deps.setTripData(nextTripData); |
| | | deps.setStartSite(nextStartSite); |
| | | deps.setEndSite(nextEndSite || {}); |
| | | deps.setIsExpired(resolveExpiredState(info, mappedStatus)); |
| | | deps.setShowReminder(Boolean(info?.showReminder)); |
| | | |
| | | return { |
| | | mappedStatus, |
| | | nextStartSite, |
| | | nextEndSite, |
| | | }; |
| | | }; |
| | | |
| | | const resolveRouteOrderStatus = (routeResponse: any): TripStatus | null => { |
| | | const statusValue = |
| | | getFieldValue(routeResponse?.content, ['tripStatus', 'orderStatus', 'status']) ?? |
| | | getFieldValue(routeResponse, ['tripStatus', 'orderStatus']); |
| | | const parsedStatus = Number(statusValue); |
| | | return Number.isFinite(parsedStatus) |
| | | ? (mapOrderStatusToTripStatus(parsedStatus) as TripStatus) |
| | | : null; |
| | | }; |
| | | |
| | | const fetchInitialShareInfo = async () => { |
| | | const info = await shareQueryOrderInfo({ orderId: deps.orderId }); |
| | | if (cancelled) return null; |
| | | return applyShareInfo(info || {}); |
| | | }; |
| | | |
| | | const fetchShareTripData = async () => { |
| | | try { |
| | | if (tripStatusRef.current > TripStatus.ArrivedDestination) { |
| | | stopPolling(); |
| | | return; |
| | | } |
| | | const routeResponse = await shareQueryOrderRoute(deps.orderId); |
| | | if (cancelled) return; |
| | | const routeStatus = resolveRouteOrderStatus(routeResponse); |
| | | const shouldFetchInfo = routeStatus !== null && routeStatus !== infoStatusRef.current; |
| | | |
| | | let mappedStatus = tripStatusRef.current; |
| | | let nextStartSite = startSiteRef.current; |
| | | let nextEndSite = endSiteRef.current; |
| | | |
| | | if (shouldFetchInfo) { |
| | | const info = await shareQueryOrderInfo({ orderId: deps.orderId }); |
| | | if (cancelled) return; |
| | | const infoState = applyShareInfo(info || {}); |
| | | mappedStatus = infoState.mappedStatus; |
| | | nextStartSite = infoState.nextStartSite; |
| | | nextEndSite = infoState.nextEndSite; |
| | | } |
| | | |
| | | if (mappedStatus > TripStatus.ArrivedDestination) { |
| | | stopPolling(); |
| | | return; |
| | | } |
| | | |
| | | syncShareRoute(routeResponse, mappedStatus, nextStartSite, nextEndSite); |
| | | errorToastShownRef.current = false; |
| | | } catch (error) { |
| | | showFetchError(error); |
| | | } |
| | | }; |
| | | |
| | | fetchInitialShareInfo() |
| | | .then((infoState) => { |
| | | if (!infoState || cancelled) return; |
| | | if (infoState.mappedStatus > TripStatus.ArrivedDestination) return; |
| | | fetchShareTripData(); |
| | | timer = setInterval(fetchShareTripData, 10000); |
| | | }) |
| | | .catch((error) => { |
| | | showFetchError(error); |
| | | }); |
| | | |
| | | return () => { |
| | | cancelled = true; |
| | | stopPolling(); |
| | | }; |
| | | }, [deps.orderId]); |
| | | } |
| | |
| | | import { CoverImage, CoverView, Image, View } from "@tarojs/components"; |
| | | import { formatDurationDisplay } from "../../../../utils/Robotaxi-public-method"; |
| | | import { TripStatus } from "../../begin-route/constants/trip-status"; |
| | | |
| | | type Deps = { |
| | | isAndroid: boolean; |
| | | tripStatus: number; |
| | | tripStatus: TripStatus; |
| | | mapScale: number; |
| | | markers: any[]; |
| | | shouldShowWaitingRadar?: boolean; |
| | |
| | | > |
| | | <> |
| | | {item.name !== "车辆位置" && |
| | | !(item.id === 2 && [7].includes(deps.tripStatus)) && ( |
| | | !(item.id === 2 && [TripStatus.ArrivedDestination].includes(deps.tripStatus)) && ( |
| | | <CoverView |
| | | className={`custom-callout-content ${ |
| | | deps.isAndroid && "txt-center " |
| | |
| | | {item.name} |
| | | </CoverView> |
| | | {deps.isAndroid && <CoverView style={{ width: "2px" }}></CoverView>} |
| | | {deps.tripStatus == 1 && ( |
| | | {deps.tripStatus === TripStatus.PublishRoute && ( |
| | | <CoverImage |
| | | className="custom-callout-icon" |
| | | src={deps.customCalloutIconSrc} |
| | |
| | | </CoverView> |
| | | )} |
| | | {item.name === "车辆位置" && |
| | | [4, 6].includes(deps.tripStatus) && |
| | | [TripStatus.VehicleApproaching, TripStatus.InService].includes(deps.tripStatus) && |
| | | !deps.isAndroid && ( |
| | | <CoverView |
| | | className="custom-callout-content" |
| | |
| | | </CoverView> |
| | | )} |
| | | {item.id === 2 && |
| | | [7].includes(deps.tripStatus) && |
| | | [TripStatus.ArrivedDestination].includes(deps.tripStatus) && |
| | | !deps.isAndroid && |
| | | deps.mapScale >= 20 && ( |
| | | <CoverView className="custom-callout-content"> |
| | |
| | | |
| | | const renderMarkerCallout = (item: any, index: number) => { |
| | | const calloutKey = `callout-${item.id}-${item.name || ""}`; |
| | | const isWaiting = [2, 3].includes(deps.tripStatus) && index === 0; |
| | | const isWaiting = [TripStatus.Publishing, TripStatus.CallingCar].includes(deps.tripStatus) && index === 0; |
| | | if (isWaiting) return renderWaitingCallout(item, calloutKey); |
| | | if (item.name === "车辆位置" && deps.isAndroid && [4, 6].includes(deps.tripStatus)) { |
| | | if (item.name === "车辆位置" && deps.isAndroid && [TripStatus.VehicleApproaching, TripStatus.InService].includes(deps.tripStatus)) { |
| | | return null; |
| | | } |
| | | return renderDefaultCallout(item, calloutKey); |
| | |
| | | |
| | | const renderMapOverlays = () => ( |
| | | <> |
| | | {!(deps.isAndroid && [1, 5, 8, 9, 10].includes(deps.tripStatus)) && |
| | | {!(deps.isAndroid && [TripStatus.PublishRoute, TripStatus.VehicleArrivedPickup, TripStatus.TripFinished, TripStatus.PaymentSuccess, TripStatus.CancelOnlyPickup].includes(deps.tripStatus)) && |
| | | deps.markers.map(renderMarkerCallout)} |
| | | {deps.shouldShowWaitingRadar && deps.waitingRadarVisible && ( |
| | | <Image |
| | |
| | | import Taro, { useRouter } from "@tarojs/taro"; |
| | | import { View, Image, Map, CoverView, CoverImage } from "@tarojs/components"; |
| | | import { useState, useEffect, useMemo, useRef } from "react"; |
| | | import { |
| | | formatDurationDisplay, |
| | | mapOrderStatusToTripStatus, |
| | | safeParse |
| | | } from "../../../utils/Robotaxi-public-method"; |
| | | import "./index.less"; |
| | | import Taro, { useRouter } from '@tarojs/taro'; |
| | | import { useEffect, useMemo, useState } from 'react'; |
| | | import './index.less'; |
| | | import useMapMarkers from './hooks/use-map-markers'; |
| | | import useVehicleTracking from './hooks/use-vehicle-tracking'; |
| | | import useRouteManager from './hooks/use-route-manager'; |
| | | import useMapViewport from './hooks/use-map-viewport'; |
| | | import useViewDelegates from './hooks/use-view-delegates'; |
| | | import useShareTripRuntime from './hooks/use-share-trip-runtime'; |
| | | import ShareTripView from './components/share-trip-view'; |
| | | import { TripStatus } from '../begin-route/constants/trip-status'; |
| | | |
| | | import homeIcon from "../../assets/back-home.png"; // 占位,建议换成首页小房子图标 |
| | | import shareContactIcon from "../../assets/share-contact.png"; // 客服占位图 |
| | | import contactServiceIcon from "../../assets/contact-service.png"; // 客服占位图 |
| | | import shareLine from "../../assets/share-line.png"; // 分享线占位图 |
| | | |
| | | // 分享页本地 hooks,避免依赖 begin-route 页面实现 |
| | | import useMapMarkers from "./hooks/use-map-markers"; |
| | | import useVehicleTracking from "./hooks/use-vehicle-tracking"; |
| | | import useRouteManager from "./hooks/use-route-manager"; |
| | | import useMapViewport from "./hooks/use-map-viewport"; |
| | | import useViewDelegates from "./hooks/use-view-delegates"; |
| | | import { |
| | | shareQueryOrderInfo, |
| | | shareQueryOrderRoute |
| | | } from "../../api/begin-route"; |
| | | import { |
| | | normalizeDistanceKm, |
| | | normalizeDurationSeconds |
| | | } from "../../utils/format"; |
| | | type StatusRule = { |
| | | showPickup: boolean; |
| | | showDropoff: boolean; |
| | | showCar: boolean; |
| | | showRoute: boolean; |
| | | showWalking: boolean; |
| | | viewport: string; |
| | | translateCar: boolean; |
| | | }; |
| | | |
| | | export default function ShareTrip() { |
| | | const router = useRouter(); |
| | | const { orderId = "" } = router.params || {}; |
| | | const [tripData, setTripData] = useState<any>(null); |
| | | const [longitude, setLongitude] = useState(113.32452); |
| | | const [latitude, setLatitude] = useState(23.10229); |
| | | const { orderId = '' } = router.params || {}; // 分享订单 ID |
| | | const [tripData, setTripData] = useState<any>(null); // 行程详情数据 |
| | | const [longitude, setLongitude] = useState(113.32452); // 地图中心经度 |
| | | const [latitude, setLatitude] = useState(23.10229); // 地图中心纬度 |
| | | const [startSite, setStartSite] = useState<any>(null); // 上车点 |
| | | const [endSite, setEndSite] = useState<any>({}); // 下车点 |
| | | const [isExpired, setIsExpired] = useState(false); // 是否已过期(大于24小时) |
| | | const [showReminder, setShowReminder] = useState(false); |
| | | const [tripStatus, setTripStatus] = useState<number>(1); |
| | | const tripStatusRef = useRef(tripStatus); |
| | | const startSiteRef = useRef<any>(null); |
| | | const endSiteRef = useRef<any>({}); |
| | | const infoStatusRef = useRef<number | null>(null); |
| | | const errorToastShownRef = useRef(false); |
| | | // 行程状态显示规则配置:showPickup-显示上车点,showDropoff-显示下车点,showCar-显示车辆,showRoute-显示路线,showWalking-显示步行路线,viewport-视口类型,translateCar-车辆是否平移 |
| | | const STATUS_RULES: Record<number, any> = { |
| | | 1: { |
| | | showPickup: true, |
| | | showDropoff: true, |
| | | showCar: false, |
| | | showRoute: true, |
| | | showWalking: true, |
| | | viewport: "route_walking", |
| | | translateCar: false |
| | | }, |
| | | 2: { |
| | | showPickup: true, |
| | | showDropoff: false, |
| | | showCar: false, |
| | | showRoute: false, |
| | | showWalking: false, |
| | | viewport: "pickup", |
| | | translateCar: false |
| | | }, |
| | | 3: { |
| | | showPickup: true, |
| | | showDropoff: false, |
| | | showCar: false, |
| | | showRoute: false, |
| | | showWalking: false, |
| | | viewport: "pickup", |
| | | translateCar: false |
| | | }, |
| | | 4: { |
| | | showPickup: true, |
| | | showDropoff: false, |
| | | showCar: true, |
| | | showRoute: true, |
| | | showWalking: false, |
| | | viewport: "route", |
| | | translateCar: true |
| | | }, |
| | | 5: { |
| | | showPickup: true, |
| | | showDropoff: false, |
| | | showCar: true, |
| | | showRoute: false, |
| | | showWalking: false, |
| | | viewport: "car_user", |
| | | translateCar: true |
| | | }, |
| | | 6: { |
| | | showPickup: false, |
| | | showDropoff: true, |
| | | showCar: true, |
| | | showRoute: true, |
| | | showWalking: false, |
| | | viewport: "route", |
| | | translateCar: true |
| | | }, |
| | | 7: { |
| | | showPickup: false, |
| | | showDropoff: true, |
| | | showCar: false, |
| | | showRoute: false, |
| | | showWalking: false, |
| | | viewport: "dropoff", |
| | | translateCar: false |
| | | }, |
| | | 8: { |
| | | showPickup: true, |
| | | showDropoff: true, |
| | | showCar: false, |
| | | showRoute: true, |
| | | showWalking: false, |
| | | viewport: "order_detail", |
| | | translateCar: false |
| | | }, |
| | | 9: { |
| | | showPickup: true, |
| | | showDropoff: true, |
| | | showCar: false, |
| | | showRoute: true, |
| | | showWalking: false, |
| | | viewport: "order_detail", |
| | | translateCar: false |
| | | }, |
| | | 10: { |
| | | showPickup: true, |
| | | showDropoff: false, |
| | | showCar: false, |
| | | showRoute: false, |
| | | showWalking: false, |
| | | viewport: "pickup", |
| | | translateCar: false |
| | | } |
| | | }; |
| | | const [isExpired, setIsExpired] = useState(false); // 是否超 24h 过期 |
| | | const [showReminder, setShowReminder] = useState(false); // 是否展示提醒 |
| | | const [tripStatus, setTripStatus] = useState<TripStatus>(TripStatus.PublishRoute); // 当前行程状态 |
| | | |
| | | const STATUS_RULES: Record<TripStatus, StatusRule> = { |
| | | [TripStatus.PublishRoute]: { showPickup: true, showDropoff: true, showCar: false, showRoute: true, showWalking: true, viewport: 'route_walking', translateCar: false }, |
| | | [TripStatus.Publishing]: { showPickup: true, showDropoff: false, showCar: false, showRoute: false, showWalking: false, viewport: 'pickup', translateCar: false }, |
| | | [TripStatus.CallingCar]: { showPickup: true, showDropoff: false, showCar: false, showRoute: false, showWalking: false, viewport: 'pickup', translateCar: false }, |
| | | [TripStatus.VehicleApproaching]: { showPickup: true, showDropoff: false, showCar: true, showRoute: true, showWalking: false, viewport: 'route', translateCar: true }, |
| | | [TripStatus.VehicleArrivedPickup]: { showPickup: true, showDropoff: false, showCar: true, showRoute: false, showWalking: false, viewport: 'car_user', translateCar: true }, |
| | | [TripStatus.InService]: { showPickup: false, showDropoff: true, showCar: true, showRoute: true, showWalking: false, viewport: 'route', translateCar: true }, |
| | | [TripStatus.ArrivedDestination]: { showPickup: false, showDropoff: true, showCar: false, showRoute: false, showWalking: false, viewport: 'dropoff', translateCar: false }, |
| | | [TripStatus.TripFinished]: { showPickup: true, showDropoff: true, showCar: false, showRoute: true, showWalking: false, viewport: 'order_detail', translateCar: false }, |
| | | [TripStatus.PaymentSuccess]: { showPickup: true, showDropoff: true, showCar: false, showRoute: true, showWalking: false, viewport: 'order_detail', translateCar: false }, |
| | | [TripStatus.CancelOnlyPickup]: { showPickup: true, showDropoff: false, showCar: false, showRoute: false, showWalking: false, viewport: 'pickup', translateCar: false }, |
| | | }; // 行程状态显示规则 |
| | | |
| | | const statusRule = useMemo( |
| | | () => STATUS_RULES[tripStatus] || STATUS_RULES[1], |
| | | [tripStatus] |
| | | ); // 当前行程状态对应的显示规则 |
| | | const shouldShowRoute = statusRule.showRoute; |
| | | () => STATUS_RULES[tripStatus] || STATUS_RULES[TripStatus.PublishRoute], |
| | | [tripStatus], |
| | | ); // 当前状态渲染规则 |
| | | const shouldShowRoute = statusRule.showRoute; // 是否展示主路线 |
| | | |
| | | const isAndroid = useMemo(() => { |
| | | try { |
| | | const info = Taro.getSystemInfoSync(); |
| | | return ( |
| | | info?.platform === "android" || /android/i.test(info?.system || "") |
| | | ); |
| | | return info?.platform === 'android' || /android/i.test(info?.system || ''); |
| | | } catch (error) { |
| | | return false; |
| | | } |
| | | }, []); |
| | | const mapLongitude = longitude ?? 116.397128; |
| | | const mapLatitude = latitude ?? 39.916527; |
| | | const vehicleMarkerId = !isAndroid && tripStatus === 6 ? 30 : 3; |
| | | const vehicleState = useVehicleTracking({ |
| | | statusRule, |
| | | vehicleMarkerId |
| | | }); // 车辆追踪状态容器 |
| | | const { |
| | | vehicleLocation, |
| | | vehicleDirection, |
| | | setVehicleDirection, |
| | | updateVehicleLocationThrottled |
| | | } = vehicleState; // 车辆状态解构 |
| | | }, []); // 平台标记 |
| | | |
| | | const markerState = useMapMarkers({ |
| | | const mapLongitude = longitude ?? 116.397128; // 地图兜底经度 |
| | | const mapLatitude = latitude ?? 39.916527; // 地图兜底纬度 |
| | | const vehicleMarkerId = !isAndroid && tripStatus === TripStatus.InService ? 30 : 3; // 车辆 marker ID |
| | | |
| | | const { vehicleLocation, vehicleDirection, setVehicleDirection, updateVehicleLocationThrottled } = useVehicleTracking({ |
| | | statusRule, |
| | | vehicleMarkerId, |
| | | }); // 车辆追踪状态容器 |
| | | |
| | | const { markers } = useMapMarkers({ |
| | | tripStatus, |
| | | statusRule, |
| | | isAndroid, |
| | |
| | | endSite, |
| | | vehicleLocation, |
| | | vehicleDirection, |
| | | vehicleMarkerId |
| | | }); // 地图标记状态容器 |
| | | const { markers } = markerState; // 标记状态解构 |
| | | vehicleMarkerId, |
| | | }); // 地图 marker 状态容器 |
| | | |
| | | const routeState = useRouteManager({ |
| | | isAndroid, |
| | | tripStatus, |
| | | statusRule, |
| | | shouldShowRoute |
| | | }); |
| | | const { |
| | | polyline, |
| | | applyRoutePoints, |
| | |
| | | routeDistance, |
| | | routeDuration, |
| | | setRouteDistance, |
| | | setRouteDuration |
| | | } = routeState; |
| | | const applyRoutePointsRef = useRef(applyRoutePoints); |
| | | const updateVehicleLocationRef = useRef(updateVehicleLocationThrottled); |
| | | const setVehicleDirectionRef = useRef(setVehicleDirection); |
| | | setRouteDuration, |
| | | } = useRouteManager({ |
| | | isAndroid, |
| | | tripStatus, |
| | | statusRule, |
| | | shouldShowRoute, |
| | | }); // 路线状态容器 |
| | | |
| | | const getDirectionByPoints = ( |
| | | from: { latitude: number; longitude: number }, |
| | | to?: { latitude: number; longitude: number } |
| | | ) => { |
| | | if (!from || !to) return 0; |
| | | const deltaLng = to.longitude - from.longitude; |
| | | const deltaLat = to.latitude - from.latitude; |
| | | const angle = Math.atan2(deltaLng, deltaLat) * (180 / Math.PI); |
| | | return ((angle % 360) + 360) % 360; |
| | | }; |
| | | |
| | | const normalizeDirection = (value: any) => { |
| | | const direction = Number(value); |
| | | if (!Number.isFinite(direction)) { |
| | | return null; |
| | | } |
| | | return ((direction % 360) + 360) % 360; |
| | | }; |
| | | |
| | | const toFiniteNumber = (value: any) => { |
| | | const num = Number(value); |
| | | return Number.isFinite(num) ? num : null; |
| | | }; |
| | | |
| | | const normalizePoint = (point: any) => { |
| | | if (!point) { |
| | | return null; |
| | | } |
| | | |
| | | if (typeof point === "string") { |
| | | const [longitudeValue, latitudeValue] = point.split(","); |
| | | const longitudeNum = toFiniteNumber(longitudeValue); |
| | | const latitudeNum = toFiniteNumber(latitudeValue); |
| | | if (longitudeNum === null || latitudeNum === null) { |
| | | return null; |
| | | } |
| | | return { longitude: longitudeNum, latitude: latitudeNum }; |
| | | } |
| | | |
| | | const longitudeNum = toFiniteNumber( |
| | | point.longitude ?? point.lon ?? point.lng |
| | | ); |
| | | const latitudeNum = toFiniteNumber(point.latitude ?? point.lat); |
| | | if (longitudeNum === null || latitudeNum === null) { |
| | | return null; |
| | | } |
| | | return { longitude: longitudeNum, latitude: latitudeNum }; |
| | | }; |
| | | |
| | | const getFieldValue = (source: any, keys: string[]) => { |
| | | for (const key of keys) { |
| | | const value = source?.[key]; |
| | | if (value !== undefined && value !== null && value !== "") { |
| | | return value; |
| | | } |
| | | } |
| | | return null; |
| | | }; |
| | | |
| | | const buildSite = (source: any, type: "start" | "end") => { |
| | | const isStart = type === "start"; |
| | | const latitudeValue = getFieldValue( |
| | | source, |
| | | isStart |
| | | ? ["startLat", "startLatitude", "upLat", "originLat", "originLatitude"] |
| | | : [ |
| | | "endLat", |
| | | "endLatitude", |
| | | "downLat", |
| | | "destinationLat", |
| | | "destinationLatitude" |
| | | ] |
| | | ); |
| | | const longitudeValue = getFieldValue( |
| | | source, |
| | | isStart |
| | | ? [ |
| | | "startLng", |
| | | "startLon", |
| | | "startLongitude", |
| | | "upLng", |
| | | "upLon", |
| | | "originLng", |
| | | "originLon", |
| | | "originLongitude" |
| | | ] |
| | | : [ |
| | | "endLng", |
| | | "endLon", |
| | | "endLongitude", |
| | | "downLng", |
| | | "downLon", |
| | | "destinationLng", |
| | | "destinationLon", |
| | | "destinationLongitude" |
| | | ] |
| | | ); |
| | | const latitudeNum = toFiniteNumber(latitudeValue); |
| | | const longitudeNum = toFiniteNumber(longitudeValue); |
| | | if (latitudeNum === null || longitudeNum === null) { |
| | | return null; |
| | | } |
| | | |
| | | return { |
| | | latitude: latitudeNum, |
| | | longitude: longitudeNum, |
| | | name: |
| | | getFieldValue( |
| | | source, |
| | | isStart |
| | | ? ["startName", "startSiteName", "upName", "originName"] |
| | | : ["endName", "endSiteName", "downName", "destinationName"] |
| | | ) || (isStart ? "上车点" : "终点"), |
| | | address: |
| | | getFieldValue( |
| | | source, |
| | | isStart |
| | | ? ["startAddress", "startSiteAddress", "upAddress", "originAddress"] |
| | | : [ |
| | | "endAddress", |
| | | "endSiteAddress", |
| | | "downAddress", |
| | | "destinationAddress" |
| | | ] |
| | | ) || "" |
| | | }; |
| | | }; |
| | | |
| | | const formatTimeText = (value: any) => { |
| | | if (!value) { |
| | | return null; |
| | | } |
| | | const text = String(value); |
| | | if (/^\d{2}:\d{2}/.test(text)) { |
| | | return text.slice(0, 5); |
| | | } |
| | | const date = new Date(text); |
| | | if (Number.isNaN(date.getTime())) { |
| | | return text; |
| | | } |
| | | const hours = `${date.getHours()}`.padStart(2, "0"); |
| | | const minutes = `${date.getMinutes()}`.padStart(2, "0"); |
| | | return `${hours}:${minutes}`; |
| | | }; |
| | | |
| | | const parseTimeToTimestamp = (value: any) => { |
| | | if (value === undefined || value === null || value === "") { |
| | | return null; |
| | | } |
| | | |
| | | if (typeof value === "number") { |
| | | if (!Number.isFinite(value) || value <= 0) { |
| | | return null; |
| | | } |
| | | // 兼容秒级时间戳,统一转换成毫秒。 |
| | | return value < 1e12 ? value * 1000 : value; |
| | | } |
| | | |
| | | const normalizedValue = String(value).trim(); |
| | | if (!normalizedValue) { |
| | | return null; |
| | | } |
| | | |
| | | if (/^\d+$/.test(normalizedValue)) { |
| | | const numericValue = Number(normalizedValue); |
| | | if (!Number.isFinite(numericValue) || numericValue <= 0) { |
| | | return null; |
| | | } |
| | | return numericValue < 1e12 ? numericValue * 1000 : numericValue; |
| | | } |
| | | |
| | | // 兼容 "2026-04-17 10:00:00" 这类在部分环境下解析不稳定的时间格式。 |
| | | const parsedTime = new Date(normalizedValue.replace(/-/g, "/")).getTime(); |
| | | return Number.isFinite(parsedTime) && parsedTime > 0 ? parsedTime : null; |
| | | }; |
| | | |
| | | const resolveExpiredState = (info: any, mappedStatus: number) => { |
| | | if (info?.expired === true || info?.isExpired === true) { |
| | | return true; |
| | | } |
| | | |
| | | if (mappedStatus < 7) { |
| | | return false; |
| | | } |
| | | |
| | | const getoffTimestamp = parseTimeToTimestamp(info?.getoffTime); |
| | | if (typeof getoffTimestamp !== "number") { |
| | | return false; |
| | | } |
| | | |
| | | // 仅以下车时间作为分享页过期依据。 |
| | | return Date.now() - getoffTimestamp > 24 * 60 * 60 * 1000; |
| | | }; |
| | | |
| | | const getCenterPoint = ( |
| | | points: Array<{ longitude: number; latitude: number }> |
| | | ) => { |
| | | if (!points.length) { |
| | | return null; |
| | | } |
| | | const init = { |
| | | minLng: points[0].longitude, |
| | | maxLng: points[0].longitude, |
| | | minLat: points[0].latitude, |
| | | maxLat: points[0].latitude |
| | | }; |
| | | const bounds = points.reduce( |
| | | (acc, point) => ({ |
| | | minLng: Math.min(acc.minLng, point.longitude), |
| | | maxLng: Math.max(acc.maxLng, point.longitude), |
| | | minLat: Math.min(acc.minLat, point.latitude), |
| | | maxLat: Math.max(acc.maxLat, point.latitude) |
| | | }), |
| | | init |
| | | ); |
| | | return { |
| | | longitude: (bounds.minLng + bounds.maxLng) / 2, |
| | | latitude: (bounds.minLat + bounds.maxLat) / 2 |
| | | }; |
| | | }; |
| | | |
| | | const extractRoutePoints = (routeResponse: any) => { |
| | | const candidates = [ |
| | | routeResponse?.points, |
| | | routeResponse?.route?.polyline, |
| | | routeResponse?.polyline, |
| | | routeResponse?.content?.points |
| | | ]; |
| | | for (const candidate of candidates) { |
| | | if (Array.isArray(candidate) && candidate.length > 0) { |
| | | return candidate.map(normalizePoint).filter(Boolean) as Array<{ |
| | | longitude: number; |
| | | latitude: number; |
| | | }>; |
| | | } |
| | | } |
| | | return []; |
| | | }; |
| | | |
| | | const extractServedPoints = (routeResponse: any) => { |
| | | if (!Array.isArray(routeResponse?.servedPoints)) { |
| | | return []; |
| | | } |
| | | return routeResponse.servedPoints |
| | | .map(normalizePoint) |
| | | .filter(Boolean) as Array<{ longitude: number; latitude: number }>; |
| | | }; |
| | | |
| | | const resolveVehiclePoint = ( |
| | | points: Array<{ longitude: number; latitude: number }>, |
| | | servedPoints: Array<{ longitude: number; latitude: number }> = [] |
| | | ) => { |
| | | const servedLastPoint = |
| | | Array.isArray(servedPoints) && servedPoints.length > 0 |
| | | ? servedPoints[servedPoints.length - 1] |
| | | : null; |
| | | if (servedLastPoint) { |
| | | return servedLastPoint; |
| | | } |
| | | return points[0] || null; |
| | | }; |
| | | |
| | | const syncShareRoute = ( |
| | | routeResponse: any, |
| | | mappedStatus: number, |
| | | nextStartSite: any, |
| | | nextEndSite: any |
| | | ) => { |
| | | const extractedRoutePoints = extractRoutePoints(routeResponse); |
| | | const servedPoints = extractServedPoints(routeResponse); |
| | | const routeMeta = |
| | | routeResponse?.content?.route || |
| | | routeResponse?.route || |
| | | routeResponse || |
| | | {}; |
| | | const distance = normalizeDistanceKm(Number(routeMeta?.distance)); |
| | | const duration = normalizeDurationSeconds(Number(routeMeta?.duration)); |
| | | |
| | | setRouteDistance(distance); |
| | | setRouteDuration(duration); |
| | | applyRoutePointsRef.current(extractedRoutePoints); |
| | | |
| | | const direction = normalizeDirection( |
| | | routeResponse?.direction ?? |
| | | routeResponse?.content?.direction ?? |
| | | routeMeta?.direction ?? |
| | | routeResponse?.vehicleDirection |
| | | ); |
| | | const vehiclePoint = resolveVehiclePoint( |
| | | extractedRoutePoints, |
| | | servedPoints |
| | | ); |
| | | if (vehiclePoint && [4, 5, 6].includes(mappedStatus)) { |
| | | updateVehicleLocationRef.current( |
| | | vehiclePoint.latitude, |
| | | vehiclePoint.longitude, |
| | | true |
| | | ); |
| | | if (typeof direction === "number") { |
| | | setVehicleDirectionRef.current(direction); |
| | | } else { |
| | | setVehicleDirectionRef.current( |
| | | getDirectionByPoints(vehiclePoint, extractedRoutePoints[1]) |
| | | ); |
| | | } |
| | | } |
| | | |
| | | const center = getCenterPoint([ |
| | | ...extractedRoutePoints, |
| | | ...(nextStartSite ? [nextStartSite] : []), |
| | | ...(nextEndSite ? [nextEndSite] : []) |
| | | ]); |
| | | if (center) { |
| | | setLongitude(center.longitude); |
| | | setLatitude(center.latitude); |
| | | } |
| | | }; |
| | | |
| | | useEffect(() => { |
| | | tripStatusRef.current = tripStatus; |
| | | }, [tripStatus]); |
| | | |
| | | useEffect(() => { |
| | | startSiteRef.current = startSite; |
| | | }, [startSite]); |
| | | |
| | | useEffect(() => { |
| | | endSiteRef.current = endSite; |
| | | }, [endSite]); |
| | | |
| | | const viewportState = useMapViewport({ |
| | | const { mapScale, updateMapViewportPadding } = useMapViewport({ |
| | | tripStatus, |
| | | statusRule, |
| | | routePoints, |
| | |
| | | endSite, |
| | | vehicleLocation, |
| | | longitude, |
| | | latitude |
| | | }); |
| | | const { mapScale, updateMapViewportPadding } = viewportState; |
| | | latitude, |
| | | }); // 视口状态容器 |
| | | |
| | | useEffect(() => { |
| | | applyRoutePointsRef.current = applyRoutePoints; |
| | | updateVehicleLocationRef.current = updateVehicleLocationThrottled; |
| | | setVehicleDirectionRef.current = setVehicleDirection; |
| | | }, [applyRoutePoints, updateVehicleLocationThrottled, setVehicleDirection]); |
| | | useShareTripRuntime({ |
| | | orderId, |
| | | tripStatus, |
| | | startSite, |
| | | endSite, |
| | | setTripStatus, |
| | | setTripData, |
| | | setStartSite, |
| | | setEndSite, |
| | | setIsExpired, |
| | | setShowReminder, |
| | | setLongitude, |
| | | setLatitude, |
| | | setRouteDistance, |
| | | setRouteDuration, |
| | | applyRoutePoints, |
| | | updateVehicleLocationThrottled, |
| | | setVehicleDirection, |
| | | }); // 分享页业务流程编排 |
| | | |
| | | useEffect(() => { |
| | | updateMapViewportPadding(); |
| | | }, [tripStatus, updateMapViewportPadding]); |
| | | }, [tripStatus, updateMapViewportPadding]); // 状态变化后刷新地图留白 |
| | | |
| | | useEffect(() => { |
| | | const timer = setTimeout(() => { |
| | | updateMapViewportPadding(); |
| | | }, 50); |
| | | return () => clearTimeout(timer); |
| | | }, [ |
| | | tripStatus, |
| | | showReminder, |
| | | tripData?.startPointName, |
| | | tripData?.endPointName, |
| | | tripData?.plateNumber, |
| | | updateMapViewportPadding |
| | | ]); |
| | | }, [tripStatus, showReminder, tripData?.startPointName, tripData?.endPointName, tripData?.plateNumber, updateMapViewportPadding]); // UI 数据变化后二次修正留白 |
| | | |
| | | useEffect(() => { |
| | | if (!orderId) { |
| | | Taro.showToast({ title: "分享订单不存在", icon: "none" }); |
| | | return; |
| | | } |
| | | |
| | | let cancelled = false; |
| | | let timer: ReturnType<typeof setInterval> | null = null; |
| | | const showFetchError = (error: unknown) => { |
| | | console.error("获取分享行程失败", error); |
| | | if (!errorToastShownRef.current) { |
| | | errorToastShownRef.current = true; |
| | | Taro.showToast({ title: "获取分享行程失败", icon: "none" }); |
| | | } |
| | | }; |
| | | const stopPolling = () => { |
| | | if (timer) { |
| | | clearInterval(timer); |
| | | timer = null; |
| | | } |
| | | }; |
| | | |
| | | const applyShareInfo = (info: any) => { |
| | | const mappedStatus = mapOrderStatusToTripStatus(info?.status); |
| | | const nextStartSite = buildSite(info, "start"); |
| | | const nextEndSite = buildSite(info, "end"); |
| | | const etaMinutes = Math.max( |
| | | 0, |
| | | Number( |
| | | info?.etaMinutes ?? |
| | | info?.estimatedWaitTime ?? |
| | | info?.arrivalMinutes ?? |
| | | info?.routeDurationMinutes ?? |
| | | 0 |
| | | ) || 0 |
| | | ); |
| | | const nextTripData = { |
| | | ...info, |
| | | tripStatus: mappedStatus, |
| | | etaMinutes, |
| | | plateNumber: info?.plateNumber || info?.vehiclePlateNumber || "", |
| | | vehicleColor: info?.vehicleColor || "", |
| | | vehicleCode: info?.vehicleCode || "", |
| | | vehiclePictures: info?.vehiclePictures || info?.vehicleImage || "", |
| | | startPointName: info?.startPointName || nextStartSite?.name || "上车点", |
| | | endPointName: info?.endPointName || nextEndSite?.name || "终点", |
| | | boardingTime: formatTimeText(info?.boardingTime || info?.pickupTime), |
| | | getoffTime: formatTimeText( |
| | | info?.getoffTime || info?.finishTime || info?.orderFinishTime |
| | | ), |
| | | customerServicePhone: |
| | | info?.customerServicePhone || info?.servicePhone || "400-000-0000" |
| | | }; |
| | | |
| | | infoStatusRef.current = mappedStatus; |
| | | tripStatusRef.current = mappedStatus; |
| | | startSiteRef.current = nextStartSite; |
| | | endSiteRef.current = nextEndSite || {}; |
| | | |
| | | setTripStatus(mappedStatus); |
| | | setTripData(nextTripData); |
| | | setStartSite(nextStartSite); |
| | | setEndSite(nextEndSite || {}); |
| | | setIsExpired(resolveExpiredState(info, mappedStatus)); |
| | | setShowReminder(Boolean(info?.showReminder)); |
| | | |
| | | return { |
| | | mappedStatus, |
| | | nextStartSite, |
| | | nextEndSite |
| | | }; |
| | | }; |
| | | |
| | | const resolveRouteOrderStatus = (routeResponse: any) => { |
| | | const statusValue = |
| | | getFieldValue(routeResponse?.content, [ |
| | | "tripStatus", |
| | | "orderStatus", |
| | | "status" |
| | | ]) ?? getFieldValue(routeResponse, ["tripStatus", "orderStatus"]); |
| | | const parsedStatus = Number(statusValue); |
| | | return Number.isFinite(parsedStatus) |
| | | ? mapOrderStatusToTripStatus(parsedStatus) |
| | | : null; |
| | | }; |
| | | |
| | | const fetchInitialShareInfo = async () => { |
| | | const info = await shareQueryOrderInfo({ orderId }); |
| | | if (cancelled) { |
| | | return null; |
| | | } |
| | | return applyShareInfo(info || {}); |
| | | }; |
| | | |
| | | const fetchShareTripData = async () => { |
| | | try { |
| | | if (tripStatusRef.current > 7) { |
| | | stopPolling(); |
| | | return; |
| | | } |
| | | |
| | | const routeResponse = await shareQueryOrderRoute(orderId); |
| | | |
| | | if (cancelled) { |
| | | return; |
| | | } |
| | | |
| | | const routeStatus = resolveRouteOrderStatus(routeResponse); |
| | | const shouldFetchInfo = |
| | | routeStatus !== null && routeStatus !== infoStatusRef.current; |
| | | |
| | | let mappedStatus = tripStatusRef.current; |
| | | let nextStartSite = startSiteRef.current; |
| | | let nextEndSite = endSiteRef.current; |
| | | |
| | | if (shouldFetchInfo) { |
| | | const info = await shareQueryOrderInfo({ orderId }); |
| | | if (cancelled) { |
| | | return; |
| | | } |
| | | const infoState = applyShareInfo(info || {}); |
| | | mappedStatus = infoState.mappedStatus; |
| | | nextStartSite = infoState.nextStartSite; |
| | | nextEndSite = infoState.nextEndSite; |
| | | } |
| | | |
| | | if (mappedStatus > 7) { |
| | | stopPolling(); |
| | | return; |
| | | } |
| | | |
| | | syncShareRoute(routeResponse, mappedStatus, nextStartSite, nextEndSite); |
| | | |
| | | errorToastShownRef.current = false; |
| | | } catch (error) { |
| | | showFetchError(error); |
| | | } |
| | | }; |
| | | |
| | | fetchInitialShareInfo() |
| | | .then(infoState => { |
| | | if (!infoState || cancelled) { |
| | | return; |
| | | } |
| | | if (infoState.mappedStatus > 7) { |
| | | return; |
| | | } |
| | | fetchShareTripData(); |
| | | timer = setInterval(fetchShareTripData, 10000); |
| | | }) |
| | | .catch(error => { |
| | | showFetchError(error); |
| | | }); |
| | | |
| | | return () => { |
| | | cancelled = true; |
| | | stopPolling(); |
| | | }; |
| | | }, [orderId, setRouteDistance, setRouteDuration]); |
| | | |
| | | const goHome = () => { |
| | | Taro.reLaunch({ url: "/pages/robotaxi/home/index" }); |
| | | }; |
| | | const durationDisplay = formatDurationDisplay(routeDuration); |
| | | const { renderMapOverlays } = useViewDelegates({ |
| | | isAndroid, |
| | | tripStatus, |
| | |
| | | shouldShowWaitingRadar: false, |
| | | waitingRadarVisible: false, |
| | | waitingRadarStyle: {}, |
| | | waitingRadarSrc: "", |
| | | waitingRadarSrc: '', |
| | | carCalloutVisible: false, |
| | | carCalloutStyle: {}, |
| | | arriveCalloutVisible: false, |
| | | arriveCalloutStyle: {}, |
| | | routeDistance, |
| | | routeDuration, |
| | | customCalloutIconSrc: |
| | | "https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/maker-tooltip-right.png" |
| | | }); |
| | | customCalloutIconSrc: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/maker-tooltip-right.png', |
| | | }); // 地图浮层视图委托 |
| | | |
| | | const goHome = () => { |
| | | Taro.reLaunch({ url: '/pages/robotaxi/home/index' }); |
| | | }; // 返回首页 |
| | | |
| | | const handleContactService = () => { |
| | | const phoneNumber = Taro.getStorageSync("customerServicePhone"); |
| | | const phoneNumber = Taro.getStorageSync('customerServicePhone'); |
| | | if (phoneNumber) { |
| | | Taro.makePhoneCall({ phoneNumber }); |
| | | } else { |
| | | Taro.showToast({ title: "暂无客服电话", icon: "none" }); |
| | | Taro.showToast({ title: '暂无客服电话', icon: 'none' }); |
| | | } |
| | | }; |
| | | |
| | | if (isExpired) { |
| | | return ( |
| | | <View className="share-trip-expired"> |
| | | <View className="expired-content"> |
| | | <Image className="expired-img" src={shareContactIcon} /> |
| | | <View className="expired-text">行程结束已超过24小时</View> |
| | | <View className="expired-sub-text">查看行程信息,请联系客服</View> |
| | | <View className="expired-btn" onClick={handleContactService}> |
| | | 联系客服 |
| | | </View> |
| | | </View> |
| | | </View> |
| | | ); |
| | | } |
| | | }; // 联系客服动作 |
| | | |
| | | return ( |
| | | <View className="home-container"> |
| | | <Map |
| | | id="map" |
| | | longitude={mapLongitude} |
| | | latitude={mapLatitude} |
| | | scale={mapScale} |
| | | markers={markers} |
| | | polyline={polyline} |
| | | style={{ width: "100%", height: "87vh" }} |
| | | > |
| | | {renderMapOverlays()} |
| | | {/* 顶部返回/首页按钮 */} |
| | | <View onClick={goHome} className="select-city"> |
| | | <Image className="select-city-icon" src={homeIcon} /> |
| | | </View> |
| | | <View className="container"> |
| | | {/* 联系客服悬浮按钮 */} |
| | | <CoverView |
| | | className={`share-contact ${showReminder ? "has-tip" : ""}`} |
| | | onClick={handleContactService} |
| | | > |
| | | <CoverImage |
| | | className="share-contact-icon" |
| | | src={contactServiceIcon} |
| | | /> |
| | | </CoverView> |
| | | {/* 提醒 */} |
| | | {showReminder && ( |
| | | <View className="tip-box"> |
| | | <View className="tip-box-content"> |
| | | <View className="tip-box-text"> |
| | | 开启提醒后,亲友行程动态秒通知 |
| | | </View> |
| | | <View |
| | | className="tip-box-btn" |
| | | onClick={() => setShowReminder(false)} |
| | | > |
| | | 去开启 |
| | | </View> |
| | | </View> |
| | | </View> |
| | | )} |
| | | <View className="container-inner"> |
| | | <View catchMove onClick={e => e.stopPropagation()}> |
| | | <View className="calling-car"> |
| | | <Image |
| | | className="calling-car-bg" |
| | | src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/site-top-bg.png" |
| | | /> |
| | | <View className="calling-car-head"> |
| | | {tripData?.tripStatus === 4 && ( |
| | | <View className="calling-car-head-title"> |
| | | 车辆还有 |
| | | <View className="calling-car-head-title-eta"> |
| | | {durationDisplay.value} |
| | | </View> |
| | | {durationDisplay.unit}到达上车点 |
| | | </View> |
| | | )} |
| | | {tripData?.tripStatus === 5 && ( |
| | | <View className="calling-car-head-title"> |
| | | 车辆已到达上车点 |
| | | </View> |
| | | )} |
| | | {tripData?.tripStatus === 6 && ( |
| | | <View className="calling-car-head-title"> |
| | | 预计 |
| | | <View className="calling-car-head-title-eta"> |
| | | {durationDisplay.value} |
| | | </View> |
| | | {durationDisplay.unit}抵达目的地 |
| | | </View> |
| | | )} |
| | | {tripData?.tripStatus > 7 && ( |
| | | <View className="calling-car-head-title">已到达目的地</View> |
| | | )} |
| | | </View> |
| | | <View className="car-card"> |
| | | <View className="car-card-detail"> |
| | | <View> |
| | | <View className="car-card-detail-num"> |
| | | {tripData?.plateNumber || "--"} |
| | | </View> |
| | | <View className="car-card-detail-color"> |
| | | {tripData?.vehicleColor || "--"} |
| | | <View className="car-card-detail-color-line"></View> |
| | | 车身编码: {tripData?.vehicleCode || "--"} |
| | | </View> |
| | | </View> |
| | | {tripData?.vehiclePictures && ( |
| | | <Image |
| | | className="car-card-detail-img" |
| | | src={ |
| | | safeParse(tripData?.vehiclePictures, {})?.url || |
| | | "https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/vehicle-default.png" |
| | | } |
| | | /> |
| | | )} |
| | | </View> |
| | | <View className="car-card-line"></View> |
| | | <View className="car-card-temp"> |
| | | <View className="car-card-temp-list"> |
| | | <Image className="car-card-temp-img" src={shareLine} /> |
| | | {/* 起点 */} |
| | | <View className="car-card-temp-row"> |
| | | <View className="car-card-temp-row-left"> |
| | | <View className="dot start"></View> |
| | | <View |
| | | className={`car-card-temp-title ${ |
| | | tripData?.tripStatus === 6 || |
| | | tripData?.tripStatus === 7 |
| | | ? "disabled-text" |
| | | : "" |
| | | }`} |
| | | > |
| | | 起点:{tripData?.startPointName} |
| | | </View> |
| | | </View> |
| | | {(tripData?.tripStatus === 6 || |
| | | tripData?.tripStatus === 7) && |
| | | tripData?.boardingTime && ( |
| | | <View className="car-card-temp-time disabled-text"> |
| | | {tripData.boardingTime} 上车 |
| | | </View> |
| | | )} |
| | | </View> |
| | | |
| | | {/* 终点 */} |
| | | <View |
| | | className="car-card-temp-row" |
| | | style={{ marginTop: "12px" }} |
| | | > |
| | | <View className="car-card-temp-row-left"> |
| | | <View className="dot end"></View> |
| | | <View |
| | | className={`car-card-temp-title ${ |
| | | tripData?.tripStatus === 7 ? "disabled-text" : "" |
| | | }`} |
| | | > |
| | | 终点:{tripData?.endPointName} |
| | | </View> |
| | | </View> |
| | | {tripData?.tripStatus === 7 && tripData?.getoffTime && ( |
| | | <View className="car-card-temp-time disabled-text"> |
| | | {tripData.getoffTime} 到达 |
| | | </View> |
| | | )} |
| | | </View> |
| | | </View> |
| | | </View> |
| | | </View> |
| | | </View> |
| | | </View> |
| | | </View> |
| | | </View> |
| | | </Map> |
| | | </View> |
| | | <ShareTripView |
| | | isExpired={isExpired} |
| | | showReminder={showReminder} |
| | | setShowReminder={setShowReminder} |
| | | tripData={tripData} |
| | | mapLongitude={mapLongitude} |
| | | mapLatitude={mapLatitude} |
| | | mapScale={mapScale} |
| | | markers={markers} |
| | | polyline={polyline} |
| | | routeDuration={routeDuration} |
| | | renderMapOverlays={renderMapOverlays} |
| | | goHome={goHome} |
| | | handleContactService={handleContactService} |
| | | /> |
| | | ); |
| | | } |