feat(begin-route): 优化行程状态处理并添加协议校验
- 统一车辆方向解析逻辑,避免重复代码
- 优化车辆marker位置更新逻辑,避免状态6时方向卡顿
- 修改剩余时长显示规则,60秒以下显示秒并四舍五入,以上显示分钟并向下取整
- 车辆即将到达时(≤60秒)显示"即将到达"而非具体时长
- 添加用户协议校验,下单前检查是否同意隐私协议
- 调整取消弹窗逻辑,根据行程状态传递不同的等待时间
- 移除车门解锁后的安全须知弹窗自动触发
- 优化WebSocket心跳超时为3秒
- 修复车辆平移动画排队问题,避免动画堆积
| | |
| | | import PassengerNotice from './passenger-notice'; |
| | | import EvaluatePopup from './evaluate-popup'; |
| | | import TipPopup from '../../select-map-site/components/tip-popup'; |
| | | import PrivacyPopup from '../../../../pages/robotaxi/home/components/privacy-popup'; |
| | | |
| | | type Props = { |
| | | mapLongitude: number; |
| | |
| | | handleCloseCustomerService: () => void; |
| | | showCancelPopup: boolean; |
| | | cancelType: number; |
| | | cancelPopupWaitMinutes: number | null; |
| | | startSite: any; |
| | | endSite: any; |
| | | fareEstimate: any; |
| | |
| | | showTipPopup: boolean; |
| | | handleCloseTipPopup: () => void; |
| | | tipType: string; |
| | | showPrivacyPopup: boolean; |
| | | handleClosePrivacyPopup: () => void; |
| | | showPassengerNotice: boolean; |
| | | handleClosePassengerNotice: () => void; |
| | | changeCheckedEnter: (checked: boolean) => void; |
| | |
| | | handleCloseCustomerService, |
| | | showCancelPopup, |
| | | cancelType, |
| | | cancelPopupWaitMinutes, |
| | | startSite, |
| | | endSite, |
| | | fareEstimate, |
| | |
| | | showTipPopup, |
| | | handleCloseTipPopup, |
| | | tipType, |
| | | showPrivacyPopup, |
| | | handleClosePrivacyPopup, |
| | | showPassengerNotice, |
| | | handleClosePassengerNotice, |
| | | changeCheckedEnter, |
| | |
| | | cancelType={cancelType} |
| | | startSite={startSite} |
| | | endSite={endSite} |
| | | waitMinutes={fareEstimate?.waitingTime} |
| | | waitMinutes={cancelPopupWaitMinutes ?? fareEstimate?.waitingTime} |
| | | tripStatus={tripStatus} |
| | | routeDurationSeconds={routeDuration} |
| | | discountAmount={fareEstimate?.discountAmount} |
| | |
| | | onClose={handleCloseTipPopup} |
| | | tipType={tipType} |
| | | /> |
| | | <PrivacyPopup |
| | | show={showPrivacyPopup} |
| | | onClose={handleClosePrivacyPopup} |
| | | onAgree={handleClosePrivacyPopup} |
| | | /> |
| | | {!showCancelPopup && ( |
| | | <PassengerNotice |
| | | show={showPassengerNotice} |
| | |
| | | orderId?: string; |
| | | orderTrunkStatus?: boolean; |
| | | handleOpenOverRoutePopup?: () => void; |
| | | handleOpenAnnouncementsPopup?: () => void; |
| | | }; |
| | | |
| | | export default function DoorTrunkControl({ |
| | | tripStatus, |
| | | orderId, |
| | | orderTrunkStatus, |
| | | handleOpenOverRoutePopup, |
| | | handleOpenAnnouncementsPopup |
| | | handleOpenOverRoutePopup |
| | | }: Props) { |
| | | const [trunkStatus, setTrunkStatus] = useState(orderTrunkStatus); // 后备箱开关状态 |
| | | const [lockStatus, setLockStatus] = useState(false); // 车门解锁状态 |
| | | const trunkPollTimerRef = useRef<any>(null); // 后备箱状态轮询定时器 |
| | | const hasShownAnnouncementsRef = useRef(false); // 安全须知是否已展示 |
| | | |
| | | // 外部后备箱状态变化时同步本地状态 |
| | | useEffect(() => { |
| | |
| | | if (doorType === 'door') { |
| | | Taro.hideLoading(); |
| | | Taro.showToast({ title: '右侧车门已解锁', icon: 'none' }); |
| | | if (!hasShownAnnouncementsRef.current) { |
| | | handleOpenAnnouncementsPopup?.(); |
| | | hasShownAnnouncementsRef.current = true; |
| | | } |
| | | } |
| | | }).catch(() => { |
| | | Taro.hideLoading(); |
| | |
| | | finishCountdownEnabled: boolean; |
| | | finishCountdownSeconds: number | null; |
| | | limitMinutes: number | null; |
| | | handleOpenCancelPopup?: (durationDisplay: any) => void; |
| | | handleOpenCancelPopup?: (displayInfo: { durationDisplay: { value: string | number; unit: string }; waitMinutes: number | null }) => void; |
| | | }; |
| | | |
| | | export default function TripStatusHeader({ |
| | |
| | | // 标题行文案映射 |
| | | const titleMap = { |
| | | 3: <>预计<View className="time">{waitMinutes}</View>分钟内有车应答</>, |
| | | 4: <>车辆还有<View className="time">{durationDisplay.value}</View>{durationDisplay.unit}到达</>, |
| | | 4: isArriveSoon |
| | | ? <>即将到达</> |
| | | : <>车辆还有<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) |
| | |
| | | { |
| | | [3, 4, 5].includes(tripStatus) && |
| | | <View className="cancel-btn"> |
| | | <Image onClick={() => handleOpenCancelPopup?.(durationDisplay)} src={require('../../../../assets/cancel-btn.png')} className="cancel" /> |
| | | <Image onClick={() => handleOpenCancelPopup?.({ durationDisplay, waitMinutes })} src={require('../../../../assets/cancel-btn.png')} className="cancel" /> |
| | | </View> |
| | | } |
| | | </View> |
| | |
| | | import { View, Image } from "@tarojs/components"; |
| | | import './css-folder/calling-car.less' |
| | | import { formatDurationDisplay } from '../../../../utils/Robotaxi-public-method'; |
| | | import { formatRemainingDurationDisplay } from '../../../utils/format'; |
| | | |
| | | 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; |
| | | /** 打开取消弹窗,入参为预计到达展示文案 */ |
| | | handleOpenCancelPopup?: (durationDisplay: any) => void; |
| | | /** 打开取消弹窗,入参为顶部卡片当前展示信息 */ |
| | | handleOpenCancelPopup?: (displayInfo: { durationDisplay: { value: string | number; unit: string }; waitMinutes: number | null }) => void; |
| | | /** 上车点超时后的回调 */ |
| | | handleCancelTimeout?: () => void; |
| | | /** 到达终点后倒计时结束回调 */ |
| | |
| | | handlePay?: () => void; |
| | | /** 打开详细评价弹窗 */ |
| | | handleOpenEvaluatePopup?: () => void; |
| | | /** 首次开门后打开安全须知弹窗 */ |
| | | handleOpenAnnouncementsPopup?: () => void; |
| | | /** 预估等待分钟数 */ |
| | | estimatedWaitMinutes?: number; |
| | | /** 预估行程秒数 */ |
| | |
| | | onToggleDetailFade, |
| | | handlePay, |
| | | handleOpenEvaluatePopup, |
| | | handleOpenAnnouncementsPopup, |
| | | estimatedWaitMinutes, |
| | | routeDurationSeconds, |
| | | finishCountdownEnabled = false, |
| | |
| | | }); |
| | | |
| | | /** 预计到达时长展示(秒/分钟) */ |
| | | const durationDisplay = formatDurationDisplay(routeDurationSeconds); |
| | | const durationDisplay = formatRemainingDurationDisplay(routeDurationSeconds); |
| | | /** 车辆即将到达上车点(<=60秒) */ |
| | | const isArriveSoon = tripStatus === 4 && (typeof routeDurationSeconds === 'number' && routeDurationSeconds <= 60); |
| | | /** 上车倒计时即将结束(<60秒) */ |
| | |
| | | orderId={orderId} |
| | | orderTrunkStatus={orderTrunkStatus} |
| | | handleOpenOverRoutePopup={handleOpenOverRoutePopup} |
| | | handleOpenAnnouncementsPopup={handleOpenAnnouncementsPopup} |
| | | /> |
| | | |
| | | <FareDetailCard |
| | |
| | | orderId={orderId} |
| | | orderTrunkStatus={orderTrunkStatus} |
| | | handleOpenOverRoutePopup={handleOpenOverRoutePopup} |
| | | handleOpenAnnouncementsPopup={handleOpenAnnouncementsPopup} |
| | | /> |
| | | |
| | | <EvaluationCard |
| | |
| | | import { View, Text, Image } from '@tarojs/components'; |
| | | import { useEffect, useState } from 'react'; |
| | | import './css-folder/cancel-popup.less' |
| | | import { formatRemainingDurationDisplay } from '../../../utils/format'; |
| | | import { |
| | | clearOrderTripStorage, |
| | | formatDurationDisplay |
| | | clearOrderTripStorage |
| | | } from '../../../../utils/Robotaxi-public-method' |
| | | |
| | | |
| | |
| | | const startSiteName = cachedStartSite?.name || cachedStartSite?.address || '-'; |
| | | const endSiteName = cachedEndSite?.name || cachedEndSite?.address || '-'; |
| | | const waitMinutesValue = typeof waitMinutes === 'number' && !Number.isNaN(waitMinutes) ? waitMinutes : 2; |
| | | const durationDisplay = formatDurationDisplay(routeDurationSeconds); |
| | | const durationDisplay = formatRemainingDurationDisplay(routeDurationSeconds); |
| | | const discountAmountValue = typeof discountAmount === 'number' && !Number.isNaN(discountAmount) ? discountAmount : 0; |
| | | |
| | | // 不同取消阶段对应的文案配置 |
| | |
| | | |
| | | // 应用 marker 状态并处理状态6刷新标记 |
| | | const applyMarkerState = (nextMarkers: any[], nextMarkersSignature: string, shouldSkipUpdate: boolean, carMarker: any) => { |
| | | if (shouldSkipUpdate) { |
| | | // 跳过整包 markers 更新时,仍兜底同步车辆 marker 位置与角度,避免朝向卡旧值 |
| | | setMarkers((prevMarkers: any[]) => { |
| | | const targetCarIndex = prevMarkers.findIndex((item: any) => item.id === deps.vehicleMarkerId); |
| | | if (targetCarIndex === -1) { |
| | | if (markersSignatureRef.current !== nextMarkersSignature) { |
| | | markersSignatureRef.current = nextMarkersSignature; |
| | | return nextMarkers; |
| | | } |
| | | return prevMarkers; |
| | | } |
| | | const prevCar = prevMarkers[targetCarIndex] || {}; |
| | | const nextCarRotate = typeof carMarker?.rotate === 'number' ? carMarker.rotate : prevCar.rotate; |
| | | const carLocationChanged = |
| | | Number(prevCar.latitude) !== Number(carMarker?.latitude) || |
| | | Number(prevCar.longitude) !== Number(carMarker?.longitude); |
| | | const carRotateChanged = Number(prevCar.rotate) !== Number(nextCarRotate); |
| | | if (!carLocationChanged && !carRotateChanged) { |
| | | return prevMarkers; |
| | | } |
| | | const next = [...prevMarkers]; |
| | | next[targetCarIndex] = { |
| | | ...prevCar, |
| | | latitude: carMarker?.latitude, |
| | | longitude: carMarker?.longitude, |
| | | rotate: nextCarRotate |
| | | }; |
| | | return next; |
| | | }); |
| | | } |
| | | 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 && deps.tripStatus === 6 && deps.statusRule.showCar && !!carMarker && !tripStatus6MarkerRefreshedRef.current) { |
| | | tripStatus6MarkerRefreshedRef.current = true; |
| | | } |
| | | carMarkerInitialized.current = deps.statusRule.showCar && !!carMarker; |
| | |
| | | import Taro from '@tarojs/taro'; |
| | | import { formatDateTime, clearOrderTripStorage, debounce, reportEvent } from '../../../../utils/Robotaxi-public-method'; |
| | | import { saveOrder, getOrderPayment, cancelOrder, endOrder } from '../../../api/begin-route'; |
| | | import { getAgreement } from '../../../api/home'; |
| | | import { checkOtp } from '../../../../utils/tools'; |
| | | import { jumpTo } from '../../../../utils/jump'; |
| | | |
| | |
| | | setCancelType: (value: number) => void; |
| | | /** 取消弹窗设置 */ |
| | | setShowCancelPopup: (value: boolean) => void; |
| | | /** 取消弹窗等待分钟数 */ |
| | | setCancelPopupWaitMinutes: (value: number | null) => void; |
| | | /** 步行点设置 */ |
| | | setWalkingPoints: (value: any[]) => void; |
| | | /** 步行时长设置 */ |
| | |
| | | setLongitude: (value: number) => void; |
| | | /** 打开取消弹窗 */ |
| | | setShowOverRoutePopup: (value: boolean) => void; |
| | | /** 打开协议弹窗 */ |
| | | handleOpenPrivacyPopup: () => void; |
| | | /** 距离与城市校验 */ |
| | | checkDistanceAndCityReady: (startSite: any) => Promise<number | null>; |
| | | /** 绑定 socket 监听 */ |
| | |
| | | * 订单动作编排:下单、取消、结束、支付。 |
| | | */ |
| | | export default function useOrderActions(deps: Deps) { |
| | | const NOT_AGREED_TEXT = 'Not agreed to the agreement'; // 协议未同意返回文案关键字 |
| | | |
| | | // 标准化文本,兼容普通空格与不可见空格差异 |
| | | const normalizeText = (value: any) => String(value || '').replace(/\s+/g, ' ').trim().toLowerCase(); |
| | | |
| | | // 判断接口返回是否为“未同意协议”场景 |
| | | const isNotAgreedMessage = (value: any) => normalizeText(value).includes(NOT_AGREED_TEXT); |
| | | |
| | | // 解析协议状态,兼容不同字段结构 |
| | | const resolveAgreementStatus = (result: any) => { |
| | | if (typeof result === 'boolean') return result; |
| | | return false; |
| | | }; |
| | | |
| | | /** |
| | | * 结束下单中状态。 |
| | | */ |
| | |
| | | coupon_id: cachedCouponId, |
| | | status_code |
| | | }); |
| | | if (isNotAgreedMessage(res) || isNotAgreedMessage(res?.data) || isNotAgreedMessage(res?.msg)) { |
| | | deps.handleOpenPrivacyPopup(); |
| | | return null; |
| | | } |
| | | if (typeof res === 'string') { |
| | | Taro.showToast({ title: res, icon: 'none', mask: true }); |
| | | setTimeout(() => { |
| | |
| | | deps.setCallSubmitting(true); |
| | | const authReady = await checkCallAuth(); |
| | | if (!authReady) return; |
| | | try { |
| | | const agreementResult = await getAgreement(); |
| | | const hasAgreed = resolveAgreementStatus(agreementResult); |
| | | if (!hasAgreed) { |
| | | deps.handleOpenPrivacyPopup(); |
| | | stopCallSubmitting(); |
| | | return; |
| | | } |
| | | } catch (error) { |
| | | Taro.showToast({ title: '协议校验失败,请重试', icon: 'none' }); |
| | | stopCallSubmitting(); |
| | | return; |
| | | } |
| | | const routeReady = checkRouteReady(); |
| | | if (!routeReady) return; |
| | | const s = getStartSiteForCall(); |
| | |
| | | */ |
| | | const restartPlanningRoute = () => { |
| | | deps.setShowCancelPopup(false); |
| | | deps.setCancelPopupWaitMinutes(null); |
| | | deps.setTripStatus(1); |
| | | deps.routeUpdateWalkingRouteForStart(deps.startSite); |
| | | const s = Taro.getStorageSync('startSite'); |
| | |
| | | /** |
| | | * 根据状态打开取消弹窗。 |
| | | */ |
| | | const handleOpenCancelPopupByTripStatus = (status: number, durationDisplay?: any) => { |
| | | if (durationDisplay && ![3, 5].includes(status)) { |
| | | const handleOpenCancelPopupByTripStatus = (status: number, displayInfo?: any) => { |
| | | const waitMinutes = Number(displayInfo?.waitMinutes); |
| | | if (status === 3 && Number.isFinite(waitMinutes)) { |
| | | deps.setCancelPopupWaitMinutes(waitMinutes); |
| | | } else { |
| | | deps.setCancelPopupWaitMinutes(null); |
| | | } |
| | | if (displayInfo?.durationDisplay && ![3, 5].includes(status)) { |
| | | deps.setCancelType(2); |
| | | deps.setShowCancelPopup(true); |
| | | return; |
| | |
| | | export default function usePopupManager() { |
| | | const [showCustomerService, setShowCustomerService] = useState(false); |
| | | const [showCancelPopup, setShowCancelPopup] = useState(false); |
| | | const [cancelPopupWaitMinutes, setCancelPopupWaitMinutes] = useState<number | null>(null); |
| | | const [showAnnouncementsPopup, setShowAnnouncementsPopup] = useState(false); |
| | | const [showOverRoutePopup, setShowOverRoutePopup] = useState(false); |
| | | const [showPassengerNotice, setShowPassengerNotice] = useState(false); |
| | |
| | | const [showEvaluatePopup, setShowEvaluatePopup] = useState(false); |
| | | const [showTipPopup, setShowTipPopup] = useState(false); |
| | | const [tipType, setTipType] = useState(''); |
| | | const [showPrivacyPopup, setShowPrivacyPopup] = useState(false); |
| | | |
| | | const setPopupVisible = (setter: (value: boolean) => void, visible: boolean) => () => setter(visible); |
| | | |
| | |
| | | }, 600); |
| | | }; |
| | | |
| | | const handleCloseCancelPopup = setPopupVisible(setShowCancelPopup, false); |
| | | const handleCloseCancelPopup = () => { |
| | | setShowCancelPopup(false); |
| | | setCancelPopupWaitMinutes(null); |
| | | }; |
| | | const handleCloseAnnouncementsPopup = setPopupVisible(setShowAnnouncementsPopup, false); |
| | | const handleOpenAnnouncementsPopup = setPopupVisible(setShowAnnouncementsPopup, true); |
| | | const handleOpenOverRoutePopup = setPopupVisible(setShowOverRoutePopup, true); |
| | |
| | | |
| | | const handleOpenEvaluatePopup = setPopupVisible(setShowEvaluatePopup, true); |
| | | const handleCloseEvaluatePopup = setPopupVisible(setShowEvaluatePopup, false); |
| | | const handleOpenPrivacyPopup = setPopupVisible(setShowPrivacyPopup, true); |
| | | const handleClosePrivacyPopup = setPopupVisible(setShowPrivacyPopup, false); |
| | | |
| | | return { |
| | | showCustomerService, |
| | |
| | | handleCloseCustomerService, |
| | | showCancelPopup, |
| | | setShowCancelPopup, |
| | | cancelPopupWaitMinutes, |
| | | setCancelPopupWaitMinutes, |
| | | handleCloseCancelPopup, |
| | | cancelType, |
| | | setCancelType, |
| | |
| | | setShowTipPopup, |
| | | handleCloseTipPopup, |
| | | tipType, |
| | | setTipType |
| | | setTipType, |
| | | showPrivacyPopup, |
| | | setShowPrivacyPopup, |
| | | handleOpenPrivacyPopup, |
| | | handleClosePrivacyPopup |
| | | }; |
| | | } |
| | |
| | | setVehicleDirection: () => { } |
| | | }; |
| | | |
| | | // 统一解析接口返回方向 |
| | | const resolveDirectionValue = (payload: any) => { |
| | | const rawDirection = payload?.direction; |
| | | const direction = Number(rawDirection); |
| | | if (!Number.isFinite(direction)) { |
| | | return null; |
| | | } |
| | | return ((direction % 360) + 360) % 360; |
| | | }; |
| | | |
| | | export default function useRouteManager(rawDeps?: Partial<Deps>) { |
| | | const deps = { ...defaultDeps, ...(rawDeps || {}) } as Deps; |
| | | // 路线渲染线集合 |
| | |
| | | if (durationSeconds !== null) { |
| | | setRouteDuration(durationSeconds); |
| | | } |
| | | const dir = Number(res?.direction); |
| | | if (Number.isFinite(dir)) { |
| | | deps.setVehicleDirection(((dir % 360) + 360) % 360); |
| | | const normalizedDirection = resolveDirectionValue(res); |
| | | if (typeof normalizedDirection === 'number') { |
| | | deps.setVehicleDirection(normalizedDirection); |
| | | } |
| | | }) |
| | | .catch(() => { }); |
| | |
| | | const lastVehiclePointRef = useRef<{ latitude: number; longitude: number } | null>(null); |
| | | const lastVehicleUpdateAtRef = useRef<number>(0); |
| | | const TRANSLATE_DURATION_MS = 100; |
| | | const isTranslatingRef = useRef(false); // 当前是否有进行中的平移动画 |
| | | const pendingTranslateRef = useRef<{ |
| | | latitude: number; |
| | | longitude: number; |
| | | rotate: number; |
| | | markerId: number; |
| | | } | null>(null); // 最新待执行的平移目标(仅保留最后一次) |
| | | |
| | | // 执行一次平移动画,完成后自动消费最新待执行目标 |
| | | const flushTranslateTask = () => { |
| | | if (isTranslatingRef.current) return; |
| | | if (!pendingTranslateRef.current) return; |
| | | const task = pendingTranslateRef.current; |
| | | pendingTranslateRef.current = null; |
| | | isTranslatingRef.current = true; |
| | | const mapCtx = Taro.createMapContext('map'); |
| | | mapCtx.translateMarker({ |
| | | markerId: task.markerId, |
| | | destination: { |
| | | latitude: task.latitude, |
| | | longitude: task.longitude, |
| | | }, |
| | | autoRotate: false, |
| | | moveWithRotate: true, |
| | | rotate: task.rotate, |
| | | duration: TRANSLATE_DURATION_MS, |
| | | complete: () => { |
| | | isTranslatingRef.current = false; |
| | | if (pendingTranslateRef.current) { |
| | | flushTranslateTask(); |
| | | } |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | // 更新车辆位置并维护最近位置引用 |
| | | const updateVehicleLocationThrottled = (lat: number, lng: number, force = false) => { |
| | |
| | | useEffect(() => { |
| | | if (!statusRule.translateCar) return; |
| | | if (!vehicleLocation) return; |
| | | const mapCtx = Taro.createMapContext('map'); |
| | | mapCtx.translateMarker({ |
| | | markerId: vehicleMarkerId, |
| | | destination: { |
| | | latitude: vehicleLocation.latitude, |
| | | longitude: vehicleLocation.longitude, |
| | | }, |
| | | autoRotate: false, |
| | | moveWithRotate: true, |
| | | pendingTranslateRef.current = { |
| | | latitude: vehicleLocation.latitude, |
| | | longitude: vehicleLocation.longitude, |
| | | rotate: typeof vehicleDirection === 'number' ? ((vehicleDirection % 360) + 360) % 360 : 0, |
| | | duration: TRANSLATE_DURATION_MS |
| | | }); |
| | | markerId: vehicleMarkerId |
| | | }; |
| | | flushTranslateTask(); |
| | | }, [statusRule.translateCar, vehicleLocation, vehicleDirection, vehicleMarkerId]); |
| | | |
| | | useEffect(() => { |
| | | if (statusRule.translateCar) return; |
| | | isTranslatingRef.current = false; |
| | | pendingTranslateRef.current = null; |
| | | }, [statusRule.translateCar]); |
| | | |
| | | return { |
| | | vehicleLocation, |
| | | setVehicleLocation, |
| | |
| | | import ReleaseRoute from '../components/release-route'; |
| | | import ReleaseBeBeing from '../components/release-be-being'; |
| | | import CallingCar from '../components/calling-car'; |
| | | import { formatDurationDisplay } from '../../../../utils/Robotaxi-public-method'; |
| | | import { formatRemainingDurationDisplay } from '../../../utils/format'; |
| | | |
| | | type Deps = { |
| | | /** 是否安卓 */ |
| | |
| | | evaluateSubmitVersion: number; |
| | | /** 评价关闭版本 */ |
| | | evaluateCloseVersion: number; |
| | | /** 打开公告 */ |
| | | handleOpenAnnouncementsPopup: () => void; |
| | | /** 打开结束弹窗 */ |
| | | handleOpenOverRoutePopup: () => void; |
| | | /** 支付 */ |
| | |
| | | */ |
| | | export default function useViewDelegates(deps: Deps) { |
| | | /** 时长文案 */ |
| | | const durationDisplay = formatDurationDisplay(deps.routeDuration); |
| | | const durationDisplay = formatRemainingDurationDisplay(deps.routeDuration); |
| | | |
| | | /** |
| | | * 渲染等待态气泡。 |
| | |
| | | priceUpdateVersion={deps.priceUpdateVersion} |
| | | evaluateSubmitVersion={deps.evaluateSubmitVersion} |
| | | evaluateCloseVersion={deps.evaluateCloseVersion} |
| | | handleOpenAnnouncementsPopup={deps.handleOpenAnnouncementsPopup} |
| | | handleOpenOverRoutePopup={deps.handleOpenOverRoutePopup} |
| | | handlePay={deps.handlePay} |
| | | handleOpenEvaluatePopup={deps.handleOpenEvaluatePopup} |
| | |
| | | } |
| | | }; |
| | | |
| | | // 统一解析方向值 |
| | | const resolveDirectionValue = (payload: any) => { |
| | | const rawDirection = payload?.content?.direction; |
| | | const direction = Number(rawDirection); |
| | | if (!Number.isFinite(direction)) { |
| | | return null; |
| | | } |
| | | return ((direction % 360) + 360) % 360; |
| | | }; |
| | | |
| | | const updateRouteSummary = (payload: any, deps: UseWebSocketEventsDeps) => { |
| | | const direction = Number(payload?.direction); |
| | | if (Number.isFinite(direction)) { |
| | | deps.setVehicleDirection(((direction % 360) + 360) % 360); |
| | | const normalizedDirection = resolveDirectionValue(payload); |
| | | if (typeof normalizedDirection === 'number') { |
| | | deps.setVehicleDirection(normalizedDirection); |
| | | } |
| | | const route = safeParse(payload?.content?.route, {}); |
| | | const distanceKm = normalizeDistanceKm(Number(route?.distance)); |
| | |
| | | handleCloseCustomerService, |
| | | showCancelPopup, |
| | | setShowCancelPopup, |
| | | cancelPopupWaitMinutes, |
| | | setCancelPopupWaitMinutes, |
| | | handleCloseCancelPopup, |
| | | cancelType, |
| | | setCancelType, |
| | |
| | | setShowTipPopup, |
| | | handleCloseTipPopup, |
| | | tipType, |
| | | setTipType |
| | | setTipType, |
| | | showPrivacyPopup, |
| | | handleOpenPrivacyPopup, |
| | | handleClosePrivacyPopup |
| | | } = usePopupManager(); // 弹窗状态编排 |
| | | const [longitude, setLongitude] = useState<number | null>(null); // 经度 |
| | | const [latitude, setLatitude] = useState<number | null>(null); // 纬度 |
| | |
| | | setCallSubmitting, |
| | | setCancelType, |
| | | setShowCancelPopup, |
| | | setCancelPopupWaitMinutes, |
| | | setWalkingPoints, |
| | | setWalkingTime, |
| | | setLatitude, |
| | | setLongitude, |
| | | setShowOverRoutePopup, |
| | | handleOpenPrivacyPopup, |
| | | checkDistanceAndCityReady, |
| | | bindSocketListeners, |
| | | routeUpdateWalkingRouteForStart, |
| | |
| | | priceUpdateVersion, |
| | | evaluateSubmitVersion, |
| | | evaluateCloseVersion, |
| | | handleOpenAnnouncementsPopup, |
| | | handleOpenOverRoutePopup, |
| | | handlePay, |
| | | handleOpenEvaluatePopup, |
| | |
| | | handleCloseCustomerService={handleCloseCustomerService} |
| | | showCancelPopup={showCancelPopup} |
| | | cancelType={cancelType} |
| | | cancelPopupWaitMinutes={cancelPopupWaitMinutes} |
| | | startSite={startSite} |
| | | endSite={endSite} |
| | | fareEstimate={fareEstimate} |
| | |
| | | showTipPopup={showTipPopup} |
| | | handleCloseTipPopup={handleCloseTipPopup} |
| | | tipType={tipType} |
| | | showPrivacyPopup={showPrivacyPopup} |
| | | handleClosePrivacyPopup={handleClosePrivacyPopup} |
| | | showPassengerNotice={showPassengerNotice} |
| | | handleClosePassengerNotice={handleClosePassengerNotice} |
| | | changeCheckedEnter={changeCheckedEnter} |
| | |
| | | } |
| | | return Math.round(num); |
| | | }; |
| | | |
| | | // 剩余时长展示(begin-route):分钟按向下截断,秒按四舍五入 |
| | | export const formatRemainingDurationDisplay = (value: any) => { |
| | | const num = Number(value); |
| | | if (Number.isNaN(num)) { |
| | | return { value: 0, unit: '分钟' as const }; |
| | | } |
| | | if (num < 60) { |
| | | return { value: Math.max(0, Math.round(num)), unit: '秒' as const }; |
| | | } |
| | | return { value: Math.max(0, Math.floor(num / 60)), unit: '分钟' as const }; |
| | | }; |
| | |
| | | private heartbeatTimer: any = null; |
| | | private heartbeatTimeoutTimer: any = null; // 用于检测心跳超时的定时器 |
| | | private heartbeatInterval: number = 10000; // 10秒心跳 |
| | | private heartbeatTimeout: number = 3000; // 5秒收不到响应即视为掉线 |
| | | private heartbeatTimeout: number = 3000; // 3秒收不到响应即视为掉线 |
| | | private listeners: Map<string, MessageCallback[]> = new Map(); |
| | | private globalEventsBound: boolean = false; |
| | | |