From aa8038c44dc8e2eda1e801b8f44f54d00fd649e3 Mon Sep 17 00:00:00 2001
From: 卢春龙 <lucl@xiaopeng.com>
Date: 星期五, 24 四月 2026 23:53:02 +0800
Subject: [PATCH] Merge branch 'feature_urgency_fix' into 'develop'

---
 src/robotaxi/pages/begin-route/hooks/use-route-manager.ts                            |   16 ++
 src/robotaxi/pages/begin-route/hooks/use-vehicle-tracking.ts                         |   57 +++++++-
 src/pages/robotaxi/home/components/map-overlay-tips/index.tsx                        |   16 ++
 src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx                          |    7 
 src/robotaxi/pages/begin-route/components/calling-car-modules/door-trunk-control.tsx |    9 -
 src/robotaxi/pages/begin-route/components/calling-car-modules/trip-status-header.tsx |    8 
 src/pages/robotaxi/home/index.tsx                                                    |   46 +++++++
 src/robotaxi/pages/begin-route/index.tsx                                             |   13 +
 src/robotaxi/pages/begin-route/hooks/use-order-actions.ts                            |   44 +++++++
 src/robotaxi/pages/begin-route/components/calling-car.tsx                            |   13 -
 src/robotaxi/pages/begin-route/hooks/use-popup-manager.ts                            |   17 ++
 src/utils/websocket.ts                                                               |    2 
 src/pages/robotaxi/home/index.less                                                   |    2 
 src/robotaxi/pages/begin-route/components/begin-route-view.tsx                       |   14 ++
 src/robotaxi/pages/begin-route/components/cancel-popup.tsx                           |    6 
 src/robotaxi/pages/begin-route/hooks/use-map-markers.ts                              |   32 +++++
 src/robotaxi/pages/begin-route/hooks/use-web-socket-events.ts                        |   16 ++
 src/robotaxi/utils/format.ts                                                         |   12 ++
 18 files changed, 273 insertions(+), 57 deletions(-)

diff --git a/src/pages/robotaxi/home/components/map-overlay-tips/index.tsx b/src/pages/robotaxi/home/components/map-overlay-tips/index.tsx
index 8d03117..b8e8126 100644
--- a/src/pages/robotaxi/home/components/map-overlay-tips/index.tsx
+++ b/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">
diff --git a/src/pages/robotaxi/home/index.less b/src/pages/robotaxi/home/index.less
index 62fc6dd..613894b 100644
--- a/src/pages/robotaxi/home/index.less
+++ b/src/pages/robotaxi/home/index.less
@@ -61,7 +61,7 @@
   position: fixed;
   top: 55%;
   left: 0px;
-  z-index: 1;
+  z-index: 2;
 
   .position-box {
 
diff --git a/src/pages/robotaxi/home/index.tsx b/src/pages/robotaxi/home/index.tsx
index 98d07cd..8646df6 100644
--- a/src/pages/robotaxi/home/index.tsx
+++ b/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}
diff --git a/src/robotaxi/pages/begin-route/components/begin-route-view.tsx b/src/robotaxi/pages/begin-route/components/begin-route-view.tsx
index 05765bb..74fd3fb 100644
--- a/src/robotaxi/pages/begin-route/components/begin-route-view.tsx
+++ b/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}
diff --git a/src/robotaxi/pages/begin-route/components/calling-car-modules/door-trunk-control.tsx b/src/robotaxi/pages/begin-route/components/calling-car-modules/door-trunk-control.tsx
index 0fcba6e..6203ff9 100644
--- a/src/robotaxi/pages/begin-route/components/calling-car-modules/door-trunk-control.tsx
+++ b/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();
diff --git a/src/robotaxi/pages/begin-route/components/calling-car-modules/trip-status-header.tsx b/src/robotaxi/pages/begin-route/components/calling-car-modules/trip-status-header.tsx
index 710c268..cd3e53e 100644
--- a/src/robotaxi/pages/begin-route/components/calling-car-modules/trip-status-header.tsx
+++ b/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>
diff --git a/src/robotaxi/pages/begin-route/components/calling-car.tsx b/src/robotaxi/pages/begin-route/components/calling-car.tsx
index adaa499..bbe8004 100644
--- a/src/robotaxi/pages/begin-route/components/calling-car.tsx
+++ b/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 
diff --git a/src/robotaxi/pages/begin-route/components/cancel-popup.tsx b/src/robotaxi/pages/begin-route/components/cancel-popup.tsx
index 93387dd..7db96d5 100644
--- a/src/robotaxi/pages/begin-route/components/cancel-popup.tsx
+++ b/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;
 
     // 不同取消阶段对应的文案配置
diff --git a/src/robotaxi/pages/begin-route/hooks/use-map-markers.ts b/src/robotaxi/pages/begin-route/hooks/use-map-markers.ts
index ff7e315..5980315 100644
--- a/src/robotaxi/pages/begin-route/hooks/use-map-markers.ts
+++ b/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;
diff --git a/src/robotaxi/pages/begin-route/hooks/use-order-actions.ts b/src/robotaxi/pages/begin-route/hooks/use-order-actions.ts
index d164442..85c3459 100644
--- a/src/robotaxi/pages/begin-route/hooks/use-order-actions.ts
+++ b/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;
diff --git a/src/robotaxi/pages/begin-route/hooks/use-popup-manager.ts b/src/robotaxi/pages/begin-route/hooks/use-popup-manager.ts
index c5843fd..4e409c9 100644
--- a/src/robotaxi/pages/begin-route/hooks/use-popup-manager.ts
+++ b/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
     };
 }
diff --git a/src/robotaxi/pages/begin-route/hooks/use-route-manager.ts b/src/robotaxi/pages/begin-route/hooks/use-route-manager.ts
index c9806f2..fcd3064 100644
--- a/src/robotaxi/pages/begin-route/hooks/use-route-manager.ts
+++ b/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(() => { });
diff --git a/src/robotaxi/pages/begin-route/hooks/use-vehicle-tracking.ts b/src/robotaxi/pages/begin-route/hooks/use-vehicle-tracking.ts
index e5a8690..514e970 100644
--- a/src/robotaxi/pages/begin-route/hooks/use-vehicle-tracking.ts
+++ b/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,
diff --git a/src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx b/src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx
index 94b5350..3f7090d 100644
--- a/src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx
+++ b/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}
diff --git a/src/robotaxi/pages/begin-route/hooks/use-web-socket-events.ts b/src/robotaxi/pages/begin-route/hooks/use-web-socket-events.ts
index 1a5e5e6..95d1e64 100644
--- a/src/robotaxi/pages/begin-route/hooks/use-web-socket-events.ts
+++ b/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));
diff --git a/src/robotaxi/pages/begin-route/index.tsx b/src/robotaxi/pages/begin-route/index.tsx
index be456fe..d66fc09 100644
--- a/src/robotaxi/pages/begin-route/index.tsx
+++ b/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}
diff --git a/src/robotaxi/utils/format.ts b/src/robotaxi/utils/format.ts
index 633531b..0f346df 100644
--- a/src/robotaxi/utils/format.ts
+++ b/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 };
+};
diff --git a/src/utils/websocket.ts b/src/utils/websocket.ts
index f3a8d38..c5ee781 100644
--- a/src/utils/websocket.ts
+++ b/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;
 

--
Gitblit v1.7.1