zjk
2026-04-17 8ff8f9c36ba3f4ea07d81673d81feab9ba4b3c82
feat(robotaxi): 新增行程分享、排队状态显示与步行导航功能
8个文件已修改
193 ■■■■■ 已修改文件
src/robotaxi/api/begin-route.ts 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/begin-route-view.tsx 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car-modules/trip-action-bar.tsx 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car-modules/trip-status-header.tsx 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car.tsx 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/css-folder/calling-car.less 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/index.tsx 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/site-search/components/site-list-item.tsx 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/api/begin-route.ts
@@ -185,3 +185,12 @@
        payload
    }, true);
}
// 订单查询队列状态
export const queryOrderQueueStatus = async (payload) => {
    return request({
        url: _ROBOTAXI_HOST + `/itinerary/base/v1/mini-program/order/queue-status`,
        method: 'GET',
        payload
    }, true);
}
src/robotaxi/pages/begin-route/components/begin-route-view.tsx
@@ -34,6 +34,7 @@
  originBoxHidden: boolean;
  originOpacity: number;
  toOrigin: () => void;
  handleWalkNavigation: () => void;
  originIcon: string;
  selectRouteIcon: string;
  packUpIcon: string;
@@ -94,6 +95,7 @@
    originBoxHidden,
    originOpacity,
    toOrigin,
    handleWalkNavigation,
    originIcon,
    selectRouteIcon,
    packUpIcon,
@@ -160,6 +162,9 @@
        <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 &&
src/robotaxi/pages/begin-route/components/calling-car-modules/trip-action-bar.tsx
@@ -1,4 +1,4 @@
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";
@@ -48,15 +48,28 @@
    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>
    );
src/robotaxi/pages/begin-route/components/calling-car-modules/trip-status-header.tsx
@@ -1,8 +1,17 @@
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 };
@@ -16,6 +25,7 @@
};
export default function TripStatusHeader({
    orderId,
    tripStatus,
    waitMinutes,
    durationDisplay,
@@ -27,6 +37,56 @@
    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}到达</>,
@@ -55,7 +115,13 @@
                    <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>
            {
@@ -66,4 +132,4 @@
            }
        </View>
    );
}
}
src/robotaxi/pages/begin-route/components/calling-car.tsx
@@ -114,6 +114,7 @@
            <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}
src/robotaxi/pages/begin-route/components/css-folder/calling-car.less
@@ -179,6 +179,19 @@
          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 {
src/robotaxi/pages/begin-route/index.tsx
@@ -1,4 +1,4 @@
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';
@@ -113,8 +113,8 @@
    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 },
@@ -312,8 +312,7 @@
    toOrder,
    toBack,
    handleCalloutTap,
    toSelectSite,
    guardPrivacyThenNavigate
    toSelectSite
  } = usePageActions({
    tripStatus,
    startSite,
@@ -454,6 +453,13 @@
    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);
@@ -502,7 +508,6 @@
  };
  const handleShareTrip = () => {
    isCheckedEnter.current = true;
    guardPrivacyThenNavigate('/robotaxi/pages/share-trip/index');
  };
  const { renderMapOverlays, renderTripCard } = useViewDelegates({
    isAndroid,
@@ -569,6 +574,19 @@
    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
@@ -595,6 +613,7 @@
      originBoxHidden={originBoxHidden}
      originOpacity={originOpacity}
      toOrigin={toOrigin}
      handleWalkNavigation={handleWalkNavigation}
      selectRouteIcon={selectRouteIcon}
      packUpIcon={packUpIcon}
      onSelectRouteClick={() => setIsRouteSelectExpanded(!isRouteSelectExpanded)}
src/robotaxi/pages/site-search/components/site-list-item.tsx
@@ -1,7 +1,7 @@
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 = {
  // 站点数据
@@ -33,16 +33,43 @@
  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,