Merge branch 'feature_urgency_fix' into feature_v1.0
| | |
| | | onOpenSetting: () => void; |
| | | /** 查看运营区域 */ |
| | | onViewOperatingArea: () => void; |
| | | /** 初始化锚点底部偏移 */ |
| | | anchorBottom?: number; |
| | | /** 透明度,用于配合上划消失 */ |
| | | opacity?: number; |
| | | /** 拖拽状态,用于控制过渡动画 */ |
| | | isDragging?: boolean; |
| | | }; |
| | | |
| | | /** |
| | | * 地图顶部提示层。 |
| | | */ |
| | | export default function MapOverlayTips(props: Props) { |
| | | const anchorStyle = typeof props.anchorBottom === 'number' |
| | | ? { |
| | | top: 'auto', |
| | | bottom: `${props.anchorBottom}px`, |
| | | opacity: props.opacity !== undefined ? props.opacity : 1, |
| | | transition: props.isDragging ? 'none' : 'opacity 0.2s ease-out' |
| | | } |
| | | : undefined; |
| | | return ( |
| | | <View className="content-box-top"> |
| | | <View className="content-box-top" style={anchorStyle}> |
| | | <View className="position-box"> |
| | | { |
| | | props.isGuest && <View className="visitor-stste ml-32"> |
| | |
| | | position: fixed; |
| | | top: 55%; |
| | | left: 0px; |
| | | z-index: 1; |
| | | z-index: 2; |
| | | |
| | | .position-box { |
| | | |
| | |
| | | const [weatherData, setWeatherData] = useState<any>(null); // 天气数据 |
| | | const [orderData, setOrderData] = useState<any>({}); // 订单数据 |
| | | const [isLogin, setIsLogin] = useState(false); // 是否登录 |
| | | const [overlayAnchorBottom, setOverlayAnchorBottom] = useState<number | null>(null); // 浮层初始化锚点(距底部) |
| | | const { |
| | | boxHeight, |
| | | originOpacity, |
| | |
| | | isInside: isOperatingArea |
| | | }); |
| | | } |
| | | setTimeout(() => { |
| | | initOverlayAnchor(); |
| | | }, 80); |
| | | }); |
| | | |
| | | useDidHide(() => { |
| | |
| | | setShowCustomerService(false); |
| | | }; |
| | | |
| | | // 初始化浮层锚点:固定在 content-box 顶部上方 32px |
| | | const initOverlayAnchor = () => { |
| | | // content-box 高度受 banner 数量影响:多 banner 复用 boxHeight,单 banner 走渲染后实测高度 |
| | | if (bannerList.length > 1) { |
| | | const bottom = Math.max(0, 280 + 32); |
| | | if (bottom <= 0) return; |
| | | setOverlayAnchorBottom(bottom); |
| | | return; |
| | | } |
| | | const query = Taro.createSelectorQuery(); |
| | | query.select('.content-box').boundingClientRect(); |
| | | query.exec((res) => { |
| | | const rect = res?.[0]; |
| | | const height = Number(rect?.height); |
| | | if (!Number.isFinite(height) || height <= 0) return; |
| | | const bottom = Math.max(0, height + 32); |
| | | setOverlayAnchorBottom(bottom); |
| | | }); |
| | | }; |
| | | |
| | | useEffect(() => { |
| | | const timer = setTimeout(() => { |
| | | Taro.nextTick(() => { |
| | | initOverlayAnchor(); |
| | | }); |
| | | }, 80); |
| | | return () => clearTimeout(timer); |
| | | }, [bannerList.length, tripStatus]); |
| | | |
| | | // 处理点击地图 marker |
| | | const handleMarkerTap = (e: any) => { |
| | | if (isLocationAuthDenied) return; |
| | |
| | | )} |
| | | </View> |
| | | {/* 定位图标 */} |
| | | <View className="origin-box" style={{ opacity: originOpacity, transition: isDragging.current ? 'none' : 'opacity 0.2s ease-out' }}> |
| | | <View |
| | | className="origin-box" |
| | | style={{ |
| | | opacity: originOpacity, |
| | | transition: isDragging.current ? 'none' : 'opacity 0.2s ease-out', |
| | | top: typeof overlayAnchorBottom === 'number' ? 'auto' : undefined, |
| | | bottom: typeof overlayAnchorBottom === 'number' ? `${overlayAnchorBottom}px` : undefined, |
| | | }} |
| | | > |
| | | <Image onClick={toOrigin} className="origin-icon" src={originIcon} /> |
| | | </View> |
| | | <MapOverlayTips |
| | |
| | | isMaintain={isMaintain} |
| | | maintainBounce={maintainBounce} |
| | | hotfixEndText={hotfixEndText} |
| | | anchorBottom={overlayAnchorBottom ?? undefined} |
| | | opacity={originOpacity} |
| | | isDragging={isDragging.current} |
| | | onShowPrivacy={() => setShowPrivacy(true)} |
| | | onOpenSetting={handleOpenSetting} |
| | | onViewOperatingArea={handleViewOperatingArea} |
| | |
| | | import TipPopup from '../../select-map-site/components/tip-popup'; |
| | | import AuthBluetoothPopup from './auth-bluetooth-popup'; |
| | | import { TripStatus } from '../constants/trip-status'; |
| | | 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 = { |
| | | [TripStatus.CallingCar]: <>预计<View className="time">{waitMinutes}</View>分钟内有车应答</>, |
| | | [TripStatus.VehicleApproaching]: <>车辆还有<View className="time">{durationDisplay.value}</View>{durationDisplay.unit}到达</>, |
| | | [TripStatus.VehicleApproaching]: isArriveSoon |
| | | ? <>即将到达</> |
| | | :<>车辆还有<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) |
| | |
| | | { |
| | | [TripStatus.CallingCar].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 { TripStatus } from '../constants/trip-status'; |
| | | |
| | | import useTripCountdown from './calling-car-modules/use-trip-countdown'; // 行程倒计时 |
| | |
| | | handlePay?: () => void; |
| | | /** 打开详细评价弹窗 */ |
| | | handleOpenEvaluatePopup?: () => void; |
| | | /** 首次开门后打开安全须知弹窗 */ |
| | | handleOpenAnnouncementsPopup?: () => void; |
| | | /** 取消行程 */ |
| | | handleCancelTrip?: () => void; |
| | | /** 修改目的地 */ |
| | |
| | | onToggleDetailFade, |
| | | handlePay, |
| | | handleOpenEvaluatePopup, |
| | | handleOpenAnnouncementsPopup, |
| | | handleCancelTrip, |
| | | handleModifyDestination, |
| | | handleShareTrip, |
| | |
| | | }); |
| | | |
| | | /** 预计到达时长展示(秒/分钟) */ |
| | | const durationDisplay = formatDurationDisplay(routeDurationSeconds); |
| | | const durationDisplay = formatRemainingDurationDisplay(routeDurationSeconds); |
| | | /** 车辆即将到达上车点(<=60秒) */ |
| | | const isArriveSoon = tripStatus === TripStatus.VehicleApproaching && (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' |
| | | import { TripStatus } from '../constants/trip-status'; |
| | | |
| | |
| | | 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 状态并处理“行驶中”阶段的刷新标记 |
| | | 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); |
| | |
| | | import Taro from '@tarojs/taro'; |
| | | import { formatDateTime, clearOrderTripStorage, debounce, reportEvent } from '../../../../utils/Robotaxi-public-method'; |
| | | import { saveOrder, getOrderPayment, cancelOrder, endOrder, routeStaleCheck } from '../../../api/begin-route'; |
| | | import { getAgreement } from '../../../api/home'; |
| | | import { checkOtp } from '../../../../utils/tools'; |
| | | import { jumpTo } from '../../../../utils/jump'; |
| | | import { subscribeAfterOrderSuccess } from '../../../../utils/subscribe'; |
| | |
| | | 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 ROUTE_STALE_NOTICE_TEXT = '路况已变,价格已更新'; // 路况变化提示文案 |
| | | 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); |
| | | |
| | | // 协议接口约定:true=已同意,false=未同意 |
| | | const resolveAgreementStatus = (result: any) => result === true; |
| | | |
| | | /** |
| | | * 结束下单中状态。 |
| | |
| | | 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(TripStatus.PublishRoute); |
| | | deps.routeUpdateWalkingRouteForStart(deps.startSite); |
| | | const s = Taro.getStorageSync('startSite'); |
| | |
| | | /** |
| | | * 根据状态打开取消弹窗。 |
| | | */ |
| | | const handleOpenCancelPopupByTripStatus = (status: TripStatus, durationDisplay?: any) => { |
| | | if (durationDisplay && ![3, 5].includes(status)) { |
| | | const handleOpenCancelPopupByTripStatus = (status: TripStatus, 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 |
| | | }; |
| | | } |
| | |
| | | return { longitude, latitude }; |
| | | }; |
| | | |
| | | // 统一解析接口返回方向 |
| | | 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; |
| | | // 路线渲染线集合 |
| | |
| | | // 选择路线选项 |
| | | const handleSelectRoute = (index: number) => { |
| | | if (!routeOptions[index] || index === selectedRouteIndex) return; |
| | | |
| | | |
| | | const routeData = routeOptions[index]; |
| | | setSelectedRouteIndex(index); |
| | | |
| | | |
| | | if (routeData?.fareEstimate) { |
| | | setFareEstimate(routeData.fareEstimate); |
| | | } |
| | | if (routeData?.xpPricingCard) { |
| | | Taro.setStorageSync('xpPricingCard', routeData.xpPricingCard); |
| | | } |
| | | |
| | | |
| | | // Check if the route payload contains new site info and update them if possible |
| | | let newStartLat: number | undefined; |
| | | let newStartLng: number | undefined; |
| | |
| | | |
| | | // fallback to polyline start/end points if origin/destination are not provided |
| | | if (points.length > 0) { |
| | | newStartLat = points[0].latitude; |
| | | newStartLng = points[0].longitude; |
| | | newEndLat = points[points.length - 1].latitude; |
| | | newEndLng = points[points.length - 1].longitude; |
| | | newStartLat = points[0].latitude; |
| | | newStartLng = points[0].longitude; |
| | | newEndLat = points[points.length - 1].latitude; |
| | | newEndLng = points[points.length - 1].longitude; |
| | | } |
| | | |
| | | if (routeData?.origin) { |
| | | newStartLat = routeData.origin.latitude ?? newStartLat; |
| | | newStartLng = routeData.origin.longitude ?? newStartLng; |
| | | newStartLat = routeData.origin.latitude ?? newStartLat; |
| | | newStartLng = routeData.origin.longitude ?? newStartLng; |
| | | } |
| | | if (routeData?.destination) { |
| | | newEndLat = routeData.destination.latitude ?? newEndLat; |
| | | newEndLng = routeData.destination.longitude ?? newEndLng; |
| | | newEndLat = routeData.destination.latitude ?? newEndLat; |
| | | newEndLng = routeData.destination.longitude ?? newEndLng; |
| | | } |
| | | |
| | | if (deps.tripStatusRef.current === TripStatus.PublishRoute && Number.isFinite(newStartLat) && Number.isFinite(newStartLng) && typeof deps.setStartSite === 'function') { |
| | | deps.setStartSite({ |
| | | ...deps.startSite, |
| | | latitude: newStartLat, |
| | | longitude: newStartLng, |
| | | name: routeData?.origin?.name || deps.startSite?.name |
| | | }); |
| | | deps.setStartSite({ |
| | | ...deps.startSite, |
| | | latitude: newStartLat, |
| | | longitude: newStartLng, |
| | | name: routeData?.origin?.name || deps.startSite?.name |
| | | }); |
| | | } |
| | | |
| | | if (deps.tripStatusRef.current === TripStatus.PublishRoute && Number.isFinite(newEndLat) && Number.isFinite(newEndLng) && typeof deps.setEndSite === 'function') { |
| | | deps.setEndSite({ |
| | | ...deps.endSite, |
| | | latitude: newEndLat, |
| | | longitude: newEndLng, |
| | | name: routeData?.destination?.name || deps.endSite?.name |
| | | }); |
| | | deps.setEndSite({ |
| | | ...deps.endSite, |
| | | latitude: newEndLat, |
| | | longitude: newEndLng, |
| | | name: routeData?.destination?.name || deps.endSite?.name |
| | | }); |
| | | } |
| | | |
| | | const distanceKm = normalizeDistanceKm(Number(route?.distance)); |
| | |
| | | if (durationSeconds !== null) { |
| | | setRouteDuration(durationSeconds); |
| | | } |
| | | |
| | | |
| | | if (Array.isArray(points) && points.length > 0) { |
| | | applyRoutePoints(points); |
| | | // 如果允许选择路线(当前业务下未发单及修改目的地确认态时 allowSelectableRoutes 为 true) |
| | |
| | | // 在发布行程阶段或修改目的地确认态时,需要包含所有路线点以便自适应视野 |
| | | const allRoutesPoints: any[] = []; |
| | | allRoutePoints.forEach(pts => { |
| | | if (Array.isArray(pts)) { |
| | | allRoutesPoints.push(...pts); |
| | | } |
| | | if (Array.isArray(pts)) { |
| | | allRoutesPoints.push(...pts); |
| | | } |
| | | }); |
| | | const allPoints = [...allRoutesPoints, ...walkingPoints]; |
| | | if (allPoints.length > 0) { |
| | |
| | | }, |
| | | }; |
| | | if (cachedCouponId) { |
| | | params.userCouponId = cachedCouponId; |
| | | } |
| | | params.userCouponId = cachedCouponId; |
| | | } |
| | | |
| | | queryRouteAndFare(params).then((res: any) => { |
| | | |
| | | setRouteAndFareError(false); |
| | | |
| | | let routeData = res; |
| | | let options: any[] = []; |
| | | |
| | | queryRouteAndFare(params).then((res: any) => { |
| | | |
| | | setRouteAndFareError(false); |
| | | |
| | | let routeData = res; |
| | | let options: any[] = []; |
| | | |
| | | // Handle both array and single object response for backward compatibility |
| | | let allPointsArr: any[][] = []; |
| | | if (Array.isArray(res) && res.length > 0) { |
| | |
| | | if (deps.allowSelectableRoutes) { |
| | | const allRoutesPoints: any[] = []; |
| | | allPointsArr.forEach(pts => { |
| | | if (Array.isArray(pts)) { |
| | | allRoutesPoints.push(...pts); |
| | | } |
| | | if (Array.isArray(pts)) { |
| | | allRoutesPoints.push(...pts); |
| | | } |
| | | }); |
| | | const allPoints = [...allRoutesPoints, ...walkingPoints]; |
| | | if (allPoints.length > 0) { |
| | |
| | | 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(() => { }); |
| | |
| | | // 构建地图 polyline 数据 |
| | | const buildPolylineLines = (displayRoutePoints: any[]) => { |
| | | const lines: any[] = []; |
| | | |
| | | |
| | | // 如果有多条路线且在未发单状态或修改目的地状态(allowSelectableRoutes === true),渲染所有路线 |
| | | if (deps.shouldShowRoute && deps.allowSelectableRoutes && allRoutePoints.length > 1) { |
| | | allRoutePoints.forEach((points, index) => { |
| | | if (!points || points.length === 0) return; |
| | | |
| | | |
| | | const isSelected = index === selectedRouteIndex; |
| | | // 未选中的路线放底层(z-index低些,如果支持的话),选中放顶层 |
| | | // 这里通过控制 push 顺序来影响层级,最后 push 选中的路线 |
| | |
| | | }); |
| | | } |
| | | }); |
| | | |
| | | |
| | | // 最后 push 选中的路线,使其显示在最上层 |
| | | if (allRoutePoints[selectedRouteIndex] && allRoutePoints[selectedRouteIndex].length > 0) { |
| | | lines.push({ |
| | |
| | | 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 ReleaseBeBeing from '../components/release-be-being'; |
| | | 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'; |
| | | 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); |
| | | const routeSummaryText = deps.routeDistance && deps.routeDuration !== null |
| | | ? `${durationDisplay.value}${durationDisplay.unit} ${deps.routeDistance.value}${deps.routeDistance.unit === 'm' ? '米' : '公里'}` |
| | | : ''; |
| | |
| | | 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={effectiveEndSite} |
| | | 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 locationReportTimer: any = null; // 位置上报定时器 |
| | | private locationReportInterval: number = 35000; // 35秒上报一次位置 |
| | | private heartbeatTimeout: number = 3000; // 3秒收不到响应即视为掉线 |
| | | private listeners: Map<string, MessageCallback[]> = new Map(); |
| | | private globalEventsBound: boolean = false; |
| | | |