feat(robotaxi): 新增上一单终点标记及车辆气泡优化
- 在路线管理器中解析并存储上一单乘客下车点坐标
- 在地图标记中新增上一单终点标记及其自定义气泡
- 调整车辆气泡布局,在存在上一单终点时显示“即将完成上一单”提示
- 优化温度控制组件,仅在车辆接近状态显示并展示实际温度值
- 调整样式细节,包括字体大小和气泡定位
| | |
| | | }); |
| | | }; |
| | | |
| | | // if (tripStatus !== TripStatus.VehicleApproaching || typeof tempStatus !== 'number') { |
| | | // return null; |
| | | // } |
| | | |
| | | if (tripStatus !== TripStatus.VehicleApproaching || typeof tempStatus !== 'number') { |
| | | return null; |
| | | } |
| | | //TODO: 温度控制等待接口开发完成后补全前端功能:远程开关、查车当前空调状态 |
| | | return ( |
| | | <View> |
| | | <View className="car-card-temp"> |
| | |
| | | } |
| | | </View> |
| | | <View className="car-card-temp-num-text"> |
| | | <View className="car-card-temp-num-text-temp color-000-25">20℃</View> |
| | | <View className="car-card-temp-num-text-temp color-000-25">{tempStatus?.toFixed(1)}°℃</View> |
| | | <View className="car-card-temp-num-text-desc color-000-25">设定温度</View> |
| | | </View> |
| | | <View className="car-card-temp-num-btn"> |
| | |
| | | showTrip1StyleEndpoint: boolean; |
| | | startSite: any; |
| | | endSite: any; |
| | | beforeUserEndSite: { longitude: number; latitude: number } | null; |
| | | vehicleLocation: { latitude: number; longitude: number } | null; |
| | | vehicleDirection: number | null; |
| | | vehicleMarkerId: number; |
| | |
| | | showTrip1StyleEndpoint: false, |
| | | startSite: null, |
| | | endSite: null, |
| | | beforeUserEndSite: null, |
| | | vehicleLocation: null, |
| | | vehicleDirection: null, |
| | | vehicleMarkerId: 3, |
| | |
| | | }; |
| | | }; |
| | | |
| | | // 构建上一单终点 marker |
| | | const createBeforeUserDropoffMarker = () => { |
| | | const latitude = Number(deps.beforeUserEndSite?.latitude); |
| | | const longitude = Number(deps.beforeUserEndSite?.longitude); |
| | | if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) return null; |
| | | return { |
| | | id: 31, |
| | | latitude, |
| | | longitude, |
| | | iconPath: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/before-user-site.png', |
| | | width: 22, |
| | | height: 36, |
| | | anchor: { x: 0.5, y: 1 }, |
| | | zIndex: 1, |
| | | name: '上一单终点', |
| | | isBeforeUserSite: true, |
| | | customCallout: { anchorY: -5, anchorX: 0, display: 'ALWAYS' } |
| | | }; |
| | | }; |
| | | |
| | | // 构建车辆 marker |
| | | const createCarMarker = () => { |
| | | if (!deps.vehicleLocation) return null; |
| | |
| | | const useAndroidNativeTrip1Callout = deps.isAndroid && ([TripStatus.PublishRoute, TripStatus.VehicleArrivedPickup, TripStatus.TripFinished, TripStatus.PaymentSuccess, TripStatus.CancelOnlyPickup].includes(deps.tripStatus) || deps.showTrip1StyleEndpoint); |
| | | const pickupMarker = createPickupMarker(pickupSite, useAndroidNativeTrip1Callout); |
| | | const dropoffMarker = createDropoffMarker(dropoffSite, useAndroidNativeTrip1Callout); |
| | | const beforeUserDropoffMarker = createBeforeUserDropoffMarker(); |
| | | const shouldWaitArrivedDirection = deps.tripStatus === TripStatus.ArrivedDestination && !deps.arrivedMarkerReady; |
| | | const effectiveShowCar = deps.statusRule.showCar && !shouldWaitArrivedDirection; |
| | | const carMarker = createCarMarker(); |
| | |
| | | if (deps.statusRule.showPickup && pickupMarker) nextMarkers.push(pickupMarker); |
| | | if (effectiveShowCar && carMarker) nextMarkers.push(carMarker); |
| | | if (deps.statusRule.showDropoff && dropoffMarker) nextMarkers.push(dropoffMarker); |
| | | if (beforeUserDropoffMarker) nextMarkers.push(beforeUserDropoffMarker); |
| | | const nextMarkersSignature = nextMarkers |
| | | .map((m: any) => [ |
| | | m.id, |
| | |
| | | const shouldHoldMarkerByTranslate = [TripStatus.VehicleApproaching, TripStatus.InService].includes(deps.tripStatus) && carMarkerInitialized.current && deps.statusRule.showCar && (!deps.statusRule.showDropoff || hasDropoffMarker); |
| | | const shouldSkipUpdate = shouldHoldMarkerByTranslate && (deps.isAndroid || tripStatus6MarkerRefreshedRef.current); |
| | | applyMarkerState(nextMarkers, nextMarkersSignature, shouldSkipUpdate, carMarker); |
| | | }, [deps.tripStatus, deps.startSite?.name, deps.startSite?.latitude, deps.startSite?.longitude, deps.endSite?.name, deps.endSite?.latitude, deps.endSite?.longitude, deps.vehicleLocation, deps.vehicleDirection, deps.vehicleMarkerId, deps.isAndroid, deps.statusRule.showPickup, deps.statusRule.showCar, deps.statusRule.showDropoff]); |
| | | }, [deps.tripStatus, deps.startSite?.name, deps.startSite?.latitude, deps.startSite?.longitude, deps.endSite?.name, deps.endSite?.latitude, deps.endSite?.longitude, deps.beforeUserEndSite?.latitude, deps.beforeUserEndSite?.longitude, deps.vehicleLocation, deps.vehicleDirection, deps.vehicleMarkerId, deps.isAndroid, deps.statusRule.showPickup, deps.statusRule.showCar, deps.statusRule.showDropoff]); |
| | | |
| | | return { |
| | | markers, |
| | |
| | | endSite?: any; |
| | | setStartSite?: (site: any) => void; |
| | | setEndSite?: (site: any) => void; |
| | | setBeforeUserEndSite?: (site: { longitude: number; latitude: number } | null) => void; |
| | | resolveSafeTopInset: () => number; |
| | | computeBottomPadding: (points: any[], viewportType: string) => number; |
| | | updateVehicleLocationThrottled: (lat: number, lng: number, force?: boolean) => void; |
| | |
| | | computeBottomPadding: () => 280, |
| | | updateVehicleLocationThrottled: () => { }, |
| | | setVehicleDirection: () => { } |
| | | }; |
| | | |
| | | // 解析“上一单乘客下车点”坐标,任一坐标无效则返回空。 |
| | | const parseBeforeUserEndSite = (endLongitudeValue: any, endLatitudeValue: any) => { |
| | | const longitude = Number(endLongitudeValue); |
| | | const latitude = Number(endLatitudeValue); |
| | | if (!Number.isFinite(longitude) || !Number.isFinite(latitude)) { |
| | | return null; |
| | | } |
| | | return { longitude, latitude }; |
| | | }; |
| | | |
| | | export default function useRouteManager(rawDeps?: Partial<Deps>) { |
| | |
| | | const requestRoute = [TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(status) ? queryOrderTrajectory : queryOrderRoute; |
| | | requestRoute({ orderId: targetOrderId, routeType }) |
| | | .then((res: any) => { |
| | | const beforeUserSite = parseBeforeUserEndSite(res?.endLongitude, res?.endlatitude); |
| | | deps.setBeforeUserEndSite?.(beforeUserSite); |
| | | const rawPoints = Array.isArray(res?.polyline) ? res.polyline : []; |
| | | const points = rawPoints |
| | | .map((item: any) => ({ |
| | |
| | | routeDistance: { value: number; unit: 'km' | 'm' } | null; |
| | | /** 路线时长 */ |
| | | routeDuration: number | null; |
| | | /** 是否存在上一单终点 */ |
| | | hasBeforeUserEndSite: boolean; |
| | | /** 自定义气泡图标 */ |
| | | customCalloutIconSrc: string; |
| | | /** 是否使用下单前端点气泡样式 */ |
| | |
| | | const routeSummaryText = deps.routeDistance && deps.routeDuration !== null |
| | | ? `${durationDisplay.value}${durationDisplay.unit} ${deps.routeDistance.value}${deps.routeDistance.unit === 'm' ? '米' : '公里'}` |
| | | : ''; |
| | | const shouldHideAndroidMarkerCallout = deps.isAndroid && [TripStatus.PublishRoute, TripStatus.VehicleArrivedPickup, TripStatus.TripFinished, TripStatus.PaymentSuccess, TripStatus.CancelOnlyPickup].includes(deps.tripStatus); // Android 指定状态下隐藏常规 marker 气泡 |
| | | |
| | | /** |
| | | * 渲染等待态气泡。 |
| | |
| | | <CoverView className='route-summary-callout-arrow' /> |
| | | </CoverView> |
| | | ) : null |
| | | ) : item.isBeforeUserSite ? ( |
| | | <CoverView className='before-user-site-callout'> |
| | | <CoverView className='before-user-site-txt'>上一站终点</CoverView> |
| | | </CoverView> |
| | | ) : ( |
| | | <> |
| | | {(item.name !== '车辆位置' && !(item.id === 2 && [TripStatus.ArrivedDestination].includes(deps.tripStatus))) && ( |
| | |
| | | </CoverView> |
| | | )} |
| | | {(item.name === '车辆位置' && [TripStatus.VehicleApproaching, TripStatus.InService].includes(deps.tripStatus) && !deps.isAndroid) && ( |
| | | <CoverView className='custom-callout-content' style={{ padding: '6px 10px', minWidth: '40px', justifyContent: 'center', zIndex: 2 }}> |
| | | <CoverView className='custom-callout-content car-callout-with-before' style={{ padding: '6px 10px', minWidth: '40px', justifyContent: 'center', zIndex: 2 }}> |
| | | {deps.hasBeforeUserEndSite && ( |
| | | <CoverView className='car-time car-before-order-tip'>即将完成上一单</CoverView> |
| | | )} |
| | | <CoverView className='car-time'> |
| | | <CoverView className='car-time-num'>{deps.routeDistance?.value ?? 0}</CoverView> |
| | | <CoverView>{deps.routeDistance?.unit === 'm' ? '米' : '公里'}</CoverView> |
| | |
| | | * 渲染单个 marker 气泡。 |
| | | */ |
| | | const renderMarkerCallout = (item: any, index: number) => { |
| | | if (shouldHideAndroidMarkerCallout && !item.isBeforeUserSite) return null; |
| | | const calloutKey = `callout-${item.id}-${item.name || ''}`; |
| | | if (item.isRouteSummaryBubble) return renderDefaultCallout(item, calloutKey); |
| | | const isWaiting = [TripStatus.Publishing, TripStatus.CallingCar].includes(deps.tripStatus) && index === 0; |
| | |
| | | */ |
| | | const renderMapOverlays = () => ( |
| | | <> |
| | | {!(deps.isAndroid && [TripStatus.PublishRoute, TripStatus.VehicleArrivedPickup, TripStatus.TripFinished, TripStatus.PaymentSuccess, TripStatus.CancelOnlyPickup].includes(deps.tripStatus)) && deps.markers.map(renderMarkerCallout)} |
| | | {deps.markers.map(renderMarkerCallout)} |
| | | {deps.shouldShowWaitingRadar && deps.waitingRadarVisible && ( |
| | | <Image className="waiting-radar-img" src={deps.waitingRadarSrc} webp={true} style={deps.waitingRadarStyle} /> |
| | | )} |
| | | {deps.isAndroid && deps.carCalloutVisible && ( |
| | | <View className="car-callout-android" style={deps.carCalloutStyle}> |
| | | <View className="custom-callout-content" style={{ padding: '6px 10px' }}> |
| | | <View className="custom-callout-content car-callout-with-before" style={{ padding: '6px 10px' }}> |
| | | {deps.hasBeforeUserEndSite && ( |
| | | <View className="car-time car-before-order-tip">即将完成上一单</View> |
| | | )} |
| | | <View className="car-time" style={{ alignItems: 'flex-end' }}> |
| | | <View className="car-time-num">{deps.routeDistance?.value ?? 0}</View> |
| | | <View style={{ flexShrink: 0 }}>{deps.routeDistance?.unit === 'm' ? '米' : '公里'}</View> |
| | |
| | | setVehicleMoveAlongPoints: (points: any[]) => void; |
| | | setRouteDistance: (value: any) => void; |
| | | setRouteDuration: (value: number) => void; |
| | | setBeforeUserEndSite: (site: { longitude: number; latitude: number } | null) => void; |
| | | erasePassedRouteSegments: (points: any[], servedPoints?: any[]) => void; |
| | | applyRoutePoints: (points: any[]) => boolean; |
| | | updateVehicleLocationThrottled: (lat: number, lng: number, force?: boolean) => void; |
| | |
| | | |
| | | const resolveOrderId = (payload: any, fallbackOrderId: string) => |
| | | payload?.content?.orderId || payload?.orderId || fallbackOrderId || Taro.getStorageSync('orderId'); |
| | | |
| | | // 解析“上一单乘客下车点”坐标,任一坐标无效则返回空,供 marker 显隐控制。 |
| | | const parseBeforeUserEndSite = (payload: any) => { |
| | | const longitude = Number(payload?.content?.endLongitude); |
| | | const latitude = Number(payload?.content?.endlatitude); |
| | | if (!Number.isFinite(longitude) || !Number.isFinite(latitude)) { |
| | | return null; |
| | | } |
| | | return { longitude, latitude }; |
| | | }; |
| | | |
| | | const isSameOrder = (incomingOrderId: string, currentOrderId: string) => { |
| | | if (!incomingOrderId || !currentOrderId) return true; |
| | |
| | | if (!isSameOrder(incomingOrderId, currentOrderId)) { |
| | | return; |
| | | } |
| | | deps.setBeforeUserEndSite(parseBeforeUserEndSite(payload)); |
| | | |
| | | const mappedTripStatus = syncTripStatus(payload, deps); |
| | | const effectiveOrderId = resolveOrderId(payload, currentOrderId); |
| | |
| | | }; |
| | | |
| | | const handleOrderRoutePayload = (payload: any, deps: UseWebSocketEventsDeps) => { |
| | | deps.setBeforeUserEndSite(parseBeforeUserEndSite(payload)); |
| | | const points = payload?.points || []; |
| | | const servedPoints = Array.isArray(payload?.servedPoints) ? payload.servedPoints : []; |
| | | const forceFullRedraw = payload?.forceFullRedraw === true; |
| | |
| | | } |
| | | |
| | | .car-time { |
| | | font-size: 24px; |
| | | font-size: 20px; |
| | | letter-spacing: normal; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .car-callout-with-before { |
| | | flex-direction: column; |
| | | align-items: flex-start; |
| | | } |
| | | |
| | | .car-before-order-tip { |
| | | margin-bottom: 4px; |
| | | } |
| | | |
| | | .car-time-num { |
| | |
| | | pointer-events: none; |
| | | } |
| | | |
| | | .before-user-site-callout { |
| | | transform: translate(-74%, -110%); |
| | | pointer-events: none; |
| | | } |
| | | |
| | | .before-user-site-txt { |
| | | font-size: 20px; |
| | | color: rgba(0, 0, 0, 0.9); |
| | | line-height: 28px; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .waiting-callout { |
| | | position: relative; |
| | | z-index: 999; |
| | |
| | | viewport: 'route_walking' | 'pickup' | 'route' | 'car_user' | 'order_detail'; |
| | | translateCar: boolean; |
| | | }; |
| | | type BeforeUserEndSite = { |
| | | longitude: number; |
| | | latitude: number; |
| | | }; |
| | | |
| | | export default function BeginRoute() { |
| | | const WAITING_TIMER_START_KEY = 'waitingTimerStartAt'; // 等待计时开始时间 |
| | |
| | | const [endSite, setEndSite] = useState<any>({}); // 下车点 |
| | | const [tripStatus, setTripStatus] = useState<TripStatus>(toTripStatus(Taro.getStorageSync('tripStatus'))); // 当前行程状态 |
| | | const [elapsedSeconds, setElapsedSeconds] = useState(0); // 等待计时秒数 |
| | | const [beforeUserEndSite, setBeforeUserEndSite] = useState<BeforeUserEndSite | null>(null); // 上一单乘客下车点 |
| | | const requoteTriggeredRef = useRef(false); // 重新报价触发标记,防止重复触发 |
| | | const [callSubmitting, setCallSubmitting] = useState(false); // 呼叫车辆提交中状态 |
| | | const [orderId, setOrderId] = useState<string>(''); // 订单ID |
| | |
| | | endSite: effectiveEndSite, |
| | | setStartSite, |
| | | setEndSite, |
| | | setBeforeUserEndSite, |
| | | updateVehicleLocationThrottled: vehicleState.updateVehicleLocationThrottled, |
| | | setVehicleDirection |
| | | }); // 路线与费用状态容器 |
| | |
| | | vehicleLocation, |
| | | vehicleDirection, |
| | | vehicleMarkerId, |
| | | beforeUserEndSite, |
| | | arrivedMarkerReady, |
| | | arrivedTooltipReady |
| | | }); // 地图标记状态容器 |
| | |
| | | setVehicleMoveAlongPoints, |
| | | setRouteDistance, |
| | | setRouteDuration, |
| | | setBeforeUserEndSite, |
| | | erasePassedRouteSegments: routeErasePassedRouteSegments, |
| | | applyRoutePoints: routeApplyRoutePoints, |
| | | updateVehicleLocationThrottled: vehicleState.updateVehicleLocationThrottled |
| | |
| | | arrivedTooltipReady, |
| | | routeDistance, |
| | | routeDuration, |
| | | hasBeforeUserEndSite: !!beforeUserEndSite, |
| | | customCalloutIconSrc, |
| | | showTrip1StyleEndpoint, |
| | | startSite, |
| | |
| | | Taro.setStorageSync('tripStatus', mapped.tripStatus); |
| | | Taro.setStorageSync('orderId', orderId); |
| | | } |
| | | this.notifyListeners('order_status', { status, ...mapped, data, content }); |
| | | this.notifyListeners('order_status', { |
| | | status, |
| | | ...mapped, |
| | | data, |
| | | content |
| | | }); |
| | | } |
| | | } |
| | | if (data?.type === 'ORDER_ROUTE' || data?.type === 'ORDER_UPDATE_LINE') { |