卢春龙
2026-04-24 aa8038c44dc8e2eda1e801b8f44f54d00fd649e3
Merge branch 'feature_urgency_fix' into 'develop'

feat(begin-route): 优化行程状态处理并添加协议校验

See merge request robotaxi1/xp-robotaxi-miniprogram!16
18个文件已修改
330 ■■■■■ 已修改文件
src/pages/robotaxi/home/components/map-overlay-tips/index.tsx 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/index.less 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/index.tsx 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/begin-route-view.tsx 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car-modules/door-trunk-control.tsx 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car-modules/trip-status-header.tsx 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car.tsx 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/cancel-popup.tsx 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-map-markers.ts 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-order-actions.ts 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-popup-manager.ts 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-route-manager.ts 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-vehicle-tracking.ts 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-web-socket-events.ts 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/index.tsx 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/utils/format.ts 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/websocket.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/components/map-overlay-tips/index.tsx
@@ -19,14 +19,28 @@
  onOpenSetting: () => void;
  /** 查看运营区域 */
  onViewOperatingArea: () => void;
  /** 初始化锚点底部偏移 */
  anchorBottom?: number;
  /** 透明度,用于配合上划消失 */
  opacity?: number;
  /** 拖拽状态,用于控制过渡动画 */
  isDragging?: boolean;
};
/**
 * 地图顶部提示层。
 */
