hejianhao
2026-02-27 7362ca24fa9c2a5a47c3a61c3fe2efa04a0be33f
feat: 新增乘车指南页面并优化行程相关功能

- 新增乘车指南页面,包含配置、样式和API接口
- 优化订单历史页面,支持再次下单时自动填充起终点
- 改进费用说明页面,修复计价规则跳转和费用显示问题
- 增强行程页面功能,新增实时轨迹查询和行程数据格式化工具
- 调整车辆状态页面,优化温度控制和车门解锁交互
- 修复首页地图定位逻辑,优化运营区域显示
- 统一调试用户ID,简化登录验证流程
- 修复多个样式问题和组件交互细节
3个文件已添加
19个文件已修改
1010 ■■■■■ 已修改文件
src/app.config.ts 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/index.tsx 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/api/begin-route.ts 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/api/home.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/api/order.ts 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car.tsx 233 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/css-folder/calling-car.less 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/css-folder/release-route.less 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/passenger-notice.tsx 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/release-route.tsx 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/index.tsx 434 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/expense-description/index.tsx 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/order/index.tsx 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/pricing-rules/index.tsx 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/ride-guide/index.config.ts 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/ride-guide/index.less 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/ride-guide/index.tsx 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/select-map-site/index.tsx 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/Robotaxi-public-method.ts 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/request.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/tools.ts 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/websocket.ts 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/app.config.ts
@@ -33,6 +33,7 @@
        'pages/expense-description/index',
        'pages/passenger-notice/index',
        'pages/pricing-rules/index',
        'pages/ride-guide/index',
      ],
    }
  ];
src/pages/robotaxi/home/index.tsx
@@ -25,6 +25,7 @@
} from '../../../utils/Robotaxi-public-method';
import { _ROBOTAXI_WS_HOST } from '../../../constants/api';
import { getGeofence, getBannerList, getAgreement, getOrderActive } from '../../../robotaxi/api/home';
import { getServicePhone } from '../../../robotaxi/api/customer-service';
import { getOrderDistance } from '../../../robotaxi/api/select-map-site';
import { checkOtp } from '../../../utils/tools';
@@ -51,7 +52,7 @@
  const [adsorptionDistance, setAdsorptionDistance] = useState(null); // 吸附距离
  const [orderData, setOrderData] = useState<any>({}); // 订单数据
  const [boxHeight, setBoxHeight] = useState<number | string>('auto'); // 活动区域高度
  const [boxHeight, setBoxHeight] = useState<number>(266); // 活动区域高度
  const startY = useRef(0); // 拖动开始时的Y坐标
  const startHeight = useRef(0); // 拖动开始时的高度
  const fullHeight = useRef(0); // 活动区域最大高度
@@ -60,6 +61,7 @@
  const shouldIgnoreRegionChange = useRef(false); // 是否忽略地图区域变化
  const resetTimer = useRef<any>(null); // 自动归位定时器
  const isReturningToOrigin = useRef(false); // 是否正在自动归位
  const skipNextLocationCenterRef = useRef(false);
  const orderStatusBound = useRef(false);
  const DEFAULT_LATITUDE = 39.916527; // 北京故宫纬度
@@ -72,8 +74,8 @@
  const ORDER_STATUS = {
    4: '车辆正在赶来',
    5: '正在前往目的地',
    6: '车辆已到达上车点,请尽快前往',
    5: '车辆已到达上车点,请尽快前往',
    6: '正在前往目的地',
    7: '车辆已到达目的地,请尽快结束行程',
  }
@@ -93,6 +95,9 @@
  // 监听页面显示
  useDidShow(() => {
    getServicePhone().then((res) => {
      Taro.setStorageSync('customerServicePhone', res?.customerServicePhone || '');
    })
    // 从本地存储中获取选中的城市
    const selectedCity = Taro.getStorageSync('selectedCity');
    if (selectedCity) {
@@ -192,7 +197,9 @@
                });
            }
          } else {
            getLocationInfo(true);
            const shouldUpdateCenter = !skipNextLocationCenterRef.current;
            skipNextLocationCenterRef.current = false;
            getLocationInfo(shouldUpdateCenter);
          }
        }
      }
@@ -293,23 +300,36 @@
      if (res && Array.isArray(res) && res.length > 0) {
        const newPolygons = formatGeofencePolygons(res);
        setPolygons(newPolygons);
        if (orderData || orderData?.id) return;
        if ((orderData || orderData?.id) && JSON.stringify(orderData) != '{}') return;
        // 定位到第一个运营区域的第一个点
        const firstPolygon = res[0];
        if (firstPolygon && firstPolygon.points && firstPolygon.points.length > 0) {
          const firstPoint = firstPolygon.points[0];
          setLatitude(Number(firstPoint.lat));
          setLongitude(Number(firstPoint.lng));
          skipNextLocationCenterRef.current = true;
          shouldIgnoreRegionChange.current = true;
          const mapCtx = Taro.createMapContext('map');
          mapCtx.moveToLocation({
            latitude: Number(firstPoint.lat),
            longitude: Number(firstPoint.lng),
          });
        }
      } else {
        setPolygons([]);
        if (orderData || orderData?.id) return;
        if ((orderData || orderData?.id) && JSON.stringify(orderData) != '{}') return;
        // 定位到城市中心
        if (selectedCity.name) {
          getCityLocation(selectedCity.name).then(loc => {
            setLatitude(loc.latitude);
            setLongitude(loc.longitude);
            skipNextLocationCenterRef.current = true;
            shouldIgnoreRegionChange.current = true;
            const mapCtx = Taro.createMapContext('map');
            mapCtx.moveToLocation({
              latitude: loc.latitude,
              longitude: loc.longitude,
            });
          }).catch(err => {
            console.error('获取城市中心失败', err);
          });
@@ -323,6 +343,13 @@
        getCityLocation(selectedCity.name).then(loc => {
          setLatitude(loc.latitude);
          setLongitude(loc.longitude);
          skipNextLocationCenterRef.current = true;
          shouldIgnoreRegionChange.current = true;
          const mapCtx = Taro.createMapContext('map');
          mapCtx.moveToLocation({
            latitude: loc.latitude,
            longitude: loc.longitude,
          });
        }).catch(err => {
          console.error('获取城市中心失败', err);
        });
@@ -338,19 +365,15 @@
      query.select('.select-site').boundingClientRect(rect => {
        if (rect) {
          const contentHeight = rect.height;
          fullHeight.current = contentHeight;
          // 根据有多少banner来,最大显示高度为536
          // 如果内容高度小于536,则使用内容高度(自适应)
          // 如果内容高度大于536,则限制为536(需要拖拽查看更多)
          minHeight.current = Math.min(contentHeight, 536);
          setBoxHeight(minHeight.current);
          const baseHeight = 266;
          fullHeight.current = Math.max(contentHeight, baseHeight);
          minHeight.current = baseHeight;
          setBoxHeight(baseHeight);
        } else {
          // 兜底逻辑
          minHeight.current = 536;
          fullHeight.current = 536;
          setBoxHeight(536);
          minHeight.current = 266;
          fullHeight.current = 266;
          setBoxHeight(266);
        }
      }).exec();
    }, 500);
