feat(robotaxi): 新增行程分享、排队状态显示与步行导航功能
| | |
| | | payload |
| | | }, true); |
| | | } |
| | | |
| | | // 订单查询队列状态 |
| | | export const queryOrderQueueStatus = async (payload) => { |
| | | return request({ |
| | | url: _ROBOTAXI_HOST + `/itinerary/base/v1/mini-program/order/queue-status`, |
| | | method: 'GET', |
| | | payload |
| | | }, true); |
| | | } |
| | |
| | | originBoxHidden: boolean; |
| | | originOpacity: number; |
| | | toOrigin: () => void; |
| | | handleWalkNavigation: () => void; |
| | | originIcon: string; |
| | | selectRouteIcon: string; |
| | | packUpIcon: string; |
| | |
| | | originBoxHidden, |
| | | originOpacity, |
| | | toOrigin, |
| | | handleWalkNavigation, |
| | | originIcon, |
| | | selectRouteIcon, |
| | | packUpIcon, |
| | |
| | | <View className='container-inner'> |
| | | {shouldShowOrigin && ( |
| | | <View className='origin-box' style={{ opacity: originBoxHidden ? 0 : originOpacity, transition: 'opacity 0.2s ease-out', pointerEvents: originBoxHidden ? 'none' : 'auto' }}> |
| | | {[4, 5].includes(tripStatus) && ( |
| | | <Image onClick={handleWalkNavigation} style={{ marginBottom: '16px' }} className='origin-icon' src={walkIcon} /> |
| | | )} |
| | | <Image onClick={toOrigin} className='origin-icon' src={originIcon} /> |
| | | { |
| | | tripStatus == 1 && |
| | |
| | | import { View, Text, Image } from "@tarojs/components"; |
| | | import { View, Text, Image, Button } from "@tarojs/components"; |
| | | import changeDestinationIcon from "../../../../assets/change-destination.png"; |
| | | import shareIcon from "../../../../assets/share-trip.png"; |
| | | import cancelIcon from "../../../../assets/cancel-trip.png"; |
| | |
| | | return ( |
| | | <View className="trip-action-bar"> |
| | | {actionList.map(item => ( |
| | | <View |
| | | key={item.key} |
| | | className="trip-action-bar-item" |
| | | hoverClass="trip-action-bar-item-hover" |
| | | onClick={() => item.onClick?.()} |
| | | > |
| | | <Image className="trip-action-bar-item-icon" src={item.icon} /> |
| | | <Text className="trip-action-bar-item-text">{item.label}</Text> |
| | | </View> |
| | | item.key === 'share' ? ( |
| | | <Button |
| | | key={item.key} |
| | | className="trip-action-bar-item trip-action-bar-share-btn" |
| | | hoverClass="trip-action-bar-item-hover" |
| | | openType="share" |
| | | onClick={() => item.onClick?.()} |
| | | > |
| | | <Image className="trip-action-bar-item-icon" src={item.icon} /> |
| | | <Text className="trip-action-bar-item-text">{item.label}</Text> |
| | | </Button> |
| | | ) : ( |
| | | <View |
| | | key={item.key} |
| | | className="trip-action-bar-item" |
| | | hoverClass="trip-action-bar-item-hover" |
| | | onClick={() => item.onClick?.()} |
| | | > |
| | | <Image className="trip-action-bar-item-icon" src={item.icon} /> |
| | | <Text className="trip-action-bar-item-text">{item.label}</Text> |
| | | </View> |
| | | ) |
| | | ))} |
| | | </View> |
| | | ); |
| | |
| | | import Taro from "@tarojs/taro"; |
| | | import { View, Image } from "@tarojs/components"; |
| | | import { useEffect, useState } from "react"; |
| | | import { formatCountdown } from '../../../../../utils/Robotaxi-public-method'; |
| | | import { queryOrderQueueStatus } from '../../../../api/begin-route'; |
| | | |
| | | type QueueStatus = { |
| | | currentPosition: number; |
| | | estimatedWaitTime: number; |
| | | status: number; |
| | | totalCount: number; |
| | | } | null; |
| | | |
| | | type Props = { |
| | | orderId?: string; |
| | | tripStatus: number; |
| | | waitMinutes: number | null; |
| | | durationDisplay: { value: string | number; unit: string }; |
| | |
| | | }; |
| | | |
| | | export default function TripStatusHeader({ |
| | | orderId, |
| | | tripStatus, |
| | | waitMinutes, |
| | | durationDisplay, |
| | |
| | | limitMinutes, |
| | | handleOpenCancelPopup |
| | | }: Props) { |
| | | const [queueStatus, setQueueStatus] = useState<QueueStatus>(null); |
| | | |
| | | useEffect(() => { |
| | | if (tripStatus !== 3 || !orderId) { |
| | | setQueueStatus(null); |
| | | return; |
| | | } |
| | | |
| | | let timer: ReturnType<typeof setInterval> | null = null; |
| | | let unmounted = false; |
| | | |
| | | // 排队位次是动态变化的,状态为 3 时定时刷新 |
| | | const fetchQueueStatus = async () => { |
| | | try { |
| | | const res = await queryOrderQueueStatus({ orderId }); |
| | | if (unmounted) { |
| | | return; |
| | | } |
| | | const queueData = res?.data ?? res; |
| | | if ( |
| | | Number(queueData?.currentPosition) > 0 && |
| | | Number(queueData?.totalCount) > 0 |
| | | ) { |
| | | setQueueStatus({ |
| | | currentPosition: Number(queueData.currentPosition), |
| | | estimatedWaitTime: Number(queueData.estimatedWaitTime) || 0, |
| | | status: Number(queueData.status) || 0, |
| | | totalCount: Number(queueData.totalCount) |
| | | }); |
| | | return; |
| | | } |
| | | setQueueStatus(null); |
| | | } catch (_) { |
| | | if (!unmounted) { |
| | | setQueueStatus(null); |
| | | } |
| | | } |
| | | }; |
| | | |
| | | fetchQueueStatus(); |
| | | timer = setInterval(fetchQueueStatus, 10000); |
| | | |
| | | return () => { |
| | | unmounted = true; |
| | | if (timer) { |
| | | clearInterval(timer); |
| | | } |
| | | }; |
| | | }, [tripStatus, orderId]); |
| | | |
| | | const titleMap = { |
| | | 3: <>预计<View className="time">{waitMinutes}</View>分钟内有车应答</>, |
| | | 4: <>车辆还有<View className="time">{durationDisplay.value}</View>{durationDisplay.unit}到达</>, |
| | |
| | | <View className='calling-car-content-title'>{titleMap[tripStatus]}</View> |
| | | )} |
| | | {descMap[tripStatus] && ( |
| | | <View style={{ paddingBottom: tripStatus === 3 ? '20px' : '0px' }} className={descMap[tripStatus]?.className}>{descMap[tripStatus]?.text}</View> |
| | | // <View style={{ paddingBottom: tripStatus === 3 ? '20px' : '0px' }} className={descMap[tripStatus]?.className}>{descMap[tripStatus]?.text}</View> |
| | | <View className={descMap[tripStatus]?.className}>{descMap[tripStatus]?.text}</View> |
| | | )} |
| | | {tripStatus === 3 && queueStatus && ( |
| | | <View className='calling-car-content-desc'> |
| | | 第{queueStatus.currentPosition}位/共{queueStatus.totalCount}位 |
| | | </View> |
| | | )} |
| | | </View> |
| | | { |
| | |
| | | } |
| | | </View> |
| | | ); |
| | | } |
| | | } |
| | |
| | | <Image className="calling-car-bg" src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/site-top-bg.png' /> |
| | | |
| | | <TripStatusHeader |
| | | orderId={orderId} |
| | | tripStatus={tripStatus} |
| | | waitMinutes={waitMinutes} |
| | | durationDisplay={durationDisplay} |
| | |
| | | color: rgba(0, 0, 0, 0.9); |
| | | } |
| | | } |
| | | |
| | | &-share-btn { |
| | | padding: 0; |
| | | margin: 0; |
| | | min-width: auto; |
| | | line-height: normal; |
| | | background: transparent; |
| | | border: none; |
| | | |
| | | &::after { |
| | | border: none; |
| | | } |
| | | } |
| | | } |
| | | |
| | | &-arrive { |
| | |
| | | import Taro, { useDidShow } from "@tarojs/taro" |
| | | import Taro, { useDidShow, useShareAppMessage } from "@tarojs/taro" |
| | | import { useEffect, useState, useRef, useMemo } from 'react'; |
| | | import { View, Image } from '@tarojs/components'; |
| | | import './index.less'; |
| | |
| | | 1: { showPickup: true, showDropoff: true, showCar: false, showRoute: true, showWalking: true, viewport: 'route_walking', translateCar: false }, |
| | | 2: { showPickup: true, showDropoff: false, showCar: false, showRoute: false, showWalking: false, viewport: 'pickup', translateCar: false }, |
| | | 3: { showPickup: true, showDropoff: false, showCar: false, showRoute: false, showWalking: false, viewport: 'pickup', translateCar: false }, |
| | | 4: { showPickup: true, showDropoff: false, showCar: true, showRoute: true, showWalking: false, viewport: 'route', translateCar: true }, |
| | | 5: { showPickup: true, showDropoff: false, showCar: true, showRoute: false, showWalking: false, viewport: 'car_user', translateCar: true }, |
| | | 4: { showPickup: true, showDropoff: false, showCar: true, showRoute: true, showWalking: true, viewport: 'route', translateCar: true }, |
| | | 5: { showPickup: true, showDropoff: false, showCar: true, showRoute: false, showWalking: true, viewport: 'car_user', translateCar: true }, |
| | | 6: { showPickup: false, showDropoff: true, showCar: true, showRoute: true, showWalking: false, viewport: 'route', translateCar: true }, |
| | | 7: { showPickup: false, showDropoff: true, showCar: true, showRoute: false, showWalking: false, viewport: 'dropoff', translateCar: false }, |
| | | 8: { showPickup: true, showDropoff: true, showCar: false, showRoute: true, showWalking: false, viewport: 'order_detail', translateCar: false }, |
| | |
| | | toOrder, |
| | | toBack, |
| | | handleCalloutTap, |
| | | toSelectSite, |
| | | guardPrivacyThenNavigate |
| | | toSelectSite |
| | | } = usePageActions({ |
| | | tripStatus, |
| | | startSite, |
| | |
| | | handleFinishCountdownTimeout, |
| | | handlePay |
| | | } = orderActions; |
| | | const shareOrderId = orderId || Taro.getStorageSync('orderId') || ''; |
| | | const onShareAppMessage = () => ({ |
| | | title: '快来查看我的 Robotaxi 行程', |
| | | path: `/robotaxi/pages/share-trip/index?orderId=${shareOrderId}` |
| | | }); |
| | | |
| | | useShareAppMessage(onShareAppMessage); |
| | | |
| | | useDidShow(() => { |
| | | handleDidShow(router.params?.skipPageFlag); |
| | |
| | | }; |
| | | const handleShareTrip = () => { |
| | | isCheckedEnter.current = true; |
| | | guardPrivacyThenNavigate('/robotaxi/pages/share-trip/index'); |
| | | }; |
| | | const { renderMapOverlays, renderTripCard } = useViewDelegates({ |
| | | isAndroid, |
| | |
| | | isCheckedEnter.current = true; |
| | | toOrder(); |
| | | }; |
| | | const handleWalkNavigation = () => { |
| | | if (![4, 5].includes(tripStatus) || !startSite?.latitude || !startSite?.longitude) { |
| | | return; |
| | | } |
| | | |
| | | Taro.openLocation({ |
| | | latitude: Number(startSite.latitude), |
| | | longitude: Number(startSite.longitude), |
| | | name: startSite.name || '上车点', |
| | | address: startSite.address || startSite.siteAddress || '', |
| | | scale: 18 |
| | | }); |
| | | }; |
| | | |
| | | return ( |
| | | <BeginRouteView |
| | |
| | | originBoxHidden={originBoxHidden} |
| | | originOpacity={originOpacity} |
| | | toOrigin={toOrigin} |
| | | handleWalkNavigation={handleWalkNavigation} |
| | | selectRouteIcon={selectRouteIcon} |
| | | packUpIcon={packUpIcon} |
| | | onSelectRouteClick={() => setIsRouteSelectExpanded(!isRouteSelectExpanded)} |
| | |
| | | import Taro from '@tarojs/taro'; |
| | | import { View, Image } from '@tarojs/components'; |
| | | import { useState, useEffect } from 'react'; |
| | | import { collectPoint } from '../../../api/site-search'; |
| | | import { collectPoint, getCollectPoints } from '../../../api/site-search'; |
| | | |
| | | type Props = { |
| | | // 站点数据 |
| | |
| | | onSelect, |
| | | renderHighlightedText |
| | | }: Props) { |
| | | const MAX_COLLECT_COUNT = 50; |
| | | const [isCollected, setIsCollected] = useState(!!item.isCollected); |
| | | |
| | | useEffect(() => { |
| | | setIsCollected(item.isCollect === 1); |
| | | }, [item.isCollect]); |
| | | |
| | | const checkCollectLimit = async () => { |
| | | // 只取总数即可,避免为校验拉完整列表 |
| | | const res = await getCollectPoints({ |
| | | pageNum: 1, |
| | | pageSize: 1 |
| | | }); |
| | | |
| | | if (Number(res?.total) >= MAX_COLLECT_COUNT) { |
| | | Taro.showToast({ |
| | | title: `最多收藏${MAX_COLLECT_COUNT}个站点`, |
| | | icon: 'none' |
| | | }); |
| | | return false; |
| | | } |
| | | |
| | | return true; |
| | | }; |
| | | |
| | | const handleCollect = async (e: any) => { |
| | | e.stopPropagation(); |
| | | try { |
| | | const type = isCollected ? 2 : 1; // 1=收藏 2=取消收藏 |
| | | |
| | | if (type === 1) { |
| | | const canCollect = await checkCollectLimit(); |
| | | if (!canCollect) { |
| | | return; |
| | | } |
| | | } |
| | | |
| | | const payload: any = { |
| | | itemType: 1, // 默认为站点处理 |
| | | latitude: item.latitude, |