zjk
2026-04-24 95f8193162ad6f257ae236258c0f63b2dde7e311
Merge branch 'feature_v1.0' of http://gitlab.xiaopeng.local:18080/robotaxi1/xp-robotaxi-miniprogram into feature_v1.0
18个文件已修改
407 ■■■■■ 已修改文件
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 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/cancel-popup.tsx 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-map-markers.ts 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-order-actions.ts 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-popup-manager.ts 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-route-manager.ts 100 ●●●●● 补丁 | 查看 | 原始文档 | 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
@@ -82,7 +82,7 @@
  position: fixed;
  top: 55%;
  left: 0px;
  z-index: 1;
  z-index: 2;
  .position-box {
src/pages/robotaxi/home/index.tsx
@@ -150,6 +150,7 @@
  const [weatherData, setWeatherData] = useState<any>(null); // 天气数据
  const [orderData, setOrderData] = useState<any>({}); // 订单数据
  const [isLogin, setIsLogin] = useState(false); // 是否登录
  const [overlayAnchorBottom, setOverlayAnchorBottom] = useState<number | null>(null); // 浮层初始化锚点(距底部)
  const {
    boxHeight,
    originOpacity,
@@ -420,6 +421,9 @@
        isInside: isOperatingArea
      });
    }
    setTimeout(() => {
      initOverlayAnchor();
    }, 80);
  });
  useDidHide(() => {
@@ -490,6 +494,35 @@
    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) => {
    if (isLocationAuthDenied) return;
@@ -551,7 +584,15 @@
          )}
        </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