@@ -592,7 +615,10 @@
  };
  const openService = () => {
    setShowCustomerService(true);
    const phoneNumber = Taro.getStorageSync('customerServicePhone');
    Taro.makePhoneCall({
      phoneNumber,
    })
  }
  const handleOpenSetting = () => {
@@ -776,7 +802,7 @@
      {/* 选择站点 */}
      <View
        className="content-box"
        style={{ height: typeof boxHeight === 'number' ? `${boxHeight}px` : 'auto', transition: isDragging.current ? 'none' : 'height 0.3s ease-out' }}
        style={{ height: `${boxHeight}px`, transition: isDragging.current ? 'none' : 'height 0.3s ease-out' }}
        onTouchStart={handleTouchStart}
        onTouchMove={handleTouchMove}
        onTouchEnd={handleTouchEnd}
src/robotaxi/api/begin-route.ts
@@ -122,4 +122,13 @@
        url: _ROBOTAXI_HOST + `/order/api/v1/mini-program/order/cancel/${orderId}`,
        method: 'PUT'
    });
}
// 订单查询实时轨迹路线
export const queryOrderRoute = async (payload) => {
    return request({
        url: _ROBOTAXI_HOST + `/order/api/v1/mini-program/order/queryOrderRoute`,
        method: 'POST',
        payload
    });
}
src/robotaxi/api/home.ts
@@ -75,4 +75,12 @@
        url: _ROBOTAXI_HOST + `/order/api/v1/mini-program/order/active`,
        method: 'GET'
    });
}
// 乘车指南
export const getRideGuideList = async () => {
    return request({
        url: _ROBOTAXI_HOST + `/operation/base/v1/mini-program/guide/list`,
        method: 'GET'
    });
}
src/robotaxi/api/order.ts
@@ -1,18 +1,8 @@
import request from '../../utils/request';
import { _ROBOTAXI_HOST } from '../../constants/api';
export interface OrderHistoryOrder {
    amount: number;
    destinationAddress: string;
    orderId: string;
    orderTime: string;
    originAddress: string;
    time?: string;
}
export interface OrderHistoryGroup {
    dateLabel: string;
    orders: OrderHistoryOrder[];
}
export interface GetOrderHistoryPayload {
src/robotaxi/pages/begin-route/components/calling-car.tsx
@@ -28,7 +28,7 @@
    handleOpenEvaluatePopup?: () => void; // 打开评价弹窗
    handleOpenAnnouncementsPopup?: () => void; // 打开公告弹窗
    estimatedWaitMinutes?: number; // 预计等待时间
    routeDurationMinutes?: number | null; // 预计行程时间
    routeDurationSeconds?: number | null; // 预计行程时间
    vehicleInfo?: { // 车辆信息
        vehiclePictures?: string[] | string; // 车辆图片
        vehicleCode?: string; // 车辆编号
@@ -38,11 +38,11 @@
    orderId?: string; // 订单ID
}
export default function CallingCar({ tripStatus = 3, handleOpenCancelPopup, handleCancelTimeout, handleOpenOverRoutePopup, handlePay, updateStatus, handleOpenEvaluatePopup, handleOpenAnnouncementsPopup, estimatedWaitMinutes, routeDurationMinutes, vehicleInfo, orderId }: Props) {
export default function CallingCar({ tripStatus = 3, handleOpenCancelPopup, handleCancelTimeout, handleOpenOverRoutePopup, handlePay, updateStatus, handleOpenEvaluatePopup, handleOpenAnnouncementsPopup, estimatedWaitMinutes, routeDurationSeconds, vehicleInfo, orderId }: Props) {
    const [trunkStatus, setTrunkStatus] = useState(false); // 后备箱状态
    const [lockStatus, setLockStatus] = useState(false); // 锁车状态
    const [tempStatus, setTempStatus] = useState(); // 温度状态
    const [tempStatus, setTempStatus] = useState<number | null>(); // 温度状态
    const [activeIndex, setActiveIndex] = useState(0) // 默认选中"很满意"
    const [waitMinutes, setWaitMinutes] = useState<number | null>(null); // 预计等待时间
    const [routeDuration, setRouteDuration] = useState<number | null>(null); // 预计行程时间
@@ -61,6 +61,9 @@
    const finishEndTimeRef = useRef<number | null>(null); // 到达目的地时间
    const lastFareOrderIdRef = useRef<string | null>(null);
    const hasShownAnnouncementsRef = useRef(false); // 是否已展示过公告弹窗
    const TEMP_MIN = 22;
    const TEMP_MAX = 28;
    const formatCountdown = (seconds: number) => {
        const safeSeconds = Math.max(0, seconds);
@@ -101,13 +104,23 @@
        return Date.now() + num * 1000;
    };
    const displayRouteDuration = typeof routeDurationMinutes === 'number' && !Number.isNaN(routeDurationMinutes) ? routeDurationMinutes : routeDuration;
    const displayRouteDurationSeconds = typeof routeDurationSeconds === 'number' && !Number.isNaN(routeDurationSeconds) ? routeDurationSeconds : routeDuration;
    const formatMoney = (value: any) => {
        const num = Number(value);
        if (Number.isNaN(num)) {
            return '--';
        }
        return num.toFixed(1);
    };
    const formatDurationDisplay = (value: any) => {
        const num = Number(value);
        if (Number.isNaN(num)) {
            return { value: 0, unit: '分钟' };
        }
        if (num < 60) {
            return { value: Math.max(0, Math.round(num)), unit: '秒' };
        }
        return { value: Math.ceil(num / 60), unit: '分钟' };
    };
    const formatNumber = (value: any, digits = 1) => {
        const num = Number(value);
@@ -121,12 +134,13 @@
        if (type === 3) return '节假日时段';
        return '普通时段';
    };
    const isArriveSoon = tripStatus === 4 && typeof displayRouteDuration === 'number' && !Number.isNaN(displayRouteDuration) && displayRouteDuration < 1;
    const durationDisplay = formatDurationDisplay(displayRouteDurationSeconds);
    const isArriveSoon = tripStatus === 4 && typeof displayRouteDurationSeconds === 'number' && !Number.isNaN(displayRouteDurationSeconds) && displayRouteDurationSeconds < 60;
    const titleMap = {
        3: <>预计<View className="time">{waitMinutes}</View>分钟内有车应答</>,
        4: <>车辆还有<View className="time">{displayRouteDuration}</View>分钟到达</>,
        4: <>车辆还有<View className="time">{durationDisplay.value}</View>{durationDisplay.unit}到达</>,
        5: <>车辆已到达上车点<View className="time">{pickupCountdownSeconds !== null ? formatCountdown(pickupCountdownSeconds) : '--:--'}</View></>,
        6: <>预计<View className="time">{displayRouteDuration}</View>分钟抵达目的地</>,
        6: <>预计<View className="time">{durationDisplay.value}</View>{durationDisplay.unit}抵达目的地</>,
        7: <>已到达目的地<View className="time">{finishCountdownSeconds !== null ? formatCountdown(finishCountdownSeconds) : '--:--'}</View></>,
        8: '行程已结束',
        9: '支付成功'
@@ -166,9 +180,11 @@
            }).catch(() => { });
        } else if (tripStatus === 4) {
            // 即将到达:加载路线时间、限停时间与当前车内温度
            const durationUnit = Taro.getStorageSync('orderRouteDurationUnit');
            const duration = Number(Taro.getStorageSync('orderRouteDuration'));
            if (!Number.isNaN(duration)) {
                setRouteDuration(duration);
            const durationSeconds = durationUnit === 's' ? duration : duration * 60;
            if (!Number.isNaN(durationSeconds)) {
                setRouteDuration(durationSeconds);
            }
            const s = Taro.getStorageSync('startSite');
            const time = Number(s?.time);
@@ -178,8 +194,8 @@
            const targetOrderId = orderId || Taro.getStorageSync('orderId');
            if (targetOrderId) {
                // 暂时不调用车辆温度
                setTempStatus(19);
                lastConfirmedTempRef.current = 19;
                setTempStatus(26);
                lastConfirmedTempRef.current = 26;
                // queryTemperatureByOrderId({ orderId: targetOrderId }).then((res: any) => {
                //     const currentTemperature = Number(res?.currentTemperature ?? res?.temperature);
                //     if (!Number.isNaN(currentTemperature)) {
@@ -190,9 +206,11 @@
            }
        } else if (tripStatus === 5) {
            // 已到达上车点:根据限停时间启动倒计时并处理超时
            const durationUnit = Taro.getStorageSync('orderRouteDurationUnit');
            const duration = Number(Taro.getStorageSync('orderRouteDuration'));
            if (!Number.isNaN(duration)) {
                setRouteDuration(duration);
            const durationSeconds = durationUnit === 's' ? duration : duration * 60;
            if (!Number.isNaN(durationSeconds)) {
                setRouteDuration(durationSeconds);
            }
            const s = Taro.getStorageSync('startSite');
            const time = Number(s?.time);
@@ -318,7 +336,18 @@
    // 处理温度变化
    const handleTempChange = (delta: number) => {
        setTempStatus((prev) => {
            if (typeof prev !== 'number') {
                return prev;
            }
            const next = prev + delta;
            if (next < TEMP_MIN) {
                Taro.showToast({ title: `温度最低${TEMP_MIN}℃`, icon: 'none' });
                return prev;
            }
            if (next > TEMP_MAX) {
                Taro.showToast({ title: `温度最高${TEMP_MAX}℃`, icon: 'none' });
                return prev;
            }
            scheduleTemperatureUpdate(next);
            return next;
        });
@@ -342,8 +371,17 @@
                });
                return;
            }
            onSuccess?.();
            if (doorType === 'trunk') {
                Taro.showToast({
                    title: '后备箱已解锁',
                    icon: 'none'
                });
            }
            if (doorType === 'door') {
                Taro.showToast({
                    title: '车门已解锁',
                    icon: 'none'
                });
                if (!hasShownAnnouncementsRef.current) {
                    handleOpenAnnouncementsPopup?.();
                    hasShownAnnouncementsRef.current = true;
@@ -435,9 +473,17 @@
                            <View className="car-card-temp">
                                <View className="car-card-temp-title">当前温度</View>
                                <View className="car-card-temp-content">
                                    <Image className="car-card-temp-content-img" src={require('../../../assets/T-down.png')} onClick={() => handleTempChange(-1)} />
                                    <Image
                                        className="car-card-temp-content-img"
                                        src={require('../../../assets/T-down.png')}
                                        onClick={typeof tempStatus === 'number' && tempStatus > TEMP_MIN ? () => handleTempChange(-1) : undefined}
                                    />
                                    <View className="car-card-temp-content-text">{tempStatus}℃</View>
                                    <Image className="car-card-temp-content-img" src={require('../../../assets/T-up.png')} onClick={() => handleTempChange(1)} />
                                    <Image
                                        className="car-card-temp-content-img"
                                        src={require('../../../assets/T-up.png')}
                                        onClick={typeof tempStatus === 'number' && tempStatus < TEMP_MAX ? () => handleTempChange(1) : undefined}
                                    />
                                </View>
                            </View>
                        </View>
@@ -473,90 +519,88 @@
                                    <Image style={{ transform: showDetail ? 'rotate(90deg)' : 'rotate(0deg)', transition: 'transform 0.3s' }} src={require('../../../assets/ccc-icon-right.png')} />
                                </View>
                            </View>
                            {showDetail &&
                                <View className="particulars-detail">
                                    {Number(fareBreakdown?.orderMoney) > 0 &&
                                        <View className="particulars-detail-item">
                                            <View className="particulars-detail-item-title">
                                                <View>订单总额</View>
                                                <View>{formatMoney(fareBreakdown?.orderMoney)}元</View>
                                            </View>
                            <View className={`particulars-detail ${showDetail ? 'expand' : ''}`}>
                                {Number(fareBreakdown?.orderMoney) > 0 &&
                                    <View className="particulars-detail-item">
                                        <View className="particulars-detail-item-title">
                                            <View>订单总额</View>
                                            <View>{formatMoney(fareBreakdown?.orderMoney)}元</View>
                                        </View>
                                    }
                                    {Array.isArray(fareBreakdown?.start) && fareBreakdown.start.length > 0 &&
                                        <View className="particulars-detail-item">
                                            <View className="particulars-detail-item-title">
                                                <View>起步价</View>
                                                <View>{formatMoney(fareBreakdown.start.reduce((sum: number, item: any) => sum + (Number(item?.money) || 0), 0))}元</View>
                                            </View>
                                            {fareBreakdown.start.filter((item: any) => item?.money || item?.time).map((item: any, index: number) => (
                                                <View key={`start-${index}`} className="particulars-detail-item-content">
                                                    <View>{item?.time || '--'}</View>
                                                    <View>{formatMoney(item?.money)}元</View>
                                                </View>
                                            ))}
                                    </View>
                                }
                                {Array.isArray(fareBreakdown?.start) && fareBreakdown.start.length > 0 &&
                                    <View className="particulars-detail-item">
                                        <View className="particulars-detail-item-title">
                                            <View>起步价</View>
                                            <View>{formatMoney(fareBreakdown.start.reduce((sum: number, item: any) => sum + (Number(item?.money) || 0), 0))}元</View>
                                        </View>
                                    }
                                    {Array.isArray(fareBreakdown?.mileage) && fareBreakdown.mileage.length > 0 &&
                                        <View className="particulars-detail-item">
                                            <View className="particulars-detail-item-title">
                                                <View>里程费</View>
                                                <View>{formatMoney(fareBreakdown.mileage.reduce((sum: number, item: any) => sum + (Number(item?.money) || 0), 0))}元</View>
                                        {fareBreakdown.start.filter((item: any) => item?.money || item?.time).map((item: any, index: number) => (
                                            <View key={`start-${index}`} className="particulars-detail-item-content">
                                                <View>{item?.time || '--'}</View>
                                                <View>{formatMoney(item?.money)}元</View>
                                            </View>
                                            {fareBreakdown.mileage.filter((item: any) => item?.money || item?.mileage).map((item: any, index: number) => (
                                                <View key={`mileage-${index}`} className="particulars-detail-item-content">
                                                    <View>{getTypeLabel(item?.type)}({formatNumber(item?.mileage)}公里)</View>
                                                    <View>{formatMoney(item?.money)}元</View>
                                                </View>
                                            ))}
                                        ))}
                                    </View>
                                }
                                {Array.isArray(fareBreakdown?.mileage) && fareBreakdown.mileage.length > 0 &&
                                    <View className="particulars-detail-item">
                                        <View className="particulars-detail-item-title">
                                            <View>里程费</View>
                                            <View>{formatMoney(fareBreakdown.mileage.reduce((sum: number, item: any) => sum + (Number(item?.money) || 0), 0))}元</View>
                                        </View>
                                    }
                                    {Array.isArray(fareBreakdown?.duration) && fareBreakdown.duration.length > 0 &&
                                        <View className="particulars-detail-item">
                                            <View className="particulars-detail-item-title">
                                                <View>时长费</View>
                                                <View>{formatMoney(fareBreakdown.duration.reduce((sum: number, item: any) => sum + (Number(item?.money) || 0), 0))}元</View>
                                        {fareBreakdown.mileage.filter((item: any) => item?.money || item?.mileage).map((item: any, index: number) => (
                                            <View key={`mileage-${index}`} className="particulars-detail-item-content">
                                                <View>{getTypeLabel(item?.type)}({formatNumber(item?.mileage)}公里)</View>
                                                <View>{formatMoney(item?.money)}元</View>
                                            </View>
                                            {fareBreakdown.duration.filter((item: any) => item?.money || item?.duration).map((item: any, index: number) => (
                                                <View key={`duration-${index}`} className="particulars-detail-item-content">
                                                    <View>{getTypeLabel(item?.type)}({formatNumber(item?.duration, 0)}分钟)</View>
                                                    <View>{formatMoney(item?.money)}元</View>
                                                </View>
                                            ))}
                                        ))}
                                    </View>
                                }
                                {Array.isArray(fareBreakdown?.duration) && fareBreakdown.duration.length > 0 &&
                                    <View className="particulars-detail-item">
                                        <View className="particulars-detail-item-title">
                                            <View>时长费</View>
                                            <View>{formatMoney(fareBreakdown.duration.reduce((sum: number, item: any) => sum + (Number(item?.money) || 0), 0))}元</View>
                                        </View>
                                    }
                                    {Array.isArray(fareBreakdown?.longDistance) && fareBreakdown.longDistance.length > 0 &&
                                        <View className="particulars-detail-item">
                                            <View className="particulars-detail-item-title">
                                                <View>远途费</View>
                                                <View>{formatMoney(fareBreakdown.longDistance.reduce((sum: number, item: any) => sum + (Number(item?.money) || 0), 0))}元</View>
                                        {fareBreakdown.duration.filter((item: any) => item?.money || item?.duration).map((item: any, index: number) => (
                                            <View key={`duration-${index}`} className="particulars-detail-item-content">
                                                <View>{getTypeLabel(item?.type)}({formatNumber(item?.duration, 0)}分钟)</View>
                                                <View>{formatMoney(item?.money)}元</View>
                                            </View>
                                            {fareBreakdown.longDistance.filter((item: any) => item?.money || item?.mileage).map((item: any, index: number) => (
                                                <View key={`long-${index}`} className="particulars-detail-item-content">
                                                    <View>{getTypeLabel(item?.type)}({formatNumber(item?.mileage)}公里)</View>
                                                    <View>{formatMoney(item?.money)}元</View>
                                                </View>
                                            ))}
                                        ))}
                                    </View>
                                }
                                {Array.isArray(fareBreakdown?.longDistance) && fareBreakdown.longDistance.length > 0 &&
                                    <View className="particulars-detail-item">
                                        <View className="particulars-detail-item-title">
                                            <View>远途费</View>
                                            <View>{formatMoney(fareBreakdown.longDistance.reduce((sum: number, item: any) => sum + (Number(item?.money) || 0), 0))}元</View>
                                        </View>
                                    }
                                    {Number(fareBreakdown?.redPacketMoney) > 0 &&
                                        <View className="particulars-detail-item">
                                            <View className="particulars-detail-item-title">
                                                <View>红包</View>
                                                <View className="color1">-{formatMoney(fareBreakdown?.redPacketMoney)}元</View>
                                        {fareBreakdown.longDistance.filter((item: any) => item?.money || item?.mileage).map((item: any, index: number) => (
                                            <View key={`long-${index}`} className="particulars-detail-item-content">
                                                <View>{getTypeLabel(item?.type)}({formatNumber(item?.mileage)}公里)</View>
                                                <View>{formatMoney(item?.money)}元</View>
                                            </View>
                                        ))}
                                    </View>
                                }
                                {Number(fareBreakdown?.redPacketMoney) > 0 &&
                                    <View className="particulars-detail-item">
                                        <View className="particulars-detail-item-title">
                                            <View>红包</View>
                                            <View className="color1">-{formatMoney(fareBreakdown?.redPacketMoney)}元</View>
                                        </View>
                                    }
                                    {Number(fareBreakdown?.couponMoney) > 0 &&
                                        <View className="particulars-detail-item">
                                            <View className="particulars-detail-item-title">
                                                <View>优惠券</View>
                                                <View className="color1">-{formatMoney(fareBreakdown?.couponMoney)}元</View>
                                            </View>
                                    </View>
                                }
                                {Number(fareBreakdown?.couponMoney) > 0 &&
                                    <View className="particulars-detail-item">
                                        <View className="particulars-detail-item-title">
                                            <View>优惠券</View>
                                            <View className="color1">-{formatMoney(fareBreakdown?.couponMoney)}元</View>
                                        </View>
                                    }
                                </View>
                            }
                                    </View>
                                }
                            </View>
                            <View className="car-card-end-cost">
                                <View className="car-card-end-cost-money">
                                    全程{formatNumber(fareBreakdown?.kilometer)}公里|{formatNumber(fareBreakdown?.time)}分钟
@@ -581,14 +625,9 @@
                    <View className="car-card-reach-text">点击按钮开车门</View>
                    <Image className='car-card-reach-img' src='https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/f4a18c3f-a2db-45e6-8967-1510355b5e3b.webp' />
                    <View className="car-card-reach-btn">
                        <View className="car-card-reach-btn-item" onClick={() => handleUnlock('trunk', () => setTrunkStatus((prev) => !prev))}>
                            {
                                trunkStatus ?
                                    <Image className="car-card-reach-btn-item-img" src={require('../../../assets/trunk-open.png')} />
                                    :
                                    <Image className="car-card-reach-btn-item-img" src={require('../../../assets/trunk-close.png')} />
                            }
                            <View className="car-card-reach-btn-item-text">{trunkStatus ? '关闭后备箱' : '打开后备箱'}</View>
                        <View className="car-card-reach-btn-item" onClick={() => handleUnlock('trunk', () => setTrunkStatus(true))}>
                            <Image className="car-card-reach-btn-item-img" src={require('../../../assets/trunk-open.png')} />
                            <View className="car-card-reach-btn-item-text">打开后备箱</View>
                        </View>
                        <View className="car-card-reach-btn-item" onClick={handleOpenOverRoutePopup}>
                            <Image className="car-card-reach-btn-item-img" src={require('../../../assets/over-gps-icon.png')} />
src/robotaxi/pages/begin-route/components/css-folder/calling-car.less
@@ -287,7 +287,7 @@
                display: flex;
                flex-direction: column;
                gap: 16px;
                overflow: hidden;
                overflow:auto;
                max-height: 0;
                margin-top: 0;
src/robotaxi/pages/begin-route/components/css-folder/release-route.less
@@ -105,7 +105,9 @@
        }
        .route-content {
            position: relative;
            margin-top: 46px;
            z-index: 10;
            .tip-icon {
                width: 22px;
src/robotaxi/pages/begin-route/components/passenger-notice.tsx
@@ -43,7 +43,7 @@
            return;
        }
        getPrivacyContent(5).then((res: any) => {
            setNoticeContent(res);
            setNoticeContent(res.content);
        }).catch(() => { });
    }, [show]);
src/robotaxi/pages/begin-route/components/release-route.tsx
@@ -18,7 +18,7 @@
    const toSeleteSitePage = (type: string) => {
        Taro.navigateTo({
            url: '/robotaxi/pages/site-search/index?type=' + type
            url: '/robotaxi/pages/site-search/index?pageType=' + type
        })
    }
    const toPriceDescription = () => {
@@ -58,8 +58,8 @@
                <View className="route-content">
                    <View className="route-content-top">
                        <View className="route-content-txt">预估等待{fareEstimate.waitingTime || 0}分钟</View>
                        <View className="route-content-top-money">
                            <Image className="tip-icon" src={tipDefaultIcon}  onClick={() => toPriceDescription()} />
                        <View className="route-content-top-money" onClick={() => toPriceDescription()} >
                            <Image className="tip-icon" src={tipDefaultIcon} />
                            预估
                            <Text className="money-num">{fareEstimate.finalFare || 0}</Text>
                            元
src/robotaxi/pages/begin-route/index.tsx
@@ -19,7 +19,7 @@
import suggest from '../../../robotaxi/assets/suggest.png'
import originIcon from '../../../robotaxi/assets/origin-icon.png'
import wsManager from '../../../utils/websocket';
import { getWalkingRoute } from '../../../utils/Robotaxi-public-method';
import { getWalkingRoute, formatDurationToClock, formatDateTime, getStoredRoutePoints, clearOrderTripStorage } from '../../../utils/Robotaxi-public-method';
import lineRight from '../../../robotaxi/assets/line-right.png';
import {
  saveOrder,
@@ -27,6 +27,7 @@
  queryVehicleByOrderId,
  getOrderPayment,
  cancelOrder,
  queryOrderRoute,
} from '../../../robotaxi/api/begin-route';
export default function SelectMapSite() {
@@ -68,7 +69,7 @@
  const tripStatusRef = useRef(tripStatus); // 行程状态引用
  const routeRequestKeyRef = useRef<string>(''); // 路线请求键
  // 在发布行程状态也允许展示路线
  const shouldShowRoute = [1, 4, 5, 6, 8, 9].includes(tripStatus);
  const shouldShowRoute = [1, 4, 6, 8, 9].includes(tripStatus);
  const authSuccessBound = useRef(false); // 认证成功是否绑定
  const messageBound = useRef(false); // 消息是否绑定
  const orderStatusBound = useRef(false); // 订单状态是否绑定
@@ -79,6 +80,7 @@
  const orderStatusHandlerRef = useRef<(data: any) => void>(); // 订单状态处理函数
  const orderRouteHandlerRef = useRef<(data: any) => void>(); // 订单路线处理函数
  const vehicleLocationHandlerRef = useRef<(data: any) => void>(); // 车辆位置处理函数
  const socketOpenHandlerRef = useRef<(() => void) | null>(null);
  // 记录已为某订单拉取过车辆信息,避免重复
  const lastFetchedVehicleOrderIdRef = useRef<string | null>(null);
  const mapRectRef = useRef<any>(null);
@@ -92,6 +94,7 @@
  }, []);
  const shouldShowWaitingRadar = isAndroid && [2, 3].includes(tripStatus);
  // 加载等待弹窗图片
  useEffect(() => {
    if (![2, 3].includes(tripStatus)) return;
    if (Taro.getEnv?.() !== Taro.ENV_TYPE.WEAPP) {
@@ -122,6 +125,7 @@
    };
  }, [tripStatus]);
  // 更新等待雷达位置
  const updateWaitingRadarPosition = () => {
    if (!shouldShowWaitingRadar) {
      return;
@@ -170,6 +174,7 @@
      .exec();
  };
  // 更新等待雷达位置
  useEffect(() => {
    updateWaitingRadarPosition();
  }, [shouldShowWaitingRadar, startSite?.latitude, startSite?.longitude, longitude, latitude]);
@@ -183,31 +188,37 @@
    }
  };
  // 获取存储的路线点
  const getStoredRoutePoints = (status?: number) => {
    const cached = Taro.getStorageSync('orderRoutePoints') || {};
    const preferredType = typeof status === 'number' && status >= 6 ? 2 : 1;
    const preferred = cached[String(preferredType)];
    if (Array.isArray(preferred) && preferred.length > 0) {
      return preferred;
  // 归一化距离(公里)
  const normalizeDistanceKm = (value: any) => {
    const num = Number(value);
    if (Number.isNaN(num)) {
      return null;
    }
    const latestType = Taro.getStorageSync('orderRouteLatestType');
    const latest = cached[String(latestType)];
    if (Array.isArray(latest) && latest.length > 0) {
      return latest;
    }
    if (Array.isArray(cached.latest) && cached.latest.length > 0) {
      return cached.latest;
    }
    const keys = Object.keys(cached);
    for (const key of keys) {
      if (Array.isArray(cached[key]) && cached[key].length > 0) {
        return cached[key];
      }
    }
    return [];
    return Number((num / 1000).toFixed(1));
  };
  // 归一化时间(秒)
  const normalizeDurationSeconds = (value: any) => {
    const num = Number(value);
    if (Number.isNaN(num)) {
      return null;
    }
    return Math.round(num);
  };
  // 格式化时间显示(分钟或秒)
  const formatDurationDisplay = (value: any) => {
    const num = Number(value);
    if (Number.isNaN(num)) {
      return { value: 0, unit: '分钟' };
    }
    if (num < 60) {
      return { value: Math.max(0, Math.round(num)), unit: '秒' };
    }
    return { value: Math.ceil(num / 60), unit: '分钟' };
  };
  // 请求路线和车费
  const requestRouteAndFare = (originLat: number, originLng: number, destLat: number, destLng: number, force = false) => {
    const requestKey = `${originLng},${originLat}-${destLng},${destLat}`;
    if (!force && requestKey === routeRequestKeyRef.current) {
@@ -239,13 +250,17 @@
      const distance = Number(route?.distance);
      const duration = Number(route?.duration);
      const points = (Array.isArray(route?.polyline) ? route?.polyline : []);
      if (!Number.isNaN(distance)) {
        setRouteDistance(distance);
        Taro.setStorageSync('orderRouteDistance', distance);
      const distanceKm = normalizeDistanceKm(distance);
      if (distanceKm !== null) {
        setRouteDistance(distanceKm);
        Taro.setStorageSync('orderRouteDistance', distanceKm);
        Taro.setStorageSync('orderRouteDistanceUnit', 'km');
      }
      if (!Number.isNaN(duration)) {
        setRouteDuration(duration);
        Taro.setStorageSync('orderRouteDuration', duration);
      const durationSeconds = normalizeDurationSeconds(duration);
      if (durationSeconds !== null) {
        setRouteDuration(durationSeconds);
        Taro.setStorageSync('orderRouteDuration', durationSeconds);
        Taro.setStorageSync('orderRouteDurationUnit', 's');
      }
      if (Array.isArray(points) && points.length > 0) {
        applyRoutePoints(points);
@@ -324,7 +339,7 @@
          })
          .catch(() => { });
      }
      if (payload?.clearRoute) {
      if (mappedTripStatus === 5) {
        setRoutePoints([]);
        setPolyline([]);
      }
@@ -353,16 +368,25 @@
    orderRouteHandlerRef.current = (payload) => {
      const points = payload?.points || [];
      applyRoutePoints(points);
      if (tripStatusRef.current !== 5) {
        applyRoutePoints(points);
      } else {
        setRoutePoints([]);
        setPolyline([]);
      }
      const distance = Number(payload?.content?.distance);
      const duration = Number(payload?.content?.duration);
      if (!Number.isNaN(distance)) {
        setRouteDistance(distance);
        Taro.setStorageSync('orderRouteDistance', distance);
      const distanceKm = normalizeDistanceKm(distance);
      if (distanceKm !== null) {
        setRouteDistance(distanceKm);
        Taro.setStorageSync('orderRouteDistance', distanceKm);
        Taro.setStorageSync('orderRouteDistanceUnit', 'km');
      }
      if (!Number.isNaN(duration)) {
        setRouteDuration(duration);
        Taro.setStorageSync('orderRouteDuration', duration);
      const durationSeconds = normalizeDurationSeconds(duration);
      if (durationSeconds !== null) {
        setRouteDuration(durationSeconds);
        Taro.setStorageSync('orderRouteDuration', durationSeconds);
        Taro.setStorageSync('orderRouteDurationUnit', 's');
      }
    };
    vehicleLocationHandlerRef.current = (payload) => {
@@ -404,12 +428,16 @@
    Taro.setStorageSync('tripStatus', tripStatus);
    tripStatusRef.current = tripStatus;
    let timer: any = null;
    if (tripStatus === 2 || tripStatus === 3) {
      // 状态为2或3时,将地图中心定位到上车点
    if (tripStatus == 2 || tripStatus == 3) {
      const s = Taro.getStorageSync('startSite');
      if (s && s.latitude && s.longitude) {
        setLatitude(s.latitude);
        setLongitude(s.longitude);
        const mapCtx = Taro.createMapContext('map');
        mapCtx.moveToLocation({
          latitude: s.latitude,
          longitude: s.longitude,
        });
      }
      const cachedStartAt = Number(Taro.getStorageSync(WAITING_TIMER_START_KEY));
@@ -431,6 +459,34 @@
      timer = setInterval(() => {
        updateElapsed();
      }, 1000);
    } else if (tripStatus == 5) {
      const s = Taro.getStorageSync('startSite');
      if (s && s.latitude && s.longitude) {
        setLatitude(s.latitude);
        setLongitude(s.longitude);
        const mapCtx = Taro.createMapContext('map');
        mapCtx.moveToLocation({
          latitude: s.latitude,
          longitude: s.longitude,
        });
      }
      setElapsedSeconds(0);
      Taro.removeStorageSync(WAITING_TIMER_START_KEY);
      Taro.removeStorageSync(WAITING_TIMER_ELAPSED_KEY);
    } else if (tripStatus == 7) {
      const e = Taro.getStorageSync('endSite');
      if (e && e.latitude && e.longitude) {
        setLatitude(e.latitude);
        setLongitude(e.longitude);
        const mapCtx = Taro.createMapContext('map');
        mapCtx.moveToLocation({
          latitude: e.latitude,
          longitude: e.longitude,
        });
      }
      setElapsedSeconds(0);
      Taro.removeStorageSync(WAITING_TIMER_START_KEY);
      Taro.removeStorageSync(WAITING_TIMER_ELAPSED_KEY);
    } else {
      setElapsedSeconds(0);
      Taro.removeStorageSync(WAITING_TIMER_START_KEY);
@@ -441,6 +497,7 @@
    };
  }, [tripStatus]);
  // 页面隐藏时保存等待时间
  useDidHide(() => {
    const currentTripStatus = tripStatusRef.current;
    if (currentTripStatus === 2 || currentTripStatus === 3) {
@@ -455,16 +512,81 @@
    }
    Taro.removeStorageSync(WAITING_TIMER_START_KEY);
    Taro.removeStorageSync(WAITING_TIMER_ELAPSED_KEY);
    if (socketOpenHandlerRef.current) {
      wsManager.off('open', socketOpenHandlerRef.current);
      socketOpenHandlerRef.current = null;
    }
  });
  // 监听tripStatus变化,应用存储的路线点
  useEffect(() => {
    if (tripStatus === 5) {
      setRoutePoints([]);
      setPolyline([]);
      return;
    }
    const storedPoints = getStoredRoutePoints(tripStatus);
    applyRoutePoints(storedPoints);
  }, [tripStatus]);
  // 在tripStatus=1时也绘制预估路线与步行路线
  useEffect(() => {
    if ((shouldShowRoute) && (routePoints.length > 0 || walkingPoints.length > 0)) {
    const s = Taro.getStorageSync('startSite');
    const pickupMarker = s?.latitude && s?.longitude ? {
      id: Date.now() + 1,
      latitude: s.latitude,
      longitude: s.longitude,
      iconPath: 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/51e7cb75-72af-4994-9d6e-d999efb2fbc7.png',
      width: 22,
      height: 40,
      anchor: { x: 0.5, y: 1 },
      zIndex: 2,
      name: s?.name || '上车点',
      customCallout: {
        anchorY: [2, 3].includes(tripStatus) ? 150 : -5,
        anchorX: 0,
        display: 'ALWAYS'
      }
    } : null;
    const dropoffMarker = endSite?.latitude && endSite?.longitude ? {
      id: Date.now() + 2,
      latitude: endSite.latitude,
      longitude: endSite.longitude,
      iconPath: 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/6a56b1eb-ec37-4839-8733-262942af070d.png',
      width: 22,
      height: 40,
      anchor: { x: 0.5, y: 1 },
      zIndex: 2,
      name: endSite.name || '目的地',
      customCallout: {
        anchorY: -5,
        anchorX: 0,
        display: 'ALWAYS'
      }
    } : null;
    const carMarker = vehicleLocation ? {
      id: Date.now() + 3,
      latitude: vehicleLocation.latitude,
      longitude: vehicleLocation.longitude,
      iconPath: 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/e51e92e6-700d-4e83-b3e0-14e6f18e0b9a.png',
      width: 40,
      height: 20,
      anchor: { x: 0.5, y: 1 },
      zIndex: 3,
      name: '车辆位置',
      customCallout: { anchorY: 50, anchorX: 0, display: 'ALWAYS' }
    } : null;
    const includePickup = [1, 2, 3, 4, 5, 8, 9].includes(tripStatus);
    const includeDropoff = [1, 6, 7, 8, 9].includes(tripStatus);
    const includeCar = [4, 5, 6, 7].includes(tripStatus);
    const nextMarkers: any[] = [];
    if (includePickup && pickupMarker) nextMarkers.push(pickupMarker);
    if (includeCar && carMarker) nextMarkers.push(carMarker);
    if (includeDropoff && dropoffMarker) nextMarkers.push(dropoffMarker);
    setMarkers(nextMarkers);
    if (shouldShowRoute && (routePoints.length > 0 || walkingPoints.length > 0)) {
      const lines: any[] = [];
      if (routePoints.length > 0) {
        lines.push({
@@ -486,16 +608,60 @@
        });
      }
      setPolyline(lines);
      const mapCtx = Taro.createMapContext('map');
      const allPoints = [
        ...routePoints,
        ...walkingPoints,
      ];
      mapCtx.includePoints({
        points: allPoints,
        padding: [50, 50, 500, 50]
      });
    } else {
      setPolyline([]);
    }
  }, [tripStatus, routePoints, walkingPoints]);
  }, [tripStatus, shouldShowRoute, routePoints, walkingPoints, startSite?.name, startSite?.latitude, startSite?.longitude, endSite.name, endSite?.latitude, endSite?.longitude, vehicleLocation]);
  const bindSocketListeners = () => {
    if (!orderStatusBound.current && orderStatusHandlerRef.current) {
      wsManager.on('order_status', orderStatusHandlerRef.current);
      orderStatusBound.current = true;
    }
    if (!orderRouteBound.current && orderRouteHandlerRef.current) {
      wsManager.on('order_route', orderRouteHandlerRef.current);
      orderRouteBound.current = true;
    }
    if (!vehicleLocationBound.current && vehicleLocationHandlerRef.current) {
      wsManager.on('vehicle_location', vehicleLocationHandlerRef.current);
      vehicleLocationBound.current = true;
    }
  };
  const ensureSocketListeners = () => {
    if (wsManager.isConnectedStatus()) {
      bindSocketListeners();
      return;
    }
    if (socketOpenHandlerRef.current) {
      return;
    }
    const handleOpen = () => {
      bindSocketListeners();
      if (socketOpenHandlerRef.current) {
        wsManager.off('open', socketOpenHandlerRef.current);
        socketOpenHandlerRef.current = null;
      }
    };
    socketOpenHandlerRef.current = handleOpen;
    wsManager.on('open', handleOpen);
  };
  // 页面初始化
  useDidShow(() => {
    const s = Taro.getStorageSync('startSite');
    const e = Taro.getStorageSync('endSite');
    const cachedOrderId = Taro.getStorageSync('orderId');
    const cachedTripStatus = Number(Taro.getStorageSync('tripStatus')) || tripStatus;
    if (s) {
      setStartSite(s);
    }
@@ -510,6 +676,50 @@
        })
        .catch(() => { });
    }
    if (cachedOrderId && [4, 5, 6, 7, 8].includes(cachedTripStatus)) {
      const routeType = cachedTripStatus <= 5 ? 1 : 2;
      queryOrderRoute({ orderId: cachedOrderId, routeType })
        .then((res: any) => {
          const data = res?.data ?? res?.result ?? res;
          const rawPoints = Array.isArray(data?.polyline) ? data.polyline : [];
          const points = rawPoints
            .map((item: any) => ({
              longitude: Number(item?.longitude),
              latitude: Number(item?.latitude),
            }))
            .filter((item: any) => Number.isFinite(item.longitude) && Number.isFinite(item.latitude));
          if (points.length > 0) {
            if (cachedTripStatus !== 5) {
              applyRoutePoints(points);
              const first = points[0];
              setVehicleLocation({ latitude: first.latitude, longitude: first.longitude });
            } else {
              const last = points[points.length - 1];
              setVehicleLocation({ latitude: last.latitude, longitude: last.longitude });
            }
            const cached = Taro.getStorageSync('orderRoutePoints') || {};
            cached[String(routeType)] = points;
            cached.latest = points;
            Taro.setStorageSync('orderRoutePoints', cached);
            Taro.setStorageSync('orderRouteLatestType', routeType);
          }
          const distance = Number(data?.distance);
          const distanceKm = normalizeDistanceKm(distance);
          if (distanceKm !== null) {
            setRouteDistance(distanceKm);
            Taro.setStorageSync('orderRouteDistance', distanceKm);
            Taro.setStorageSync('orderRouteDistanceUnit', 'km');
          }
          const duration = Number(data?.duration);
          const durationSeconds = normalizeDurationSeconds(duration);
          if (durationSeconds !== null) {
            setRouteDuration(durationSeconds);
            Taro.setStorageSync('orderRouteDuration', durationSeconds);
            Taro.setStorageSync('orderRouteDurationUnit', 's');
          }
        })
        .catch(() => { });
    }
    if (s && s.latitude && s.longitude) {
      setLatitude(s.latitude);
@@ -518,14 +728,24 @@
      getLocationInfo();
    }
    const storedPoints = getStoredRoutePoints(tripStatus);
    applyRoutePoints(storedPoints);
    if (cachedTripStatus === 5) {
      applyRoutePoints([]);
    } else if (cachedTripStatus !== 5) {
      applyRoutePoints(storedPoints);
    } else {
      applyRoutePoints([]);
    }
    const storedDistance = Number(Taro.getStorageSync('orderRouteDistance'));
    if (!Number.isNaN(storedDistance)) {
      setRouteDistance(storedDistance);
    const storedDistanceUnit = Taro.getStorageSync('orderRouteDistanceUnit');
    const normalizedStoredDistance = storedDistanceUnit === 'km' ? storedDistance : normalizeDistanceKm(storedDistance);
    if (normalizedStoredDistance !== null) {
      setRouteDistance(normalizedStoredDistance);
    }
    const storedDuration = Number(Taro.getStorageSync('orderRouteDuration'));
    if (!Number.isNaN(storedDuration)) {
      setRouteDuration(storedDuration);
    const storedDurationUnit = Taro.getStorageSync('orderRouteDurationUnit');
    const normalizedStoredDuration = storedDurationUnit === 's' ? storedDuration : Math.round(storedDuration * 60);
    if (!Number.isNaN(normalizedStoredDuration)) {
      setRouteDuration(normalizedStoredDuration);
    }
    const originLng = s?.longitude || longitude;
    const originLat = s?.latitude || latitude;
@@ -537,6 +757,9 @@
      updateWalkingRouteForStart();
    }
    if (cachedOrderId) {
      ensureSocketListeners();
    }
  });
  useEffect(() => {
@@ -552,94 +775,6 @@
  }, [startSite?.longitude, startSite?.latitude, endSite?.longitude, endSite?.latitude, longitude, latitude]);
  // 生成标记点
  useEffect(() => {
    const s = Taro.getStorageSync('startSite');
    const pickupMarker = s?.latitude && s?.longitude ? {
      id: Date.now() + 1,
      latitude: s.latitude,
      longitude: s.longitude,
      iconPath: 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/51e7cb75-72af-4994-9d6e-d999efb2fbc7.png',
      width: 22,
      height: 40,
      anchor: { x: 0.5, y: 1 },
      zIndex: 2,
      name: s?.name || '上车点',
      customCallout: {
        anchorY: [2, 3].includes(tripStatus) ? 150 : 8,
        anchorX: 0,
        display: 'ALWAYS'
      }
    } : null;
    const dropoffMarker = endSite?.latitude && endSite?.longitude ? {
      id: Date.now() + 2,
      latitude: endSite.latitude,
      longitude: endSite.longitude,
      iconPath: 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/6a56b1eb-ec37-4839-8733-262942af070d.png',
      width: 22,
      height: 40,
      anchor: { x: 0.5, y: 1 },
      zIndex: 2,
      name: endSite.name || '目的地',
      customCallout: {
        anchorY: 8,
        anchorX: 0,
        display: 'ALWAYS'
      }
    } : null;
    const carMarker = vehicleLocation ? {
      id: Date.now() + 3,
      latitude: vehicleLocation.latitude,
      longitude: vehicleLocation.longitude,
      iconPath: 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/e51e92e6-700d-4e83-b3e0-14e6f18e0b9a.png',
      width: 40,
      height: 20,
      anchor: { x: 0.5, y: 1 },
      zIndex: 3,
      name: '车辆位置',
      customCallout: { anchorY: 0, anchorX: 0, display: 'ALWAYS' }
    } : null;
    const includePickup = [1, 2, 3, 4, 5, 8, 9].includes(tripStatus);
    const includeDropoff = [1, 6, 7, 8, 9].includes(tripStatus);
    const includeCar = [4, 5, 6, 7].includes(tripStatus);
    const nextMarkers: any[] = [];
    if (includePickup && pickupMarker) nextMarkers.push(pickupMarker);
    if (includeCar && carMarker) nextMarkers.push(carMarker);
    if (includeDropoff && dropoffMarker) nextMarkers.push(dropoffMarker);
    setMarkers(nextMarkers);
  }, [tripStatus, startSite?.name, startSite?.latitude, startSite?.longitude, endSite.name, endSite?.latitude, endSite?.longitude, vehicleLocation])
  // 调整视图以包含所有路线点(含步行)
  useEffect(() => {
    if (shouldShowRoute && (routePoints.length > 0 || walkingPoints.length > 0)) {
      const mapCtx = Taro.createMapContext('map');
      const allPoints = [
        ...routePoints,
        ...walkingPoints,
      ];
      mapCtx.includePoints({
        points: allPoints,
        padding: [50, 50, 500, 50]
      });
    }
  }, [tripStatus, routePoints, walkingPoints]);
  const formatTime = (sec: number) => {
    const m = Math.floor(sec / 60);
    const s = sec % 60;
    return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
  }
  const formatDateTime = (date: Date) => {
    const y = date.getFullYear();
    const mo = String(date.getMonth() + 1).padStart(2, '0');
    const d = String(date.getDate()).padStart(2, '0');
    const h = String(date.getHours()).padStart(2, '0');
    const mi = String(date.getMinutes()).padStart(2, '0');
    const s = String(date.getSeconds()).padStart(2, '0');
    return `${y}-${mo}-${d} ${h}:${mi}:${s}`;
  };
  // 获取用户位置信息
  const getLocationInfo = () => {
@@ -737,12 +872,7 @@
    setShowCancelPopup(false);
    setTripStatus(1);
    setOrderId('');
    Taro.removeStorageSync('orderRouteDuration');
    Taro.removeStorageSync('orderRouteDistance');
    Taro.removeStorageSync('orderRoutePoints');
    Taro.removeStorageSync('orderRouteLatestType');
    Taro.removeStorageSync('orderId');
    Taro.removeStorageSync('tripStatus');
    clearOrderTripStorage();
    updateWalkingRouteForStart();
    const s = Taro.getStorageSync('startSite');
    const e = Taro.getStorageSync('endSite');
@@ -858,13 +988,8 @@
  const toBack = () => {
    if (tripStatus == 9 || tripStatus == 1) {
      Taro.removeStorageSync('tripStatus');
      clearOrderTripStorage();
      Taro.removeStorageSync('endSite');
      Taro.removeStorageSync('orderRouteDuration');
      Taro.removeStorageSync('orderRouteDistance');
      Taro.removeStorageSync('orderRoutePoints');
      Taro.removeStorageSync('orderRouteLatestType');
      Taro.removeStorageSync('orderId');
    }
    Taro.switchTab({
      url: '/pages/robotaxi/home/index'
@@ -881,6 +1006,8 @@
      updateWaitingRadarPosition();
    }
  };
  const durationDisplay = formatDurationDisplay(routeDuration);
  return (
    <View className="home-container">
@@ -907,7 +1034,7 @@
                  <CoverView className="waiting-callout-content">
                    {/* <CoverView className="waiting-callout-bg">
                      <CoverImage className="waiting-callout-bg-img" src={waitingCalloutBgSrc} />
                      <CoverView className="waiting-callout-bg-text">{formatTime(elapsedSeconds)}</CoverView>
                      <CoverView className="waiting-callout-bg-text">{formatDurationToClock(elapsedSeconds)}</CoverView>
                    </CoverView> */}
                    <CoverView className="waiting-callout-txt">
                      <CoverView className="waiting-callout-txt-content">{item.name}</CoverView>
@@ -924,16 +1051,19 @@
                  (!['车辆位置', '已到达'].includes(item.name)) &&
                  <CoverView className="custom-callout-content">
                    {item.name}
                    {
                      tripStatus == 1 && <CoverImage className="custom-callout-icon" src='https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/f9e285fc-4c09-404d-90f8-4bac178565d0.png' />
                    }
                  </CoverView>
                }
                {
                  (item.name === '车辆位置' && [4, 5, 6].includes(tripStatus)) &&
                  (item.name === '车辆位置' && [4, 6].includes(tripStatus)) &&
                  <CoverView className="custom-callout-content">
                    <CoverView className="car-time">
                      <CoverView className="car-time-num">{routeDistance ?? 0}</CoverView>
                      <CoverView>公里</CoverView>
                      <CoverView className="car-time-num">{routeDuration ?? 0}</CoverView>
                      <CoverView>分钟</CoverView>
                      <CoverView className="car-time-num">{durationDisplay.value}</CoverView>
                      <CoverView>{durationDisplay.unit}</CoverView>
                    </CoverView>
                  </CoverView>
                }
@@ -1005,7 +1135,7 @@
                  updateStatus={() => setTripStatus(prev => prev + 1)}
                  handleOpenEvaluatePopup={handleOpenEvaluatePopup}
                  estimatedWaitMinutes={fareEstimate?.waitingTime}
                  routeDurationMinutes={routeDuration}
                  routeDurationSeconds={routeDuration}
                />
              }
            </View>
src/robotaxi/pages/expense-description/index.tsx
@@ -14,9 +14,11 @@
  const [fareData, setFareData] = useState<FareBreakdownResponse | null>(null);
  const [faqData, setFaqData] = useState<FAQItem[]>([]);
  const [showEvaluatePopup, setShowEvaluatePopup] = useState(false);
  const [orderId, setOrderId] = useState('');
  useEffect(() => {
    fetchData(router.params?.orderId);
    setOrderId(router.params?.orderId || '');
    fetchData(router.params?.orderId || '');
    fetchFAQ();
  }, []);
@@ -60,9 +62,7 @@
      return;
    }
    const firstId = faqData[0]?.id;
    if (typeof firstId === 'number') {
      setExpandedItems([firstId]);
    }
    setExpandedItems([firstId]);
  }, [faqData, expandedItems.length]);
  const getTimeLabel = (type: number) => {
@@ -84,6 +84,10 @@
    return (amount || 0).toFixed(2);
  };
  const toRulePage = () => {
    Taro.navigateTo({ url: '/robotaxi/pages/pricing-rules/index' });
  };
  if (!fareData) return null;
  return (
@@ -92,7 +96,7 @@
      <View className='order-info'>
        <View className='flex-between-card'>
          <View className='title'>账单信息</View>
          <View className='look-rule' onClick={() => Taro.navigateTo({ url: '/robotaxi/pages/expense-rule/index' })}>
          <View className='look-rule' onClick={() => toRulePage()}>
            <Text className='look-rule-txt'>查看计价规则</Text>
            <Image className='arrow-icon' src={Subtract}></Image>
          </View>
@@ -141,17 +145,41 @@
            </View>
          ))}
          {/* 优惠券 */}
          {/* 远途费 */}
          <View className='flex-between-card'>
            <View className='price-label'>优惠券</View>
            <View className='price-value color-red'>-{formatMoney(fareData.couponMoney)}元</View>
            <View className='price-label'>远途费</View>
            <View className='price-value'>{sumMoney(fareData.longDistance)}元</View>
          </View>
          {fareData.longDistance?.map((item, index) => (
            <View className='flex-between-card mb-16' key={index}>
              <View className='price-notice-label'>{getTimeLabel(item.type)}({item.mileage}公里)</View>
              <View className='price-notice-value'>{formatMoney(item.money)}元</View>
            </View>
          ))}
          {/* 红包 */}
          {
            fareData.redPacketMoney > 0 &&
            <View className='flex-between-card mb-16'>
              <View className='price-label'>红包</View>
              <View className='price-value color-red'>-{formatMoney(fareData.redPacketMoney)}元</View>
            </View>
          }
          {/* 优惠券 */}
          {
            fareData.couponMoney > 0 &&
            <View className='flex-between-card'>
              <View className='price-label'>优惠券</View>
              <View className='price-value color-red'>-{formatMoney(fareData.couponMoney)}元</View>
            </View>
          }
        </View>
        {/* 总价格 */}
        <View className='flex-card total'>
          <Text className='total-text'>总费用</Text>
          <Text className='num'>{formatMoney(fareData.payMoney)}</Text>
          <Text className='num'>{formatMoney(fareData.orderMoney)}</Text>
          <Text className='unit'>元</Text>
        </View>
      </View>
src/robotaxi/pages/order/index.tsx
@@ -2,7 +2,7 @@
import { Component, useState, useEffect } from 'react';
import { View, Text, Image, Input } from '@tarojs/components';
import './index.less';
import { getOrderHistoryList, OrderHistoryGroup, OrderHistoryOrder } from '../../../robotaxi/api/order';
import { getOrderHistoryList, OrderHistoryGroup } from '../../../robotaxi/api/order';
export default function OrderPage() {
  const [groups, setGroups] = useState<OrderHistoryGroup[]>([])
@@ -49,8 +49,24 @@
    })
  }
  const againOrder = (item?: OrderHistoryOrder) => {
    Taro.navigateTo({
  const againOrder = (item: any) => {
    Taro.setStorageSync('startSite', {
      id: item.startPointId || '',
      name: item.startPointName || '',
      latitude: item.startLat || 0,
      longitude: item.startLon || 0,
      electronicAreaId: item.xpElectronicFenceAreaId || '',
      time: item.startParkingTimeLimit || 0,
    })
    Taro.setStorageSync('endSite', {
      id: item.endPointId || '',
      name: item.endPointName || '',
      latitude: item.endLat || 0,
      longitude: item.endLon || 0,
      electronicAreaId: item.xpElectronicFenceAreaId || '',
      time: item.endParkingTimeLimit || 0,
    })
    Taro.switchTab({
      url: '/pages/robotaxi/home/index'
    })
  }
@@ -66,7 +82,7 @@
  return (
    <View className='order-page'>
      {groups.length > 0 && groups.map((group, gi) => {
        return <View key={group.dateLabel || gi}>
        return (<View key={group.dateLabel || gi}>
          {(group.orders || []).map((item, index) => {
            return <View className='order-item' key={item.orderId || index} onClick={() => toOrderDetail(item)}>
              <View className='order-time'>{`${item.time} ${item.orderTime}` || formatTime(item.time)}</View>
@@ -77,7 +93,7 @@
              </View>
            </View>
          })}
        </View>
        </View>)
      })}
      {
        !groups.length && <View className='no-data-box'>
src/robotaxi/pages/pricing-rules/index.tsx
@@ -1,24 +1,24 @@
import Taro, { useRouter } from '@tarojs/taro';
import Taro from '@tarojs/taro';
import { useState, useEffect } from 'react';
import { View, Text, RichText } from '@tarojs/components';
import { getPricingRules } from '../../api/pricing';
import './index.less';
export default function PricingRulesPage() {
  const router = useRouter();
  const { cityID } = router.params;
  const [pricingData, setPricingData] = useState<any>(null);
  const [loading, setLoading] = useState(true);
  const [pricingHtml, setPricingHtml] = useState<string>('');
  const [pricingNodes, setPricingNodes] = useState<any[]>([]);
  useEffect(() => {
    if (cityID) {
      fetchPricingRules(cityID);
    const cachedCity = Taro.getStorageSync('selectedCity');
    const cachedCityId = cachedCity?.id;
    if (cachedCityId) {
      fetchPricingRules(String(cachedCityId));
    } else {
      setLoading(false);
    }
  }, [cityID]);
  }, []);
  const extractHtmlFromData = (data: any): string => {
    if (!data) return '';
src/robotaxi/pages/ride-guide/index.config.ts
New file
@@ -0,0 +1,3 @@
export default {
  navigationBarTitleText: '乘车指南'
};
src/robotaxi/pages/ride-guide/index.less
New file
@@ -0,0 +1,5 @@
.ride-guide {
  height: 100%;
  width: 100%;
  overflow-y: auto;
}
src/robotaxi/pages/ride-guide/index.tsx
New file
@@ -0,0 +1,22 @@
import Taro from "@tarojs/taro";
import { View, Image } from "@tarojs/components";
import { useEffect, useState } from 'react';
import './index.less'
import { getRideGuideList } from '../../api/home';
export default function PassengerNotice() {
  const [content, setContent] = useState('');
  useEffect(() => {
    getRideGuideList().then((res: any) => {
      const { url } = JSON.parse(res?.guides?.[0]?.bannerImage)
      setContent(url);
    }).catch(() => { });
  }, []);
  return (
    <View className='ride-guide'>
      <Image src={content} style={{ width: '100%', height: '100%' }} mode="aspectFit" />
    </View>
  )
}
src/robotaxi/pages/select-map-site/index.tsx
@@ -192,7 +192,10 @@
  };
  const openService = () => {
    setShowCustomerService(true);
    const phoneNumber = Taro.getStorageSync('customerServicePhone');
    Taro.makePhoneCall({
      phoneNumber,
    })
  }
@@ -311,14 +314,14 @@
    });
  }, [pageType, userLatitude, userLongitude, latitude, longitude, selectedSite?.latitude, selectedSite?.longitude]);
  useEffect(() => {
    if (!polyline.length || !polyline[0]?.points?.length) return;
    const mapCtx = Taro.createMapContext('map');
    mapCtx.includePoints({
      points: polyline[0].points,
      padding: [80, 80, 200, 80]
    });
  }, [polyline]);
  // useEffect(() => {
  //   if (!polyline.length || !polyline[0]?.points?.length) return;
  //   const mapCtx = Taro.createMapContext('map');
  //   mapCtx.includePoints({
  //     points: polyline[0].points,
  //     padding: [80, 80, 200, 80]
  //   });
  // }, [polyline]);
  const toCoupon = () => {
    Taro.navigateTo({
src/utils/Robotaxi-public-method.ts
@@ -315,3 +315,59 @@
    };
  }
};
// 格式化秒数为时钟时间(分钟:秒)
export const formatDurationToClock = (sec: number) => {
  const safeSeconds = Math.max(0, sec);
  const m = Math.floor(safeSeconds / 60);
  const s = safeSeconds % 60;
  return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
};
// 格式化日期时间
export const formatDateTime = (date: Date) => {
  const y = date.getFullYear();
  const mo = String(date.getMonth() + 1).padStart(2, '0');
  const d = String(date.getDate()).padStart(2, '0');
  const h = String(date.getHours()).padStart(2, '0');
  const mi = String(date.getMinutes()).padStart(2, '0');
  const s = String(date.getSeconds()).padStart(2, '0');
  return `${y}-${mo}-${d} ${h}:${mi}:${s}`;
};
// 获取存储的路线点
export const getStoredRoutePoints = (status?: number) => {
  const cached = Taro.getStorageSync('orderRoutePoints') || {};
  const preferredType = typeof status === 'number' && status >= 6 ? 2 : 1;
  const preferred = cached[String(preferredType)];
  if (Array.isArray(preferred) && preferred.length > 0) {
    return preferred;
  }
  const latestType = Taro.getStorageSync('orderRouteLatestType');
  const latest = cached[String(latestType)];
  if (Array.isArray(latest) && latest.length > 0) {
    return latest;
  }
  if (Array.isArray(cached.latest) && cached.latest.length > 0) {
    return cached.latest;
  }
  const keys = Object.keys(cached);
  for (const key of keys) {
    if (Array.isArray(cached[key]) && cached[key].length > 0) {
      return cached[key];
    }
  }
  return [];
};
// 清除订单行程存储
export const clearOrderTripStorage = () => {
  Taro.removeStorageSync('orderRouteDuration');
  Taro.removeStorageSync('orderRouteDurationUnit');
  Taro.removeStorageSync('orderRouteDistance');
  Taro.removeStorageSync('orderRouteDistanceUnit');
  Taro.removeStorageSync('orderRoutePoints');
  Taro.removeStorageSync('orderRouteLatestType');
  Taro.removeStorageSync('orderId');
  Taro.removeStorageSync('tripStatus');
};
src/utils/request.ts
@@ -151,7 +151,7 @@
  // 获取uid
  const uid = await getStorage(_UID) || '-1';
  header['XP-Uid'] = uid.toString();
  header['XP-Uid'] = '20260227200715' || uid.toString();
  // 获取设备id
  header['XP-Deviceid'] = Taro.getStorageSync('wxOpenId') || 'default_device_id';
src/utils/tools.ts
@@ -543,21 +543,22 @@
// 查看本地是否已有otp
export function checkOtp(autoLogin?, hideBack?) {
  return new Promise((resolve) => {
    getStorage('otp')
      .then(res => {
        resolve(res);
        // 无otp,发起登录请求
        if (!res && autoLogin) {
          if (hideBack) {
            jumpTo({ url: '/pages/user/login?hideBack=1' });
          } else {
            jumpTo({ url: '/pages/user/login' });
          }
        }
      })
      .catch(() => {
        resolve('');
      });
    resolve(true)
    // getStorage('otp')
    //   .then(res => {
    //     resolve(res);
    //     // 无otp,发起登录请求
    //     if (!res && autoLogin) {
    //       if (hideBack) {
    //         jumpTo({ url: '/pages/user/login?hideBack=1' });
    //       } else {
    //         jumpTo({ url: '/pages/user/login' });
    //       }
    //     }
    //   })
    //   .catch(() => {
    //     resolve('');
    //   });
  });
}
src/utils/websocket.ts
@@ -42,7 +42,7 @@
    const uid = await getStorage(_UID) || '-1';
    const task = Taro.connectSocket({
      header: {
        'XP-Uid': uid.toString(),
        'XP-Uid': '20260227200715' || uid.toString(),
      },
      url: this.url,
      success: () => { },
@@ -88,7 +88,7 @@
        this.reconnectAttempts = 0;
        this.startHeartbeat();
        const uid = await getStorage(_UID) || '-1';
        this.send({ type: 'auth', appUserId: uid, timestamp: Date.now() });
        this.send({ type: 'auth', appUserId: '20260227200715', timestamp: Date.now() });
        this.notifyListeners('open', { status: 'connected' });
      });
@@ -136,7 +136,7 @@
      this.reconnectAttempts = 0;
      this.startHeartbeat();
      const uid = await getStorage(_UID) || '-1';
      this.send({ type: 'auth', appUserId: '12345678', timestamp: Date.now() });
      this.send({ type: 'auth', appUserId: '20260227200715', timestamp: Date.now() });
      this.notifyListeners('open', { status: 'connected' });
    });
@@ -339,10 +339,13 @@
      // 2-12 新增
      const points = this.normalizeRoutePoints(content);
      if (!Number.isNaN(duration)) {
        Taro.setStorageSync('orderRouteDuration', duration);
        Taro.setStorageSync('orderRouteDuration', Math.round(duration));
        Taro.setStorageSync('orderRouteDurationUnit', 's');
      }
      if (!Number.isNaN(distance)) {
        Taro.setStorageSync('orderRouteDistance', distance);
        const distanceKm = Number((distance / 1000).toFixed(1));
        Taro.setStorageSync('orderRouteDistance', distanceKm);
        Taro.setStorageSync('orderRouteDistanceUnit', 'km');
      }
      if (points.length > 0) {
        const cached = Taro.getStorageSync('orderRoutePoints') || {};