Merge branch 'feature_v1.0' into feature_v2.0
| | |
| | | import Taro from '@tarojs/taro'; |
| | | import { useEffect, useMemo, useRef, useState } from 'react'; |
| | | import { getUserQuickCapsule } from '../../../../robotaxi/api/home'; |
| | | import { getOrderDistance } from '../../../../robotaxi/api/select-map-site'; |
| | | import { |
| | | RECOMMENDED_CONFIRM_RIDE_SOURCE_TYPE, |
| | | completeConfirmRideSelectionSession, |
| | | writeConfirmRideSiteSourceType |
| | | } from '../../../../robotaxi/constants/confirm-ride-report'; |
| | | import { ORDER_DISTANCE_MIN_DISTANCE_TOAST } from '../../../../robotaxi/constants/order-distance'; |
| | | import { |
| | | ensureOrderDistanceConfig, |
| | | isWithinStartEndMinDistance, |
| | | OrderDistanceConfig |
| | | } from '../../../../robotaxi/utils/order-distance'; |
| | | |
| | | type CapsuleSite = { |
| | | id: string | number; |
| | |
| | | export default function useQuickCapsule(deps: Deps) { |
| | | const [capsuleSite, setCapsuleSite] = useState<CapsuleSite | null>(null); // 快捷胶囊站点 |
| | | const requestSeqRef = useRef(0); // 快捷胶囊请求序号,避免旧请求覆盖 |
| | | const distanceConfigRef = useRef<OrderDistanceConfig>({}); // 快捷胶囊下单距离配置缓存 |
| | | |
| | | /** |
| | | * 获取用于请求的上车点 ID(仅使用当前选中站点,避免滑动过程误用旧缓存)。 |
| | |
| | | const clearQuickCapsule = () => { |
| | | setCapsuleSite(null); |
| | | }; |
| | | |
| | | /** |
| | | * 确保快捷胶囊点击时具备距离配置,避免漏掉起终点最短距离校验。 |
| | | */ |
| | | const ensureDistanceConfigReady = async () => ensureOrderDistanceConfig({ |
| | | currentConfig: distanceConfigRef.current, |
| | | fetchConfig: getOrderDistance, |
| | | onConfigResolved: (config) => { |
| | | distanceConfigRef.current = config; |
| | | } |
| | | }); |
| | | |
| | | /** |
| | | * 归一化快捷胶囊站点结构,兼容不同后端字段。 |
| | |
| | | Taro.showToast({ title: '暂无快捷目的地', icon: 'none' }); |
| | | return; |
| | | } |
| | | const distanceConfig = await ensureDistanceConfigReady(); |
| | | if (!isWithinStartEndMinDistance({ |
| | | startSite: targetStartSite, |
| | | endSite: capsuleSite, |
| | | minDistance: distanceConfig?.minDistance |
| | | })) { |
| | | Taro.showToast({ title: ORDER_DISTANCE_MIN_DISTANCE_TOAST, icon: 'none' }); |
| | | return; |
| | | } |
| | | Taro.setStorageSync('endSite', capsuleSite); |
| | | Taro.setStorageSync('startSite', targetStartSite); |
| | | writeConfirmRideSiteSourceType('start', RECOMMENDED_CONFIRM_RIDE_SOURCE_TYPE); |
| | |
| | | payload: QueryOrderRoutePayload |
| | | ): Promise<any> => { |
| | | return request({ |
| | | url: _ROBOTAXI_HOST + `/order/api/v1/mini-program/order/queryOrderTrajectory`, |
| | | // url: _ROBOTAXI_HOST + `/order/api/v1/mini-program/order/queryOrderTrajectory`, |
| | | url: _ROBOTAXI_HOST + `/order/base/v1/mini-program/orderActive/share/queryOrderTrajectory`, |
| | | method: 'GET', |
| | | payload, |
| | | showToast: false, |
| | |
| | | }, true); |
| | | } |
| | | |
| | | // 分享订单查询实时轨迹路线 |
| | | export const shareQueryOrderRoute = async (orderId: OrderId): Promise<ShareQueryOrderRouteResponse> => { |
| | | return request({ |
| | | url: _ROBOTAXI_HOST + `/order/base/v1/mini-program/orderActive/share/queryOrderRoute/${orderId}`, |
| | | method: 'GET' |
| | | }, true); |
| | | } |
| | | |
| | | // 静默查询分享订单实时路线,供分享页自行处理业务异常,避免默认拦截 toast。 |
| | | export const shareQueryOrderRouteSilently = async (orderId: OrderId): Promise<any> => { |
| | | return request({ |
| | |
| | | method: 'GET', |
| | | showToast: false, |
| | | skipException: true |
| | | }, true); |
| | | } |
| | | |
| | | // 分享订单查询订单详情 |
| | | export const shareQueryOrderInfo = async ( |
| | | payload: ShareQueryOrderInfoPayload |
| | | ): Promise<ShareQueryOrderInfoResponse> => { |
| | | return request({ |
| | | url: _ROBOTAXI_HOST + '/order/base/v1/mini-program/orderActive/share/orderInfo', |
| | | method: 'GET', |
| | | payload |
| | | }, true); |
| | | } |
| | | |
| | |
| | | import Taro from '@tarojs/taro'; |
| | | import { View, Text, Image } from '@tarojs/components'; |
| | | import { useEffect, useState } from 'react'; |
| | | import { |
| | | ORDER_DISTANCE_MAX_DISTANCE_TITLE, |
| | | ORDER_DISTANCE_MIN_DISTANCE_TITLE, |
| | | ORDER_DISTANCE_TIP_TYPE_MIN_DISTANCE |
| | | } from '../../constants/order-distance'; |
| | | import './index.less' |
| | | |
| | | type Props = { |
| | |
| | | <View className='tip-mask'></View> |
| | | <View className='tip-content' animation={animationData}> |
| | | <Image onClick={toHome} className='close-icon' src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/close-icon.png" /> |
| | | <View className='tip-title'>{tipType === '1' ? '上车点与目的地距离较近,建议步行前往' : '当前位置距上车点较远,暂不支持叫车,请重新选择上车点'}</View> |
| | | <View className='tip-title'> |
| | | {tipType === ORDER_DISTANCE_TIP_TYPE_MIN_DISTANCE ? ORDER_DISTANCE_MIN_DISTANCE_TITLE : ORDER_DISTANCE_MAX_DISTANCE_TITLE} |
| | | </View> |
| | | <View onClick={onClose} className='reselect-btn'>重新选点</View> |
| | | </View> |
| | | </View> |
| New file |
| | |
| | | /** 起终点距离过近提示类型。 */ |
| | | export const ORDER_DISTANCE_TIP_TYPE_MIN_DISTANCE = '1'; |
| | | |
| | | /** 用户距离起点过远提示类型。 */ |
| | | export const ORDER_DISTANCE_TIP_TYPE_MAX_DISTANCE = '2'; |
| | | |
| | | /** 起终点距离过近弹窗文案。 */ |
| | | export const ORDER_DISTANCE_MIN_DISTANCE_TITLE = '上车点与目的地距离较近,建议步行前往'; |
| | | |
| | | /** 用户距离起点过远弹窗文案。 */ |
| | | export const ORDER_DISTANCE_MAX_DISTANCE_TITLE = '当前位置距上车点较远,暂不支持叫车,请重新选择上车点'; |
| | | |
| | | /** 首页快捷胶囊距离过近提示文案。 */ |
| | | export const ORDER_DISTANCE_MIN_DISTANCE_TOAST = '上车点与目的地距离较近,请重新选择'; |
| | |
| | | </View> |
| | | } |
| | | </View> |
| | | <View className="car-card-end-cost" onClick={toMoneyDetail}> |
| | | <View className="car-card-end-cost-money"> |
| | | 全程{formatTripDistance(fareBreakdown?.kilometer)}|{formatTripDurationFromSeconds(fareBreakdown?.time)} |
| | | <View className="car-card-end-cost"> |
| | | <View className="car-card-end-cost-label"> |
| | | 行程费用 |
| | | </View> |
| | | <Image src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/ccc-icon-right.png" /> |
| | | <View className="car-card-end-cost-content"> |
| | | <View className="car-card-end-cost-money" onClick={toMoneyDetail}> |
| | | 全程{formatTripDistance(fareBreakdown?.kilometer)}|{formatTripDurationFromSeconds(fareBreakdown?.time)} |
| | | </View> |
| | | <Image src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/ccc-icon-right.png" /> |
| | | </View> |
| | | </View> |
| | | { |
| | | (fareBreakdown?.operateFree != 1 && [TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(tripStatus)) && Number(fareBreakdown?.redPacketMoney + fareBreakdown?.couponMoney) > 0 && |
| | |
| | | { |
| | | fareBreakdown?.operateFree != 1 ? |
| | | <View className="pay-box-money"><View>{formatMoney( |
| | | fareBreakdown?.payMoney <= 0 |
| | | fareBreakdown?.payMoney <= 0 |
| | | ? 0 : fareBreakdown?.payMoney)}</View><View className="unit">元</View></View> |
| | | : |
| | | <View className="pay-box-free">载客测试免费</View> |
| | |
| | | margin-top: 16px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: flex-end; |
| | | gap: 4px; |
| | | justify-content: space-between; |
| | | |
| | | &-money { |
| | | &-label { |
| | | font-size: 24px; |
| | | line-height: 44px; |
| | | letter-spacing: normal; |
| | | color: rgba(0, 0, 0, 0.4); |
| | | color: rgba(0, 0, 0, 0.9); |
| | | } |
| | | |
| | | image { |
| | | margin-left: 4px; |
| | | width: 32px; |
| | | height: 32px; |
| | | &-content { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 4px; |
| | | |
| | | &-money { |
| | | font-size: 24px; |
| | | line-height: 44px; |
| | | letter-spacing: normal; |
| | | color: rgba(0, 0, 0, 0.4); |
| | | } |
| | | |
| | | image { |
| | | margin-left: 4px; |
| | | width: 32px; |
| | | height: 32px; |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | height: 17.5px; |
| | | } |
| | | } |
| | | } |
| | | } |
| | |
| | | import { getOrderDistance } from '../../../api/select-map-site'; |
| | | import { getDistance } from '../../../../utils/Robotaxi-public-method'; |
| | | import { ensureSelectedCityByLocation } from '../../../../robotaxi/utils/city'; |
| | | import { |
| | | ORDER_DISTANCE_TIP_TYPE_MAX_DISTANCE, |
| | | ORDER_DISTANCE_TIP_TYPE_MIN_DISTANCE |
| | | } from '../../../constants/order-distance'; |
| | | |
| | | type OrderDistanceConfig = { |
| | | maxDistance?: number; |
| | |
| | | const loc = await Taro.getLocation({ type: 'gcj02' }); |
| | | const distance = getDistance(loc.latitude, loc.longitude, startSite.latitude, startSite.longitude); |
| | | if (distance > maxDistance) { |
| | | deps.setTipType('2'); |
| | | deps.setTipType(ORDER_DISTANCE_TIP_TYPE_MAX_DISTANCE); |
| | | deps.setShowTipPopup(true); |
| | | return false; |
| | | } |
| | |
| | | if (minDistance) { |
| | | const distance = getDistance(startSite.latitude, startSite.longitude, deps.endSite.latitude, deps.endSite.longitude); |
| | | if (distance < minDistance) { |
| | | deps.setTipType('1'); |
| | | deps.setTipType(ORDER_DISTANCE_TIP_TYPE_MIN_DISTANCE); |
| | | deps.setShowTipPopup(true); |
| | | return false; |
| | | } |
| | |
| | | waitingFeePerMinute !== null && |
| | | waitingFeePerMinute !== undefined && |
| | | waitingFeePerMinute !== '' |
| | | ? `司机到达预约地点${waitingFreeMinutes}分钟后开始收取等待费,为${formatMoney(waitingFeePerMinute)}元/分钟,不足一分钟按一分钟计算` |
| | | ? `司机到达预约地点${waitingFreeMinutes}分钟后开始收取等待费,为${formatMoney(waitingFeePerMinute)}元/分钟,不足1分钟按1分钟计算` |
| | | : ''; |
| | | |
| | | const longDistanceStep1 = basicPricing?.num7; |
| | |
| | | } from '../../../../utils/Robotaxi-public-method'; |
| | | import { getGeofence } from '../../../api/home'; |
| | | import { getOrderDistance } from '../../../api/select-map-site'; |
| | | import { |
| | | ensureOrderDistanceConfig, |
| | | isWithinPickupMaxDistance, |
| | | isWithinStartEndMinDistance, |
| | | normalizeOrderDistanceConfig |
| | | } from '../../../utils/order-distance'; |
| | | import { collectPoint } from '../../../api/site-search'; |
| | | import { checkOtp } from '../../../../utils/tools'; |
| | | import { jumpTo } from '../../../../utils/jump'; |
| | |
| | | writeConfirmRideSiteSourceType |
| | | } from '../../../constants/confirm-ride-report'; |
| | | import { GeofenceType } from '../../../constants/geofence'; |
| | | import { |
| | | ORDER_DISTANCE_TIP_TYPE_MAX_DISTANCE, |
| | | ORDER_DISTANCE_TIP_TYPE_MIN_DISTANCE |
| | | } from '../../../constants/order-distance'; |
| | | |
| | | const AUTO_SCALE_MAX = 17; // 自动缩放最大等级 |
| | | const AUTO_SCALE_MIN = 11; // 自动缩放最小等级 |
| | |
| | | }; |
| | | |
| | | /** |
| | | * 获取距离配置,避免用户快速确认时使用到未初始化完成的配置。 |
| | | */ |
| | | const ensureDistanceConfigReady = async () => ensureOrderDistanceConfig({ |
| | | currentConfig: state.distanceConfig, |
| | | fetchConfig: getOrderDistance, |
| | | onConfigResolved: state.setDistanceConfig |
| | | }); |
| | | |
| | | /** |
| | | * 确认选择站点(包含起终点距离与电子围栏校验)。 |
| | | */ |
| | | const confirmSite = async () => { |
| | |
| | | return; |
| | | } |
| | | if (state.pageType === 'start') { |
| | | const distanceConfig = await ensureDistanceConfigReady(); |
| | | const endSiteCache = Taro.getStorageSync('endSite'); |
| | | if (endSiteCache?.electronicAreaId && state.selectedSite?.electronicAreaId && endSiteCache.electronicAreaId !== state.selectedSite.electronicAreaId) { |
| | | Taro.showToast({ title: '请在同一运营区内选择', icon: 'none' }); |
| | | return; |
| | | } |
| | | if (state.userLatitude && state.userLongitude) { |
| | | const distance = getDistance(state.userLatitude, state.userLongitude, state.selectedSite.latitude, state.selectedSite.longitude); |
| | | if (distance > state.distanceConfig.maxDistance) { |
| | | state.setTipType('2'); |
| | | state.setShowTipPopup(true); |
| | | return; |
| | | } |
| | | if (!isWithinPickupMaxDistance({ |
| | | userLatitude: state.userLatitude, |
| | | userLongitude: state.userLongitude, |
| | | pickupSite: state.selectedSite, |
| | | maxDistance: distanceConfig?.maxDistance |
| | | })) { |
| | | state.setTipType(ORDER_DISTANCE_TIP_TYPE_MAX_DISTANCE); |
| | | state.setShowTipPopup(true); |
| | | return; |
| | | } |
| | | if (!isWithinStartEndMinDistance({ |
| | | startSite: state.selectedSite, |
| | | endSite: endSiteCache, |
| | | minDistance: distanceConfig?.minDistance |
| | | })) { |
| | | state.setTipType(ORDER_DISTANCE_TIP_TYPE_MIN_DISTANCE); |
| | | state.setShowTipPopup(true); |
| | | return; |
| | | } |
| | | reportConfirmMapSiteEvent(); |
| | | Taro.setStorageSync('startSite', state.selectedSite); |
| | |
| | | jumpTo({ url: '/pages/user/login?hideBack=1' }); |
| | | return; |
| | | } |
| | | const distanceConfig = await ensureDistanceConfigReady(); |
| | | const startSiteCache = Taro.getStorageSync('startSite'); |
| | | if (startSiteCache?.electronicAreaId && state.selectedSite?.electronicAreaId && startSiteCache.electronicAreaId !== state.selectedSite.electronicAreaId) { |
| | | Taro.showToast({ title: '请在同一运营区内选择', icon: 'none' }); |
| | | return; |
| | | } |
| | | if (startSiteCache && startSiteCache.longitude && startSiteCache.latitude) { |
| | | const distance = getDistance(startSiteCache.latitude, startSiteCache.longitude, state.selectedSite.latitude, state.selectedSite.longitude); |
| | | if (distance < state.distanceConfig.minDistance) { |
| | | state.setTipType('1'); |
| | | state.setShowTipPopup(true); |
| | | return; |
| | | } |
| | | if (!isWithinStartEndMinDistance({ |
| | | startSite: startSiteCache, |
| | | endSite: state.selectedSite, |
| | | minDistance: distanceConfig?.minDistance |
| | | })) { |
| | | state.setTipType(ORDER_DISTANCE_TIP_TYPE_MIN_DISTANCE); |
| | | state.setShowTipPopup(true); |
| | | return; |
| | | } |
| | | reportConfirmMapSiteEvent(); |
| | | const modifySession = Taro.getStorageSync(MODIFY_DESTINATION_SESSION_KEY); // 修改目的地会话中将终点写入候选缓存,返回行程页进入预览态 |
| | |
| | | |
| | | getOrderDistance().then((res: any) => { |
| | | if (res) { |
| | | state.setDistanceConfig({ |
| | | maxDistance: res.maxDistance, |
| | | minDistance: res.minDistance, |
| | | adsorptionDistance: res.adsorptionDistance |
| | | }); |
| | | state.setDistanceConfig(normalizeOrderDistanceConfig(res)); |
| | | } |
| | | }).catch((err) => { |
| | | console.error('获取距离配置失败', err); |
| | |
| | | useAndroidNativeTrip1Callout: boolean |
| | | ) => { |
| | | if (!(pickupSite?.latitude && pickupSite?.longitude)) return null; |
| | | if (deps.isAndroid && [TripStatus.VehicleArrivedPickup, TripStatus.InService].includes(deps.tripStatus)) { |
| | | if (deps.isAndroid && deps.tripStatus === TripStatus.InService) { |
| | | return null; |
| | | } |
| | | return { |
| | |
| | | ) => { |
| | | if (!(dropoffSite?.latitude && dropoffSite?.longitude)) return null; |
| | | // 分享行程只有从 5/6 及之后的状态才渲染下车点,避免 4 态提前入列引发地图多次缩放。 |
| | | if (deps.tripStatus < TripStatus.VehicleArrivedPickup) return null; |
| | | if (deps.tripStatus <= TripStatus.VehicleArrivedPickup) return null; |
| | | const dropoffName = |
| | | [TripStatus.ArrivedDestination].includes(deps.tripStatus) ? '已到达' : dropoffSite.name || '目的地'; |
| | | return { |
| | |
| | | const shouldWaitArrivedDirection = deps.tripStatus === TripStatus.ArrivedDestination && !deps.arrivedMarkerReady; |
| | | const effectiveShowCar = deps.statusRule.showCar && !shouldWaitArrivedDirection; |
| | | const shouldUseAndroidDropoffOnly = |
| | | deps.isAndroid && [TripStatus.VehicleArrivedPickup, TripStatus.InService].includes(deps.tripStatus); |
| | | deps.isAndroid && deps.tripStatus === TripStatus.InService; |
| | | const carMarker = createCarMarker(); |
| | | const nextMarkers: any[] = []; |
| | | if (!shouldUseAndroidDropoffOnly && deps.statusRule.showPickup && pickupMarker) nextMarkers.push(pickupMarker); |
| | |
| | | keyParts.push(pickupSite?.latitude, pickupSite?.longitude); |
| | | return { points, keyParts }; |
| | | } |
| | | if (viewportType === "pickup_center") { |
| | | const pickupPoint = buildSiteViewportPoint(pickupSite); |
| | | if (pickupPoint) points.push(pickupPoint); |
| | | keyParts.push(pickupSite?.latitude, pickupSite?.longitude); |
| | | return { points, keyParts }; |
| | | } |
| | | if (viewportType === "route") { |
| | | if (deps.routePoints.length > 0) points.push(...deps.routePoints); |
| | | keyParts.push(buildPointsSignature(deps.routePoints)); |
| | |
| | | : [topPadding, 50, bottomPadding, 50]; |
| | | holdDynamicScale(); |
| | | Taro.nextTick(() => { |
| | | if (viewportType === "pickup_center") { |
| | | if (activeTripViewportSyncTimerRef.current) { |
| | | clearTimeout(activeTripViewportSyncTimerRef.current); |
| | | activeTripViewportSyncTimerRef.current = null; |
| | | } |
| | | mapCtx.moveToLocation({ |
| | | latitude: points[0].latitude, |
| | | longitude: points[0].longitude |
| | | }); |
| | | activeTripViewportSyncTimerRef.current = setTimeout(() => { |
| | | if (typeof deps.setLatitude === "function") { |
| | | deps.setLatitude(points[0].latitude); |
| | | } |
| | | if (typeof deps.setLongitude === "function") { |
| | | deps.setLongitude(points[0].longitude); |
| | | } |
| | | activeTripViewportSyncTimerRef.current = null; |
| | | }, ACTIVE_TRIP_VIEWPORT_SYNC_ANIMATION_MS); |
| | | return; |
| | | } |
| | | if (viewportType === "dropoff") { |
| | | mapCtx.moveToLocation({ |
| | | latitude: points[0].latitude, |
| | |
| | | if (![TripStatus.VehicleApproaching, TripStatus.InService].includes(deps.tripStatus)) { |
| | | lastActiveTripViewportSyncAtRef.current = 0; |
| | | activeTripViewportSyncingRef.current = false; |
| | | if (activeTripViewportSyncTimerRef.current) { |
| | | clearTimeout(activeTripViewportSyncTimerRef.current); |
| | | activeTripViewportSyncTimerRef.current = null; |
| | | } |
| | | return; |
| | | } |
| | | syncActiveTripViewportToVehicle(); |
| | |
| | | ]); |
| | | // 路线态统一交给 includePoints 计算视口,避免先写一次几何中心,再被视口纠正,出现地图“先偏后回”的抖动。 |
| | | // 这里只保留无路线视口的轻量兜底。 |
| | | if (center && [TripStatus.Publishing, TripStatus.CallingCar, TripStatus.VehicleArrivedPickup].includes(mappedStatus)) { |
| | | if (center && [TripStatus.Publishing, TripStatus.CallingCar].includes(mappedStatus)) { |
| | | deps.setLongitude(center.longitude); |
| | | deps.setLatitude(center.latitude); |
| | | } |
| | |
| | | stopPolling(); |
| | | return; |
| | | } |
| | | if ([TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(infoState.mappedStatus)) { |
| | | const routeResponse = unwrapSilentResponse( |
| | | await requestShareRouteByStatus(infoState.mappedStatus) |
| | | ); |
| | | if (cancelled) return; |
| | | syncShareRoute(routeResponse, infoState.mappedStatus, infoState.nextStartSite, infoState.nextEndSite); |
| | | stopPolling(); |
| | | errorToastShownRef.current = false; |
| | | return; |
| | | } |
| | | const shouldContinuePolling = await fetchShareTripData(); |
| | | if (cancelled) return; |
| | | if (shouldContinuePolling) { |
| | |
| | | import { CoverImage, CoverView, Image, View } from '@tarojs/components'; |
| | | import { memo, useCallback, useMemo } from 'react'; |
| | | import { formatDurationDisplay } from '../../../../utils/Robotaxi-public-method'; |
| | | import { TripStatus } from '../../../constants/trip-status'; |
| | | import { formatRemainingDurationDisplay } from '../../../utils/format'; |
| | | |
| | | type Deps = { |
| | | isAndroid: boolean; |
| | |
| | | if (!hasRouteDuration) { |
| | | return { value: '--', unit: '' }; |
| | | } |
| | | return formatDurationDisplay(deps.routeDuration as number); |
| | | return formatRemainingDurationDisplay(deps.routeDuration as number); |
| | | }, [deps.routeDuration, hasRouteDuration]); |
| | | // 与 begin-route 保持一致:车辆气泡与路线摘要不足 1 分钟时统一展示 1 分钟。 |
| | | const vehicleMetricDurationDisplay = useMemo<DurationDisplay>(() => ( |
| | | durationDisplay.unit === '秒' |
| | | ? { value: 1, unit: '分钟' } |
| | | : durationDisplay |
| | | ), [durationDisplay]); |
| | | const shouldHideAndroidMarkerCallout = deps.isAndroid |
| | | && [TripStatus.PublishRoute, TripStatus.VehicleArrivedPickup, TripStatus.TripFinished, TripStatus.PaymentSuccess, TripStatus.CancelOnlyPickup].includes(deps.tripStatus); |
| | | const androidCarCalloutContent = useMemo(() => ( |
| | | <AndroidCarCalloutContent |
| | | routeDistance={deps.routeDistance} |
| | | durationDisplay={durationDisplay} |
| | | durationDisplay={vehicleMetricDurationDisplay} |
| | | /> |
| | | ), [deps.routeDistance, durationDisplay]); |
| | | ), [deps.routeDistance, vehicleMetricDurationDisplay]); |
| | | const routeSummaryText = useMemo(() => ( |
| | | deps.routeDistance && hasRouteDuration |
| | | ? `${durationDisplay.value}${durationDisplay.unit} ${deps.routeDistance.value}${deps.routeDistance.unit === 'm' ? '米' : '公里'}` |
| | | ? `${vehicleMetricDurationDisplay.value}${vehicleMetricDurationDisplay.unit} ${deps.routeDistance.value}${deps.routeDistance.unit === 'm' ? '米' : '公里'}` |
| | | : '' |
| | | ), [deps.routeDistance, durationDisplay, hasRouteDuration]); |
| | | ), [deps.routeDistance, hasRouteDuration, vehicleMetricDurationDisplay]); |
| | | |
| | | const renderWaitingCallout = useCallback((item: any, calloutKey: string) => ( |
| | | <CoverView slot='callout' marker-id={item.id} key={calloutKey} className='waiting-callout'> |
| | |
| | | <CoverView className='car-time'> |
| | | <CoverView className='car-time-num'>{deps.routeDistance?.value ?? 0}</CoverView> |
| | | <CoverView>{deps.routeDistance?.unit === 'm' ? '米' : '公里'}</CoverView> |
| | | <CoverView style={{ marginLeft: '2.5px' }} className='car-time-num'>{durationDisplay.value}</CoverView> |
| | | <CoverView>{durationDisplay.unit}</CoverView> |
| | | <CoverView style={{ marginLeft: '2.5px' }} className='car-time-num'>{vehicleMetricDurationDisplay.value}</CoverView> |
| | | <CoverView>{vehicleMetricDurationDisplay.unit}</CoverView> |
| | | </CoverView> |
| | | </CoverView> |
| | | )} |
| | |
| | | deps.isAndroid, |
| | | deps.routeDistance, |
| | | deps.tripStatus, |
| | | durationDisplay, |
| | | routeSummaryText, |
| | | vehicleMetricDurationDisplay, |
| | | ]); |
| | | |
| | | const renderMarkerCallout = useCallback((item: any, index: number) => { |
| | |
| | | [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.VehicleArrivedPickup]: { showPickup: true, showDropoff: false, showCar: true, showRoute: false, showWalking: false, viewport: 'pickup_center', 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: true, showWalking: false, viewport: 'order_detail', translateCar: false }, |
| | | [TripStatus.TripFinished]: { showPickup: true, showDropoff: true, showCar: false, showRoute: true, showWalking: false, viewport: 'order_detail', translateCar: false }, |
| | |
| | | import { useState, useEffect, useRef } from 'react'; |
| | | import { View, Text, Image, Input } from '@tarojs/components'; |
| | | import './index.less'; |
| | | import { getDistance, reportEvent, toUnixSeconds } from '../../../utils/Robotaxi-public-method'; |
| | | import { reportEvent, toUnixSeconds } from '../../../utils/Robotaxi-public-method'; |
| | | import { getOrderDistance } from '../../api/select-map-site'; |
| | | import { |
| | | ensureOrderDistanceConfig, |
| | | isWithinPickupMaxDistance, |
| | | isWithinStartEndMinDistance, |
| | | normalizeOrderDistanceConfig |
| | | } from '../../utils/order-distance'; |
| | | import { checkOtp } from '../../../utils/tools'; |
| | | import TipPopup from '../../components/tip-popup'; |
| | | import SiteListItem from './components/site-list-item'; |
| | |
| | | HOME_START_SITE_SOURCE_MANUAL_SELECTED, |
| | | writeHomeStartSiteDisplayState |
| | | } from '../../hooks/use-home-start-site-display'; |
| | | import { |
| | | ORDER_DISTANCE_TIP_TYPE_MAX_DISTANCE, |
| | | ORDER_DISTANCE_TIP_TYPE_MIN_DISTANCE |
| | | } from '../../constants/order-distance'; |
| | | import { |
| | | HISTORY_CONFIRM_RIDE_SOURCE_TYPE, |
| | | RECOMMENDED_CONFIRM_RIDE_SOURCE_TYPE, |
| | |
| | | useEffect(() => { |
| | | getOrderDistance().then((res: any) => { |
| | | if (res) { |
| | | setDistanceConfig({ |
| | | maxDistance: res.maxDistance, |
| | | minDistance: res.minDistance, |
| | | adsorptionDistance: res.adsorptionDistance |
| | | }); |
| | | setDistanceConfig(normalizeOrderDistanceConfig(res)); |
| | | } |
| | | }).catch(() => { }); |
| | | }, []); |
| | | |
| | | /** |
| | | * 确保搜索页距离配置可用,避免快速确认时漏掉校验。 |
| | | */ |
| | | const ensureDistanceConfigReady = async () => ensureOrderDistanceConfig({ |
| | | currentConfig: distanceConfig, |
| | | fetchConfig: getOrderDistance, |
| | | onConfigResolved: setDistanceConfig |
| | | }); |
| | | |
| | | // 处理终点选择后的业务校验与跳转 |
| | | const handleEndSiteSelection = async ( |
| | | item: any, |
| | | sourceType = UNKNOWN_CONFIRM_RIDE_SOURCE_TYPE |
| | | ) => { |
| | | const resolvedDistanceConfig = await ensureDistanceConfigReady(); |
| | | const startSiteCache = Taro.getStorageSync('startSite'); |
| | | if (startSiteCache?.electronicAreaId && item?.electronicAreaId && startSiteCache.electronicAreaId !== item.electronicAreaId) { |
| | | Taro.showToast({ |
| | |
| | | return |
| | | } |
| | | try { |
| | | let minDistance = distanceConfig?.minDistance; |
| | | if (!distanceConfig?.minDistance) { |
| | | const res = await getOrderDistance(); |
| | | if (res) { |
| | | minDistance = res.minDistance; |
| | | setDistanceConfig({ |
| | | maxDistance: res.maxDistance, |
| | | minDistance: res.minDistance, |
| | | adsorptionDistance: res.adsorptionDistance, |
| | | geo_lat: item.latitude, |
| | | geo_lng: item.longitude, |
| | | odd_grid_id: item.electronicAreaId, |
| | | timestamp: Date.now(), |
| | | }); |
| | | } |
| | | } |
| | | if (startSiteCache && startSiteCache.longitude && startSiteCache.latitude) { |
| | | const distance = getDistance(startSiteCache.latitude, startSiteCache.longitude, item.latitude, item.longitude); |
| | | if (minDistance && distance < minDistance) { |
| | | setTipType('1'); |
| | | setShowTipPopup(true); |
| | | return; |
| | | } |
| | | if (!isWithinStartEndMinDistance({ |
| | | startSite: startSiteCache, |
| | | endSite: item, |
| | | minDistance: resolvedDistanceConfig?.minDistance |
| | | })) { |
| | | setTipType(ORDER_DISTANCE_TIP_TYPE_MIN_DISTANCE); |
| | | setShowTipPopup(true); |
| | | return; |
| | | } |
| | | } catch { } |
| | | const modifySession = Taro.getStorageSync(MODIFY_DESTINATION_SESSION_KEY); |
| | |
| | | return |
| | | } |
| | | if (pageType == 'start') { |
| | | const resolvedDistanceConfig = await ensureDistanceConfigReady(); |
| | | const endSiteCache = Taro.getStorageSync('endSite'); |
| | | if (endSiteCache?.electronicAreaId && item?.electronicAreaId && endSiteCache.electronicAreaId !== item.electronicAreaId) { |
| | | Taro.showToast({ |
| | |
| | | return |
| | | } |
| | | try { |
| | | if (!distanceConfig?.maxDistance) { |
| | | const res = await getOrderDistance(); |
| | | if (res) { |
| | | setDistanceConfig({ |
| | | maxDistance: res.maxDistance, |
| | | minDistance: res.minDistance, |
| | | adsorptionDistance: res.adsorptionDistance |
| | | }); |
| | | } |
| | | } |
| | | const loc = await Taro.getLocation({ type: 'gcj02' }); |
| | | const distance = getDistance(loc.latitude, loc.longitude, item.latitude, item.longitude); |
| | | if (distanceConfig?.maxDistance && distance > distanceConfig.maxDistance) { |
| | | setTipType('2'); |
| | | if (!isWithinPickupMaxDistance({ |
| | | userLatitude: loc.latitude, |
| | | userLongitude: loc.longitude, |
| | | pickupSite: item, |
| | | maxDistance: resolvedDistanceConfig?.maxDistance |
| | | })) { |
| | | setTipType(ORDER_DISTANCE_TIP_TYPE_MAX_DISTANCE); |
| | | setShowTipPopup(true); |
| | | return; |
| | | } |
| | | if (!isWithinStartEndMinDistance({ |
| | | startSite: item, |
| | | endSite: endSiteCache, |
| | | minDistance: resolvedDistanceConfig?.minDistance |
| | | })) { |
| | | setTipType(ORDER_DISTANCE_TIP_TYPE_MIN_DISTANCE); |
| | | setShowTipPopup(true); |
| | | return; |
| | | } |
| New file |
| | |
| | | import { getDistance } from '../../utils/Robotaxi-public-method'; |
| | | |
| | | /** 下单距离配置。 */ |
| | | export type OrderDistanceConfig = { |
| | | /** 用户到起点的最大距离。 */ |
| | | maxDistance?: number; |
| | | /** 起终点之间的最小距离。 */ |
| | | minDistance?: number; |
| | | /** 点位吸附距离。 */ |
| | | adsorptionDistance?: number; |
| | | }; |
| | | |
| | | type CoordinateSite = { |
| | | latitude?: number | string; |
| | | longitude?: number | string; |
| | | }; |
| | | |
| | | /** |
| | | * 统一将下单距离配置归一化为数字,避免字符串配置影响校验。 |
| | | */ |
| | | export const normalizeOrderDistanceConfig = (config: any): OrderDistanceConfig => ({ |
| | | maxDistance: Number.isFinite(Number(config?.maxDistance)) ? Number(config.maxDistance) : undefined, |
| | | minDistance: Number.isFinite(Number(config?.minDistance)) ? Number(config.minDistance) : undefined, |
| | | adsorptionDistance: Number.isFinite(Number(config?.adsorptionDistance)) ? Number(config.adsorptionDistance) : undefined |
| | | }); |
| | | |
| | | /** |
| | | * 确保下单距离配置可用,必要时拉取最新配置并回写页面状态。 |
| | | */ |
| | | export const ensureOrderDistanceConfig = async (params: { |
| | | currentConfig: any; |
| | | fetchConfig: () => Promise<any>; |
| | | onConfigResolved?: (config: OrderDistanceConfig) => void; |
| | | }) => { |
| | | const normalizedCurrentConfig = normalizeOrderDistanceConfig(params.currentConfig); |
| | | if ( |
| | | Number.isFinite(normalizedCurrentConfig.maxDistance) |
| | | && Number.isFinite(normalizedCurrentConfig.minDistance) |
| | | ) { |
| | | return normalizedCurrentConfig; |
| | | } |
| | | const response = await params.fetchConfig(); |
| | | const normalizedNextConfig = normalizeOrderDistanceConfig(response); |
| | | params.onConfigResolved?.(normalizedNextConfig); |
| | | return normalizedNextConfig; |
| | | }; |
| | | |
| | | /** |
| | | * 判断站点是否具备有效经纬度。 |
| | | */ |
| | | export const hasValidSiteCoordinate = (site: CoordinateSite | null | undefined) => ( |
| | | Number.isFinite(Number(site?.latitude)) && Number.isFinite(Number(site?.longitude)) |
| | | ); |
| | | |
| | | /** |
| | | * 校验用户当前位置到起点的最大允许距离。 |
| | | */ |
| | | export const isWithinPickupMaxDistance = (params: { |
| | | userLatitude?: number | null; |
| | | userLongitude?: number | null; |
| | | pickupSite?: CoordinateSite | null; |
| | | maxDistance?: number; |
| | | }) => { |
| | | if ( |
| | | !Number.isFinite(Number(params.userLatitude)) |
| | | || !Number.isFinite(Number(params.userLongitude)) |
| | | || !hasValidSiteCoordinate(params.pickupSite) |
| | | || !Number.isFinite(Number(params.maxDistance)) |
| | | ) { |
| | | return true; |
| | | } |
| | | const distance = getDistance( |
| | | Number(params.userLatitude), |
| | | Number(params.userLongitude), |
| | | Number(params.pickupSite?.latitude), |
| | | Number(params.pickupSite?.longitude) |
| | | ); |
| | | return distance <= Number(params.maxDistance); |
| | | }; |
| | | |
| | | /** |
| | | * 校验起终点是否满足最短下单距离。 |
| | | */ |
| | | export const isWithinStartEndMinDistance = (params: { |
| | | startSite?: CoordinateSite | null; |
| | | endSite?: CoordinateSite | null; |
| | | minDistance?: number; |
| | | }) => { |
| | | if ( |
| | | !hasValidSiteCoordinate(params.startSite) |
| | | || !hasValidSiteCoordinate(params.endSite) |
| | | || !Number.isFinite(Number(params.minDistance)) |
| | | ) { |
| | | return true; |
| | | } |
| | | const distance = getDistance( |
| | | Number(params.startSite?.latitude), |
| | | Number(params.startSite?.longitude), |
| | | Number(params.endSite?.latitude), |
| | | Number(params.endSite?.longitude) |
| | | ); |
| | | return distance >= Number(params.minDistance); |
| | | }; |