@@ -561,6 +602,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
@@ -8,6 +8,7 @@
import TipPopup from '../../select-map-site/components/tip-popup';
import AuthBluetoothPopup from './auth-bluetooth-popup';
import { TripStatus } from '../constants/trip-status';
import PrivacyPopup from '../../../../pages/robotaxi/home/components/privacy-popup';
type Props = {
  mapLongitude: number;
@@ -46,6 +47,7 @@
  handleCloseCustomerService: () => void;
  showCancelPopup: boolean;
  cancelType: number;
  cancelPopupWaitMinutes: number | null;
  startSite: any;
  endSite: any;
  fareEstimate: any;
@@ -61,6 +63,8 @@
  showTipPopup: boolean;
  handleCloseTipPopup: () => void;
  tipType: string;
  showPrivacyPopup: boolean;
  handleClosePrivacyPopup: () => void;
  showPassengerNotice: boolean;
  handleClosePassengerNotice: () => void;
  changeCheckedEnter: (checked: boolean) => void;
@@ -111,6 +115,7 @@
    handleCloseCustomerService,
    showCancelPopup,
    cancelType,
    cancelPopupWaitMinutes,
    startSite,
    endSite,
    fareEstimate,
@@ -126,6 +131,8 @@
    showTipPopup,
    handleCloseTipPopup,
    tipType,
    showPrivacyPopup,
    handleClosePrivacyPopup,
    showPassengerNotice,
    handleClosePassengerNotice,
    changeCheckedEnter,
@@ -202,7 +209,7 @@
        cancelType={cancelType}
        startSite={startSite}
        endSite={endSite}
        waitMinutes={fareEstimate?.waitingTime}
        waitMinutes={cancelPopupWaitMinutes ?? fareEstimate?.waitingTime}
        tripStatus={tripStatus}
        routeDurationSeconds={routeDuration}
        discountAmount={fareEstimate?.discountAmount}
@@ -223,6 +230,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
@@ -9,20 +9,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(() => {
@@ -158,10 +155,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
@@ -22,7 +22,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({
@@ -91,7 +91,9 @@
    // 标题行文案映射
    const titleMap = {
        [TripStatus.CallingCar]: <>预计<View className="time">{waitMinutes}</View>分钟内有车应答</>,
        [TripStatus.VehicleApproaching]: <>车辆还有<View className="time">{durationDisplay.value}</View>{durationDisplay.unit}到达</>,
        [TripStatus.VehicleApproaching]: isArriveSoon
            ? <>即将到达</>
            :<>车辆还有<View className="time">{durationDisplay.value}</View>{durationDisplay.unit}到达</>,
        [TripStatus.VehicleArrivedPickup]: <>车辆已到达上车点<View className="time" style={isPickupCountdownSoon ? { color: '#E03434' } : undefined}>{pickupCountdownSeconds !== null ? formatCountdown(pickupCountdownSeconds) : '--:--'}</View></>,
        [TripStatus.InService]: <>预计<View className="time">{durationDisplay.value}</View>{durationDisplay.unit}抵达目的地</>,
        [TripStatus.ArrivedDestination]: (finishCountdownEnabled || finishCountdownSeconds !== null)
@@ -130,7 +132,7 @@
            {
                [TripStatus.CallingCar].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 { TripStatus } from '../constants/trip-status';
import useTripCountdown from './calling-car-modules/use-trip-countdown'; // 行程倒计时
@@ -40,8 +40,6 @@
    handlePay?: () => void;
    /** 打开详细评价弹窗 */
    handleOpenEvaluatePopup?: () => void;
    /** 首次开门后打开安全须知弹窗 */
    handleOpenAnnouncementsPopup?: () => void;
    /** 取消行程 */
    handleCancelTrip?: () => void;
    /** 修改目的地 */
@@ -88,7 +86,6 @@
    onToggleDetailFade,
    handlePay,
    handleOpenEvaluatePopup,
    handleOpenAnnouncementsPopup,
    handleCancelTrip,
    handleModifyDestination,
    handleShareTrip,
@@ -127,7 +124,7 @@
    });
    /** 预计到达时长展示(秒/分钟) */
    const durationDisplay = formatDurationDisplay(routeDurationSeconds);
    const durationDisplay = formatRemainingDurationDisplay(routeDurationSeconds);
    /** 车辆即将到达上车点(<=60秒) */
    const isArriveSoon = tripStatus === TripStatus.VehicleApproaching && (typeof routeDurationSeconds === 'number' && routeDurationSeconds <= 60);
    /** 上车倒计时即将结束(<60秒) */
@@ -190,7 +187,6 @@
                        orderId={orderId}
                        orderTrunkStatus={orderTrunkStatus}
                        handleOpenOverRoutePopup={handleOpenOverRoutePopup}
                        handleOpenAnnouncementsPopup={handleOpenAnnouncementsPopup}
                    />
                    <FareDetailCard
@@ -216,7 +212,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'
import { TripStatus } from '../constants/trip-status';
@@ -30,7 +30,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
@@ -146,6 +146,36 @@
  // 应用 marker 状态并处理“行驶中”阶段的刷新标记
  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);
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, routeStaleCheck } from '../../../api/begin-route';
import { getAgreement } from '../../../api/home';
import { checkOtp } from '../../../../utils/tools';
import { jumpTo } from '../../../../utils/jump';
import { subscribeAfterOrderSuccess } from '../../../../utils/subscribe';
@@ -39,6 +40,8 @@
  setCancelType: (value: number) => void;
  /** 取消弹窗设置 */
  setShowCancelPopup: (value: boolean) => void;
  /** 取消弹窗等待分钟数 */
  setCancelPopupWaitMinutes: (value: number | null) => void;
  /** 步行点设置 */
  setWalkingPoints: (value: any[]) => void;
  /** 步行时长设置 */
@@ -49,6 +52,8 @@
  setLongitude: (value: number) => void;
  /** 打开取消弹窗 */
  setShowOverRoutePopup: (value: boolean) => void;
  /** 打开协议弹窗 */
  handleOpenPrivacyPopup: () => void;
  /** 距离与城市校验 */
  checkDistanceAndCityReady: (startSite: any) => Promise<number | null>;
  /** 绑定 socket 监听 */
@@ -64,6 +69,16 @@
 */
export default function useOrderActions(deps: Deps) {
  const ROUTE_STALE_NOTICE_TEXT = '路况已变,价格已更新'; // 路况变化提示文案
  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;
  /**
   * 结束下单中状态。
@@ -108,6 +123,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(() => {
@@ -195,6 +214,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();
@@ -235,6 +267,7 @@
   */
  const restartPlanningRoute = () => {
    deps.setShowCancelPopup(false);
    deps.setCancelPopupWaitMinutes(null);
    deps.setTripStatus(TripStatus.PublishRoute);
    deps.routeUpdateWalkingRouteForStart(deps.startSite);
    const s = Taro.getStorageSync('startSite');
@@ -281,8 +314,14 @@
  /**
   * 根据状态打开取消弹窗。
   */
  const handleOpenCancelPopupByTripStatus = (status: TripStatus, durationDisplay?: any) => {
    if (durationDisplay && ![3, 5].includes(status)) {
  const handleOpenCancelPopupByTripStatus = (status: TripStatus, 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
@@ -48,6 +48,16 @@
  return { longitude, latitude };
};
// 统一解析接口返回方向
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;
  // 路线渲染线集合
@@ -196,17 +206,17 @@
  // 选择路线选项
  const handleSelectRoute = (index: number) => {
    if (!routeOptions[index] || index === selectedRouteIndex) return;
    const routeData = routeOptions[index];
    setSelectedRouteIndex(index);
    if (routeData?.fareEstimate) {
      setFareEstimate(routeData.fareEstimate);
    }
    if (routeData?.xpPricingCard) {
      Taro.setStorageSync('xpPricingCard', routeData.xpPricingCard);
    }
    // Check if the route payload contains new site info and update them if possible
    let newStartLat: number | undefined;
    let newStartLng: number | undefined;
@@ -218,37 +228,37 @@
    // fallback to polyline start/end points if origin/destination are not provided
    if (points.length > 0) {
       newStartLat = points[0].latitude;
       newStartLng = points[0].longitude;
       newEndLat = points[points.length - 1].latitude;
       newEndLng = points[points.length - 1].longitude;
      newStartLat = points[0].latitude;
      newStartLng = points[0].longitude;
      newEndLat = points[points.length - 1].latitude;
      newEndLng = points[points.length - 1].longitude;
    }
    if (routeData?.origin) {
        newStartLat = routeData.origin.latitude ?? newStartLat;
        newStartLng = routeData.origin.longitude ?? newStartLng;
      newStartLat = routeData.origin.latitude ?? newStartLat;
      newStartLng = routeData.origin.longitude ?? newStartLng;
    }
    if (routeData?.destination) {
        newEndLat = routeData.destination.latitude ?? newEndLat;
        newEndLng = routeData.destination.longitude ?? newEndLng;
      newEndLat = routeData.destination.latitude ?? newEndLat;
      newEndLng = routeData.destination.longitude ?? newEndLng;
    }
    if (deps.tripStatusRef.current === TripStatus.PublishRoute && Number.isFinite(newStartLat) && Number.isFinite(newStartLng) && typeof deps.setStartSite === 'function') {
        deps.setStartSite({
           ...deps.startSite,
           latitude: newStartLat,
           longitude: newStartLng,
           name: routeData?.origin?.name || deps.startSite?.name
        });
      deps.setStartSite({
        ...deps.startSite,
        latitude: newStartLat,
        longitude: newStartLng,
        name: routeData?.origin?.name || deps.startSite?.name
      });
    }
    if (deps.tripStatusRef.current === TripStatus.PublishRoute && Number.isFinite(newEndLat) && Number.isFinite(newEndLng) && typeof deps.setEndSite === 'function') {
        deps.setEndSite({
           ...deps.endSite,
           latitude: newEndLat,
           longitude: newEndLng,
           name: routeData?.destination?.name || deps.endSite?.name
        });
      deps.setEndSite({
        ...deps.endSite,
        latitude: newEndLat,
        longitude: newEndLng,
        name: routeData?.destination?.name || deps.endSite?.name
      });
    }
    const distanceKm = normalizeDistanceKm(Number(route?.distance));
@@ -259,7 +269,7 @@
    if (durationSeconds !== null) {
      setRouteDuration(durationSeconds);
    }
    if (Array.isArray(points) && points.length > 0) {
      applyRoutePoints(points);
      // 如果允许选择路线(当前业务下未发单及修改目的地确认态时 allowSelectableRoutes 为 true)
@@ -267,9 +277,9 @@
        // 在发布行程阶段或修改目的地确认态时,需要包含所有路线点以便自适应视野
        const allRoutesPoints: any[] = [];
        allRoutePoints.forEach(pts => {
           if (Array.isArray(pts)) {
              allRoutesPoints.push(...pts);
           }
          if (Array.isArray(pts)) {
            allRoutesPoints.push(...pts);
          }
        });
        const allPoints = [...allRoutesPoints, ...walkingPoints];
        if (allPoints.length > 0) {
@@ -379,16 +389,16 @@
      },
    };
    if (cachedCouponId) {
        params.userCouponId = cachedCouponId;
      }
      params.userCouponId = cachedCouponId;
    }
      queryRouteAndFare(params).then((res: any) => {
        setRouteAndFareError(false);
        let routeData = res;
        let options: any[] = [];
    queryRouteAndFare(params).then((res: any) => {
      setRouteAndFareError(false);
      let routeData = res;
      let options: any[] = [];
      // Handle both array and single object response for backward compatibility
      let allPointsArr: any[][] = [];
      if (Array.isArray(res) && res.length > 0) {
@@ -433,9 +443,9 @@
        if (deps.allowSelectableRoutes) {
          const allRoutesPoints: any[] = [];
          allPointsArr.forEach(pts => {
             if (Array.isArray(pts)) {
                allRoutesPoints.push(...pts);
             }
            if (Array.isArray(pts)) {
              allRoutesPoints.push(...pts);
            }
          });
          const allPoints = [...allRoutesPoints, ...walkingPoints];
          if (allPoints.length > 0) {
@@ -504,9 +514,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(() => { });
@@ -567,12 +577,12 @@
  // 构建地图 polyline 数据
  const buildPolylineLines = (displayRoutePoints: any[]) => {
    const lines: any[] = [];
    // 如果有多条路线且在未发单状态或修改目的地状态(allowSelectableRoutes === true),渲染所有路线
    if (deps.shouldShowRoute && deps.allowSelectableRoutes && allRoutePoints.length > 1) {
      allRoutePoints.forEach((points, index) => {
        if (!points || points.length === 0) return;
        const isSelected = index === selectedRouteIndex;
        // 未选中的路线放底层(z-index低些,如果支持的话),选中放顶层
        // 这里通过控制 push 顺序来影响层级,最后 push 选中的路线
@@ -588,7 +598,7 @@
          });
        }
      });
      // 最后 push 选中的路线,使其显示在最上层
      if (allRoutePoints[selectedRouteIndex] && allRoutePoints[selectedRouteIndex].length > 0) {
        lines.push({
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
@@ -4,8 +4,8 @@
import ReleaseBeBeing from '../components/release-be-being';
import CallingCar from '../components/calling-car';
import ModifyDestinationReview from '../components/modify-destination-review';
import { formatDurationDisplay } from '../../../../utils/Robotaxi-public-method';
import { TripStatus } from '../constants/trip-status';
import { formatRemainingDurationDisplay } from '../../../utils/format';
type Deps = {
  /** 是否安卓 */
@@ -80,8 +80,6 @@
  evaluateSubmitVersion: number;
  /** 评价关闭版本 */
  evaluateCloseVersion: number;
  /** 打开公告 */
  handleOpenAnnouncementsPopup: () => void;
  /** 打开结束弹窗 */
  handleOpenOverRoutePopup: () => void;
  /** 支付 */
@@ -145,7 +143,7 @@
 */
export default function useViewDelegates(deps: Deps) {
  /** 时长文案 */
  const durationDisplay = formatDurationDisplay(deps.routeDuration);
    const durationDisplay = formatRemainingDurationDisplay(deps.routeDuration);
  const routeSummaryText = deps.routeDistance && deps.routeDuration !== null
    ? `${durationDisplay.value}${durationDisplay.unit} ${deps.routeDistance.value}${deps.routeDistance.unit === 'm' ? '米' : '公里'}`
    : '';
@@ -330,7 +328,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
@@ -174,10 +174,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
@@ -75,6 +75,8 @@
    handleCloseCustomerService,
    showCancelPopup,
    setShowCancelPopup,
    cancelPopupWaitMinutes,
    setCancelPopupWaitMinutes,
    handleCloseCancelPopup,
    cancelType,
    setCancelType,
@@ -97,7 +99,10 @@
    setShowTipPopup,
    handleCloseTipPopup,
    tipType,
    setTipType
    setTipType,
    showPrivacyPopup,
    handleOpenPrivacyPopup,
    handleClosePrivacyPopup
  } = usePopupManager(); // 弹窗状态编排
  const [longitude, setLongitude] = useState<number | null>(null); // 经度
  const [latitude, setLatitude] = useState<number | null>(null); // 纬度
@@ -541,11 +546,13 @@
    setCallSubmitting,
    setCancelType,
    setShowCancelPopup,
    setCancelPopupWaitMinutes,
    setWalkingPoints,
    setWalkingTime,
    setLatitude,
    setLongitude,
    setShowOverRoutePopup,
    handleOpenPrivacyPopup,
    checkDistanceAndCityReady,
    bindSocketListeners,
    routeUpdateWalkingRouteForStart,
@@ -735,7 +742,6 @@
    priceUpdateVersion,
    evaluateSubmitVersion,
    evaluateCloseVersion,
    handleOpenAnnouncementsPopup,
    handleOpenOverRoutePopup,
    handlePay,
    handleOpenEvaluatePopup,
@@ -832,6 +838,7 @@
      handleCloseCustomerService={handleCloseCustomerService}
      showCancelPopup={showCancelPopup}
      cancelType={cancelType}
      cancelPopupWaitMinutes={cancelPopupWaitMinutes}
      startSite={startSite}
      endSite={effectiveEndSite}
      fareEstimate={fareEstimate}
@@ -847,6 +854,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,9 +18,9 @@
  private heartbeatTimer: any = null;
  private heartbeatTimeoutTimer: any = null; // 用于检测心跳超时的定时器
  private heartbeatInterval: number = 10000; // 10秒心跳
  private heartbeatTimeout: number = 3000;   // 5秒收不到响应即视为掉线
  private locationReportTimer: any = null; // 位置上报定时器
  private locationReportInterval: number = 35000; // 35秒上报一次位置
  private heartbeatTimeout: number = 3000;   // 3秒收不到响应即视为掉线
  private listeners: Map<string, MessageCallback[]> = new Map();
  private globalEventsBound: boolean = false;