export default function MapOverlayTips(props: Props) {
  const anchorStyle = typeof props.anchorBottom === 'number'
    ? {
        top: 'auto',
        bottom: `${props.anchorBottom}px`,
        opacity: props.opacity !== undefined ? props.opacity : 1,
        transition: props.isDragging ? 'none' : 'opacity 0.2s ease-out'
      }
    : undefined;
  return (
    <View className="content-box-top">
    <View className="content-box-top" style={anchorStyle}>
      <View className="position-box">
        {
          props.isGuest && <View className="visitor-stste ml-32">
src/pages/robotaxi/home/index.less
@@ -61,7 +61,7 @@
  position: fixed;
  top: 55%;
  left: 0px;
  z-index: 1;
  z-index: 2;
  .position-box {
src/pages/robotaxi/home/index.tsx
@@ -101,6 +101,7 @@
  const [bannerList, setBannerList] = useState<any[]>([]); // 轮播图列表
  const [orderData, setOrderData] = useState<any>({}); // 订单数据
  const [isLogin, setIsLogin] = useState(false); // 是否登录
  const [overlayAnchorBottom, setOverlayAnchorBottom] = useState<number | null>(null); // 浮层初始化锚点(距底部)
  const {
    boxHeight,
    originOpacity,
@@ -320,6 +321,9 @@
  useDidShow(() => {
    bootstrap.onDidShow();
    setTimeout(() => {
      initOverlayAnchor();
    }, 80);
  });
  const { handleRegionChange, queryPointerByCenter } = useRegionChange({
@@ -359,6 +363,35 @@
  const handleCloseCustomerService = () => {
    setShowCustomerService(false);
  };
  // 初始化浮层锚点:固定在 content-box 顶部上方 32px
  const initOverlayAnchor = () => {
    // content-box 高度受 banner 数量影响:多 banner 复用 boxHeight,单 banner 走渲染后实测高度
    if (bannerList.length > 1) {
      const bottom = Math.max(0, 280 + 32);
      if (bottom <= 0) return;
      setOverlayAnchorBottom(bottom);
      return;
    }
    const query = Taro.createSelectorQuery();
    query.select('.content-box').boundingClientRect();
    query.exec((res) => {
      const rect = res?.[0];
      const height = Number(rect?.height);
      if (!Number.isFinite(height) || height <= 0) return;
      const bottom = Math.max(0, height + 32);
      setOverlayAnchorBottom(bottom);
    });
  };
  useEffect(() => {
    const timer = setTimeout(() => {
      Taro.nextTick(() => {
        initOverlayAnchor();
      });
    }, 80);
    return () => clearTimeout(timer);
  }, [bannerList.length, tripStatus]);
  // 处理点击地图 marker
  const handleMarkerTap = (e: any) => {
@@ -410,7 +443,15 @@
          <Image className="select-city-icon" src={selectCityIcon} />
        </View>
        {/* 定位图标 */}
        <View className="origin-box" style={{ opacity: originOpacity, transition: isDragging.current ? 'none' : 'opacity 0.2s ease-out' }}>
        <View
          className="origin-box"
          style={{
            opacity: originOpacity,
            transition: isDragging.current ? 'none' : 'opacity 0.2s ease-out',
            top: typeof overlayAnchorBottom === 'number' ? 'auto' : undefined,
            bottom: typeof overlayAnchorBottom === 'number' ? `${overlayAnchorBottom}px` : undefined,
          }}
        >
          <Image onClick={toOrigin} className="origin-icon" src={originIcon} />
        </View>
        <MapOverlayTips
@@ -420,6 +461,9 @@
          isMaintain={isMaintain}
          maintainBounce={maintainBounce}
          hotfixEndText={hotfixEndText}
          anchorBottom={overlayAnchorBottom ?? undefined}
          opacity={originOpacity}
          isDragging={isDragging.current}
          onShowPrivacy={() => setShowPrivacy(true)}
          onOpenSetting={handleOpenSetting}
          onViewOperatingArea={handleViewOperatingArea}
src/robotaxi/pages/begin-route/components/begin-route-view.tsx
@@ -9,6 +9,7 @@
import PassengerNotice from './passenger-notice';
import EvaluatePopup from './evaluate-popup';
import TipPopup from '../../select-map-site/components/tip-popup';
import PrivacyPopup from '../../../../pages/robotaxi/home/components/privacy-popup';
type Props = {
  mapLongitude: number;
@@ -38,6 +39,7 @@
  handleCloseCustomerService: () => void;
  showCancelPopup: boolean;
  cancelType: number;
  cancelPopupWaitMinutes: number | null;
  startSite: any;
  endSite: any;
  fareEstimate: any;
@@ -53,6 +55,8 @@
  showTipPopup: boolean;
  handleCloseTipPopup: () => void;
  tipType: string;
  showPrivacyPopup: boolean;
  handleClosePrivacyPopup: () => void;
  showPassengerNotice: boolean;
  handleClosePassengerNotice: () => void;
  changeCheckedEnter: (checked: boolean) => void;
@@ -91,6 +95,7 @@
    handleCloseCustomerService,
    showCancelPopup,
    cancelType,
    cancelPopupWaitMinutes,
    startSite,
    endSite,
    fareEstimate,
@@ -106,6 +111,8 @@
    showTipPopup,
    handleCloseTipPopup,
    tipType,
    showPrivacyPopup,
    handleClosePrivacyPopup,
    showPassengerNotice,
    handleClosePassengerNotice,
    changeCheckedEnter,
@@ -163,7 +170,7 @@
        cancelType={cancelType}
        startSite={startSite}
        endSite={endSite}
        waitMinutes={fareEstimate?.waitingTime}
        waitMinutes={cancelPopupWaitMinutes ?? fareEstimate?.waitingTime}
        tripStatus={tripStatus}
        routeDurationSeconds={routeDuration}
        discountAmount={fareEstimate?.discountAmount}
@@ -184,6 +191,11 @@
        onClose={handleCloseTipPopup}
        tipType={tipType}
      />
      <PrivacyPopup
        show={showPrivacyPopup}
        onClose={handleClosePrivacyPopup}
        onAgree={handleClosePrivacyPopup}
      />
      {!showCancelPopup && (
        <PassengerNotice
          show={showPassengerNotice}
src/robotaxi/pages/begin-route/components/calling-car-modules/door-trunk-control.tsx
@@ -8,20 +8,17 @@
    orderId?: string;
    orderTrunkStatus?: boolean;
    handleOpenOverRoutePopup?: () => void;
    handleOpenAnnouncementsPopup?: () => void;
};
export default function DoorTrunkControl({
    tripStatus,
    orderId,
    orderTrunkStatus,
    handleOpenOverRoutePopup,
    handleOpenAnnouncementsPopup
    handleOpenOverRoutePopup
}: Props) {
    const [trunkStatus, setTrunkStatus] = useState(orderTrunkStatus); // 后备箱开关状态
    const [lockStatus, setLockStatus] = useState(false); // 车门解锁状态
    const trunkPollTimerRef = useRef<any>(null); // 后备箱状态轮询定时器
    const hasShownAnnouncementsRef = useRef(false); // 安全须知是否已展示
    // 外部后备箱状态变化时同步本地状态
    useEffect(() => {
@@ -157,10 +154,6 @@
            if (doorType === 'door') {
                Taro.hideLoading();
                Taro.showToast({ title: '右侧车门已解锁', icon: 'none' });
                if (!hasShownAnnouncementsRef.current) {
                    handleOpenAnnouncementsPopup?.();
                    hasShownAnnouncementsRef.current = true;
                }
            }
        }).catch(() => {
            Taro.hideLoading();
src/robotaxi/pages/begin-route/components/calling-car-modules/trip-status-header.tsx
@@ -12,7 +12,7 @@
    finishCountdownEnabled: boolean;
    finishCountdownSeconds: number | null;
    limitMinutes: number | null;
    handleOpenCancelPopup?: (durationDisplay: any) => void;
    handleOpenCancelPopup?: (displayInfo: { durationDisplay: { value: string | number; unit: string }; waitMinutes: number | null }) => void;
};
export default function TripStatusHeader({
@@ -30,7 +30,9 @@
    // 标题行文案映射
    const titleMap = {
        3: <>预计<View className="time">{waitMinutes}</View>分钟内有车应答</>,
        4: <>车辆还有<View className="time">{durationDisplay.value}</View>{durationDisplay.unit}到达</>,
        4: isArriveSoon
            ? <>即将到达</>
            : <>车辆还有<View className="time">{durationDisplay.value}</View>{durationDisplay.unit}到达</>,
        5: <>车辆已到达上车点<View className="time" style={isPickupCountdownSoon ? { color: '#E03434' } : undefined}>{pickupCountdownSeconds !== null ? formatCountdown(pickupCountdownSeconds) : '--:--'}</View></>,
        6: <>预计<View className="time">{durationDisplay.value}</View>{durationDisplay.unit}抵达目的地</>,
        7: (finishCountdownEnabled || finishCountdownSeconds !== null)
@@ -63,7 +65,7 @@
            {
                [3, 4, 5].includes(tripStatus) &&
                <View className="cancel-btn">
                    <Image onClick={() => handleOpenCancelPopup?.(durationDisplay)} src={require('../../../../assets/cancel-btn.png')} className="cancel" />
                    <Image onClick={() => handleOpenCancelPopup?.({ durationDisplay, waitMinutes })} src={require('../../../../assets/cancel-btn.png')} className="cancel" />
                </View>
            }
        </View>
src/robotaxi/pages/begin-route/components/calling-car.tsx
@@ -1,6 +1,6 @@
import { View, Image } from "@tarojs/components";
import './css-folder/calling-car.less'
import { formatDurationDisplay } from '../../../../utils/Robotaxi-public-method';
import { formatRemainingDurationDisplay } from '../../../utils/format';
import useTripCountdown from './calling-car-modules/use-trip-countdown';
import TripStatusHeader from './calling-car-modules/trip-status-header';
@@ -13,8 +13,8 @@
type Props = {
    /** 行程状态:3呼叫中、4即将到达、5已到达上车点、6行程中、7到达终点、8待支付、9已支付 */
    tripStatus?: number;
    /** 打开取消弹窗,入参为预计到达展示文案 */
    handleOpenCancelPopup?: (durationDisplay: any) => void;
    /** 打开取消弹窗,入参为顶部卡片当前展示信息 */
    handleOpenCancelPopup?: (displayInfo: { durationDisplay: { value: string | number; unit: string }; waitMinutes: number | null }) => void;
    /** 上车点超时后的回调 */
    handleCancelTimeout?: () => void;
    /** 到达终点后倒计时结束回调 */
@@ -35,8 +35,6 @@
    handlePay?: () => void;
    /** 打开详细评价弹窗 */
    handleOpenEvaluatePopup?: () => void;
    /** 首次开门后打开安全须知弹窗 */
    handleOpenAnnouncementsPopup?: () => void;
    /** 预估等待分钟数 */
    estimatedWaitMinutes?: number;
    /** 预估行程秒数 */
@@ -67,7 +65,6 @@
    onToggleDetailFade,
    handlePay,
    handleOpenEvaluatePopup,
    handleOpenAnnouncementsPopup,
    estimatedWaitMinutes,
    routeDurationSeconds,
    finishCountdownEnabled = false,
@@ -94,7 +91,7 @@
    });
    /** 预计到达时长展示(秒/分钟) */
    const durationDisplay = formatDurationDisplay(routeDurationSeconds);
    const durationDisplay = formatRemainingDurationDisplay(routeDurationSeconds);
    /** 车辆即将到达上车点(<=60秒) */
    const isArriveSoon = tripStatus === 4 && (typeof routeDurationSeconds === 'number' && routeDurationSeconds <= 60);
    /** 上车倒计时即将结束(<60秒) */
@@ -130,7 +127,6 @@
                        orderId={orderId} 
                        orderTrunkStatus={orderTrunkStatus}
                        handleOpenOverRoutePopup={handleOpenOverRoutePopup}
                        handleOpenAnnouncementsPopup={handleOpenAnnouncementsPopup}
                    />
                    
                    <FareDetailCard 
@@ -149,7 +145,6 @@
                orderId={orderId} 
                orderTrunkStatus={orderTrunkStatus}
                handleOpenOverRoutePopup={handleOpenOverRoutePopup}
                handleOpenAnnouncementsPopup={handleOpenAnnouncementsPopup}
            />
            <EvaluationCard 
src/robotaxi/pages/begin-route/components/cancel-popup.tsx
@@ -2,9 +2,9 @@
import { View, Text, Image } from '@tarojs/components';
import { useEffect, useState } from 'react';
import './css-folder/cancel-popup.less'
import { formatRemainingDurationDisplay } from '../../../utils/format';
import {
    clearOrderTripStorage,
    formatDurationDisplay
    clearOrderTripStorage
} from '../../../../utils/Robotaxi-public-method'
@@ -29,7 +29,7 @@
    const startSiteName = cachedStartSite?.name || cachedStartSite?.address || '-';
    const endSiteName = cachedEndSite?.name || cachedEndSite?.address || '-';
    const waitMinutesValue = typeof waitMinutes === 'number' && !Number.isNaN(waitMinutes) ? waitMinutes : 2;
    const durationDisplay = formatDurationDisplay(routeDurationSeconds);
    const durationDisplay = formatRemainingDurationDisplay(routeDurationSeconds);
    const discountAmountValue = typeof discountAmount === 'number' && !Number.isNaN(discountAmount) ? discountAmount : 0;
    // 不同取消阶段对应的文案配置
src/robotaxi/pages/begin-route/hooks/use-map-markers.ts
@@ -121,11 +121,41 @@
  // 应用 marker 状态并处理状态6刷新标记
  const applyMarkerState = (nextMarkers: any[], nextMarkersSignature: string, shouldSkipUpdate: boolean, carMarker: any) => {
    if (shouldSkipUpdate) {
      // 跳过整包 markers 更新时,仍兜底同步车辆 marker 位置与角度,避免朝向卡旧值
      setMarkers((prevMarkers: any[]) => {
        const targetCarIndex = prevMarkers.findIndex((item: any) => item.id === deps.vehicleMarkerId);
        if (targetCarIndex === -1) {
          if (markersSignatureRef.current !== nextMarkersSignature) {
            markersSignatureRef.current = nextMarkersSignature;
            return nextMarkers;
          }
          return prevMarkers;
        }
        const prevCar = prevMarkers[targetCarIndex] || {};
        const nextCarRotate = typeof carMarker?.rotate === 'number' ? carMarker.rotate : prevCar.rotate;
        const carLocationChanged =
          Number(prevCar.latitude) !== Number(carMarker?.latitude) ||
          Number(prevCar.longitude) !== Number(carMarker?.longitude);
        const carRotateChanged = Number(prevCar.rotate) !== Number(nextCarRotate);
        if (!carLocationChanged && !carRotateChanged) {
          return prevMarkers;
        }
        const next = [...prevMarkers];
        next[targetCarIndex] = {
          ...prevCar,
          latitude: carMarker?.latitude,
          longitude: carMarker?.longitude,
          rotate: nextCarRotate
        };
        return next;
      });
    }
    if (!shouldSkipUpdate && markersSignatureRef.current !== nextMarkersSignature) {
      markersSignatureRef.current = nextMarkersSignature;
      setMarkers(nextMarkers);
    }
    if (!deps.isAndroid && [4, 6].includes(deps.tripStatus) && deps.statusRule.showCar && !!carMarker && !tripStatus6MarkerRefreshedRef.current) {
    if (!deps.isAndroid && deps.tripStatus === 6 && deps.statusRule.showCar && !!carMarker && !tripStatus6MarkerRefreshedRef.current) {
      tripStatus6MarkerRefreshedRef.current = true;
    }
    carMarkerInitialized.current = deps.statusRule.showCar && !!carMarker;
src/robotaxi/pages/begin-route/hooks/use-order-actions.ts
@@ -1,6 +1,7 @@
import Taro from '@tarojs/taro';
import { formatDateTime, clearOrderTripStorage, debounce, reportEvent } from '../../../../utils/Robotaxi-public-method';
import { saveOrder, getOrderPayment, cancelOrder, endOrder } from '../../../api/begin-route';
import { getAgreement } from '../../../api/home';
import { checkOtp } from '../../../../utils/tools';
import { jumpTo } from '../../../../utils/jump';
@@ -33,6 +34,8 @@
  setCancelType: (value: number) => void;
  /** 取消弹窗设置 */
  setShowCancelPopup: (value: boolean) => void;
  /** 取消弹窗等待分钟数 */
  setCancelPopupWaitMinutes: (value: number | null) => void;
  /** 步行点设置 */
  setWalkingPoints: (value: any[]) => void;
  /** 步行时长设置 */
@@ -43,6 +46,8 @@
  setLongitude: (value: number) => void;
  /** 打开取消弹窗 */
  setShowOverRoutePopup: (value: boolean) => void;
  /** 打开协议弹窗 */
  handleOpenPrivacyPopup: () => void;
  /** 距离与城市校验 */
  checkDistanceAndCityReady: (startSite: any) => Promise<number | null>;
  /** 绑定 socket 监听 */
@@ -57,6 +62,17 @@
 * 订单动作编排:下单、取消、结束、支付。
 */
export default function useOrderActions(deps: Deps) {
  const NOT_AGREED_TEXT = 'Not agreed to the agreement'; // 协议未同意返回文案关键字
  // 标准化文本,兼容普通空格与不可见空格差异
  const normalizeText = (value: any) => String(value || '').replace(/\s+/g, ' ').trim().toLowerCase();
  // 判断接口返回是否为“未同意协议”场景
  const isNotAgreedMessage = (value: any) => normalizeText(value).includes(NOT_AGREED_TEXT);
  // 协议接口约定:true=已同意,false=未同意
  const resolveAgreementStatus = (result: any) => result === true;
  /**
   * 结束下单中状态。
   */
@@ -90,6 +106,10 @@
      coupon_id: cachedCouponId,
      status_code
    });
    if (isNotAgreedMessage(res) || isNotAgreedMessage(res?.data) || isNotAgreedMessage(res?.msg)) {
      deps.handleOpenPrivacyPopup();
      return null;
    }
    if (typeof res === 'string') {
      Taro.showToast({ title: res, icon: 'none', mask: true });
      setTimeout(() => {
@@ -150,6 +170,19 @@
    deps.setCallSubmitting(true);
    const authReady = await checkCallAuth();
    if (!authReady) return;
    try {
      const agreementResult = await getAgreement();
      const hasAgreed = resolveAgreementStatus(agreementResult);
      if (!hasAgreed) {
        deps.handleOpenPrivacyPopup();
        stopCallSubmitting();
        return;
      }
    } catch (error) {
      Taro.showToast({ title: '协议校验失败,请重试', icon: 'none' });
      stopCallSubmitting();
      return;
    }
    const routeReady = checkRouteReady();
    if (!routeReady) return;
    const s = getStartSiteForCall();
@@ -187,6 +220,7 @@
   */
  const restartPlanningRoute = () => {
    deps.setShowCancelPopup(false);
    deps.setCancelPopupWaitMinutes(null);
    deps.setTripStatus(1);
    deps.routeUpdateWalkingRouteForStart(deps.startSite);
    const s = Taro.getStorageSync('startSite');
@@ -233,8 +267,14 @@
  /**
   * 根据状态打开取消弹窗。
   */
  const handleOpenCancelPopupByTripStatus = (status: number, durationDisplay?: any) => {
    if (durationDisplay && ![3, 5].includes(status)) {
  const handleOpenCancelPopupByTripStatus = (status: number, displayInfo?: any) => {
    const waitMinutes = Number(displayInfo?.waitMinutes);
    if (status === 3 && Number.isFinite(waitMinutes)) {
      deps.setCancelPopupWaitMinutes(waitMinutes);
    } else {
      deps.setCancelPopupWaitMinutes(null);
    }
    if (displayInfo?.durationDisplay && ![3, 5].includes(status)) {
      deps.setCancelType(2);
      deps.setShowCancelPopup(true);
      return;
src/robotaxi/pages/begin-route/hooks/use-popup-manager.ts
@@ -3,6 +3,7 @@
export default function usePopupManager() {
    const [showCustomerService, setShowCustomerService] = useState(false);
    const [showCancelPopup, setShowCancelPopup] = useState(false);
    const [cancelPopupWaitMinutes, setCancelPopupWaitMinutes] = useState<number | null>(null);
    const [showAnnouncementsPopup, setShowAnnouncementsPopup] = useState(false);
    const [showOverRoutePopup, setShowOverRoutePopup] = useState(false);
    const [showPassengerNotice, setShowPassengerNotice] = useState(false);
@@ -10,6 +11,7 @@
    const [showEvaluatePopup, setShowEvaluatePopup] = useState(false);
    const [showTipPopup, setShowTipPopup] = useState(false);
    const [tipType, setTipType] = useState('');
    const [showPrivacyPopup, setShowPrivacyPopup] = useState(false);
    const setPopupVisible = (setter: (value: boolean) => void, visible: boolean) => () => setter(visible);
@@ -21,7 +23,10 @@
        }, 600);
    };
    const handleCloseCancelPopup = setPopupVisible(setShowCancelPopup, false);
    const handleCloseCancelPopup = () => {
        setShowCancelPopup(false);
        setCancelPopupWaitMinutes(null);
    };
    const handleCloseAnnouncementsPopup = setPopupVisible(setShowAnnouncementsPopup, false);
    const handleOpenAnnouncementsPopup = setPopupVisible(setShowAnnouncementsPopup, true);
    const handleOpenOverRoutePopup = setPopupVisible(setShowOverRoutePopup, true);
@@ -32,6 +37,8 @@
    const handleOpenEvaluatePopup = setPopupVisible(setShowEvaluatePopup, true);
    const handleCloseEvaluatePopup = setPopupVisible(setShowEvaluatePopup, false);
    const handleOpenPrivacyPopup = setPopupVisible(setShowPrivacyPopup, true);
    const handleClosePrivacyPopup = setPopupVisible(setShowPrivacyPopup, false);
    return {
        showCustomerService,
@@ -39,6 +46,8 @@
        handleCloseCustomerService,
        showCancelPopup,
        setShowCancelPopup,
        cancelPopupWaitMinutes,
        setCancelPopupWaitMinutes,
        handleCloseCancelPopup,
        cancelType,
        setCancelType,
@@ -62,6 +71,10 @@
        setShowTipPopup,
        handleCloseTipPopup,
        tipType,
        setTipType
        setTipType,
        showPrivacyPopup,
        setShowPrivacyPopup,
        handleOpenPrivacyPopup,
        handleClosePrivacyPopup
    };
}
src/robotaxi/pages/begin-route/hooks/use-route-manager.ts
@@ -31,6 +31,16 @@
  setVehicleDirection: () => { }
};
// 统一解析接口返回方向
const resolveDirectionValue = (payload: any) => {
  const rawDirection = payload?.direction;
  const direction = Number(rawDirection);
  if (!Number.isFinite(direction)) {
    return null;
  }
  return ((direction % 360) + 360) % 360;
};
export default function useRouteManager(rawDeps?: Partial<Deps>) {
  const deps = { ...defaultDeps, ...(rawDeps || {}) } as Deps;
  // 路线渲染线集合
@@ -280,9 +290,9 @@
        if (durationSeconds !== null) {
          setRouteDuration(durationSeconds);
        }
        const dir = Number(res?.direction);
        if (Number.isFinite(dir)) {
          deps.setVehicleDirection(((dir % 360) + 360) % 360);
        const normalizedDirection = resolveDirectionValue(res);
        if (typeof normalizedDirection === 'number') {
          deps.setVehicleDirection(normalizedDirection);
        }
      })
      .catch(() => { });
src/robotaxi/pages/begin-route/hooks/use-vehicle-tracking.ts
@@ -23,6 +23,40 @@
  const lastVehiclePointRef = useRef<{ latitude: number; longitude: number } | null>(null);
  const lastVehicleUpdateAtRef = useRef<number>(0);
  const TRANSLATE_DURATION_MS = 100;
  const isTranslatingRef = useRef(false); // 当前是否有进行中的平移动画
  const pendingTranslateRef = useRef<{
    latitude: number;
    longitude: number;
    rotate: number;
    markerId: number;
  } | null>(null); // 最新待执行的平移目标(仅保留最后一次)
  // 执行一次平移动画,完成后自动消费最新待执行目标
  const flushTranslateTask = () => {
    if (isTranslatingRef.current) return;
    if (!pendingTranslateRef.current) return;
    const task = pendingTranslateRef.current;
    pendingTranslateRef.current = null;
    isTranslatingRef.current = true;
    const mapCtx = Taro.createMapContext('map');
    mapCtx.translateMarker({
      markerId: task.markerId,
      destination: {
        latitude: task.latitude,
        longitude: task.longitude,
      },
      autoRotate: false,
      moveWithRotate: true,
      rotate: task.rotate,
      duration: TRANSLATE_DURATION_MS,
      complete: () => {
        isTranslatingRef.current = false;
        if (pendingTranslateRef.current) {
          flushTranslateTask();
        }
      }
    });
  };
  // 更新车辆位置并维护最近位置引用
  const updateVehicleLocationThrottled = (lat: number, lng: number, force = false) => {
@@ -37,20 +71,21 @@
  useEffect(() => {
    if (!statusRule.translateCar) return;
    if (!vehicleLocation) return;
    const mapCtx = Taro.createMapContext('map');
    mapCtx.translateMarker({
      markerId: vehicleMarkerId,
      destination: {
        latitude: vehicleLocation.latitude,
        longitude: vehicleLocation.longitude,
      },
      autoRotate: false,
      moveWithRotate: true,
    pendingTranslateRef.current = {
      latitude: vehicleLocation.latitude,
      longitude: vehicleLocation.longitude,
      rotate: typeof vehicleDirection === 'number' ? ((vehicleDirection % 360) + 360) % 360 : 0,
      duration: TRANSLATE_DURATION_MS
    });
      markerId: vehicleMarkerId
    };
    flushTranslateTask();
  }, [statusRule.translateCar, vehicleLocation, vehicleDirection, vehicleMarkerId]);
  useEffect(() => {
    if (statusRule.translateCar) return;
    isTranslatingRef.current = false;
    pendingTranslateRef.current = null;
  }, [statusRule.translateCar]);
  return {
    vehicleLocation,
    setVehicleLocation,
src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx
@@ -3,7 +3,7 @@
import ReleaseRoute from '../components/release-route';
import ReleaseBeBeing from '../components/release-be-being';
import CallingCar from '../components/calling-car';
import { formatDurationDisplay } from '../../../../utils/Robotaxi-public-method';
import { formatRemainingDurationDisplay } from '../../../utils/format';
type Deps = {
  /** 是否安卓 */
@@ -74,8 +74,6 @@
  evaluateSubmitVersion: number;
  /** 评价关闭版本 */
  evaluateCloseVersion: number;
  /** 打开公告 */
  handleOpenAnnouncementsPopup: () => void;
  /** 打开结束弹窗 */
  handleOpenOverRoutePopup: () => void;
  /** 支付 */
@@ -93,7 +91,7 @@
 */
export default function useViewDelegates(deps: Deps) {
  /** 时长文案 */
  const durationDisplay = formatDurationDisplay(deps.routeDuration);
  const durationDisplay = formatRemainingDurationDisplay(deps.routeDuration);
  /**
   * 渲染等待态气泡。
@@ -225,7 +223,6 @@
          priceUpdateVersion={deps.priceUpdateVersion}
          evaluateSubmitVersion={deps.evaluateSubmitVersion}
          evaluateCloseVersion={deps.evaluateCloseVersion}
          handleOpenAnnouncementsPopup={deps.handleOpenAnnouncementsPopup}
          handleOpenOverRoutePopup={deps.handleOpenOverRoutePopup}
          handlePay={deps.handlePay}
          handleOpenEvaluatePopup={deps.handleOpenEvaluatePopup}
src/robotaxi/pages/begin-route/hooks/use-web-socket-events.ts
@@ -159,10 +159,20 @@
    }
};
// 统一解析方向值
const resolveDirectionValue = (payload: any) => {
    const rawDirection = payload?.content?.direction;
    const direction = Number(rawDirection);
    if (!Number.isFinite(direction)) {
        return null;
    }
    return ((direction % 360) + 360) % 360;
};
const updateRouteSummary = (payload: any, deps: UseWebSocketEventsDeps) => {
    const direction = Number(payload?.direction);
    if (Number.isFinite(direction)) {
        deps.setVehicleDirection(((direction % 360) + 360) % 360);
    const normalizedDirection = resolveDirectionValue(payload);
    if (typeof normalizedDirection === 'number') {
        deps.setVehicleDirection(normalizedDirection);
    }
    const route = safeParse(payload?.content?.route, {});
    const distanceKm = normalizeDistanceKm(Number(route?.distance));
src/robotaxi/pages/begin-route/index.tsx
@@ -49,6 +49,8 @@
    handleCloseCustomerService,
    showCancelPopup,
    setShowCancelPopup,
    cancelPopupWaitMinutes,
    setCancelPopupWaitMinutes,
    handleCloseCancelPopup,
    cancelType,
    setCancelType,
@@ -71,7 +73,10 @@
    setShowTipPopup,
    handleCloseTipPopup,
    tipType,
    setTipType
    setTipType,
    showPrivacyPopup,
    handleOpenPrivacyPopup,
    handleClosePrivacyPopup
  } = usePopupManager(); // 弹窗状态编排
  const [longitude, setLongitude] = useState<number | null>(null); // 经度
  const [latitude, setLatitude] = useState<number | null>(null); // 纬度
@@ -397,11 +402,13 @@
    setCallSubmitting,
    setCancelType,
    setShowCancelPopup,
    setCancelPopupWaitMinutes,
    setWalkingPoints,
    setWalkingTime,
    setLatitude,
    setLongitude,
    setShowOverRoutePopup,
    handleOpenPrivacyPopup,
    checkDistanceAndCityReady,
    bindSocketListeners,
    routeUpdateWalkingRouteForStart,
@@ -486,7 +493,6 @@
    priceUpdateVersion,
    evaluateSubmitVersion,
    evaluateCloseVersion,
    handleOpenAnnouncementsPopup,
    handleOpenOverRoutePopup,
    handlePay,
    handleOpenEvaluatePopup,
@@ -539,6 +545,7 @@
      handleCloseCustomerService={handleCloseCustomerService}
      showCancelPopup={showCancelPopup}
      cancelType={cancelType}
      cancelPopupWaitMinutes={cancelPopupWaitMinutes}
      startSite={startSite}
      endSite={endSite}
      fareEstimate={fareEstimate}
@@ -554,6 +561,8 @@
      showTipPopup={showTipPopup}
      handleCloseTipPopup={handleCloseTipPopup}
      tipType={tipType}
      showPrivacyPopup={showPrivacyPopup}
      handleClosePrivacyPopup={handleClosePrivacyPopup}
      showPassengerNotice={showPassengerNotice}
      handleClosePassengerNotice={handleClosePassengerNotice}
      changeCheckedEnter={changeCheckedEnter}
src/robotaxi/utils/format.ts
@@ -18,3 +18,15 @@
  }
  return Math.round(num);
};
// 剩余时长展示(begin-route):分钟按向下截断,秒按四舍五入
export const formatRemainingDurationDisplay = (value: any) => {
  const num = Number(value);
  if (Number.isNaN(num)) {
    return { value: 0, unit: '分钟' as const };
  }
  if (num < 60) {
    return { value: Math.max(0, Math.round(num)), unit: '秒' as const };
  }
  return { value: Math.max(0, Math.floor(num / 60)), unit: '分钟' as const };
};
src/utils/websocket.ts
@@ -18,7 +18,7 @@
  private heartbeatTimer: any = null;
  private heartbeatTimeoutTimer: any = null; // 用于检测心跳超时的定时器
  private heartbeatInterval: number = 10000; // 10秒心跳
  private heartbeatTimeout: number = 3000;   // 5秒收不到响应即视为掉线
  private heartbeatTimeout: number = 3000;   // 3秒收不到响应即视为掉线
  private listeners: Map<string, MessageCallback[]> = new Map();
  private globalEventsBound: boolean = false;