zjk
2026-05-22 4f2145aa061e2c0e53fb3c46a10707827562e115
Merge branch 'feature_v1.0' of http://gitlab.xiaopeng.local:18080/robotaxi1/xp-robotaxi-miniprogram into feature_v1.0
94个文件已删除
1个文件已添加
16个文件已修改
789 ■■■■■ 已修改文件
src/robotaxi/assets/Subtract.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/T-down-no.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/T-down.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/T-switch-close.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/T-switch-open.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/T-up-no.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/T-up.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/active-star.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/address.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/arrow.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/back-home.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/back-icon.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/blue-right-icon.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/bluetooth-icon.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/cancel-btn.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/cancel-trip.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/ccc-icon-right.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/change-coupon.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/change-destination.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/change-site-up.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/check.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/clear-icon.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/close-icon.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/cloudy.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/complaint-btn.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/contact-service.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/coupon-arrow-right.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/coupon-arrow.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/coupon-select.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/coupon-selected.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/coupon.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/delete-icon.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/dotted-line.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/down.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/favorite.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/fog.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/line-right.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/lock-close.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/lock-open.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/map.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/marker-site.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/marketing-close.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/marketing-coupon-topBg.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/marketing-single-left.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/marketing-single-right.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/marketing-single-top-left.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/marketing-single-top-right.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/mine-all.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/mine-arrow.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/mine-back.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/mine-cancelled.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/mine-finished.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/mine-line.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/mine-pay.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/more.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/no-star.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/open-box-bg.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/order-to-detail.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/origin-icon.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/over-gps-icon.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/pack-up.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/personal-center.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/pointer.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/rain.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/rainstorm.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/right-icon.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/right.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/route-state-dot.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/route-state-icon.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/sandDust.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/select-city.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/select-line.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/select-type-line.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/share-line.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/share-trip.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/skip-arrows.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/sleet.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/snow.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/submit-cancel-btn.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/suggest.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/summary-icon.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/sunny.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/thundershower.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/tip-blue-icon.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/tip-default-icon.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/tooltip-bg.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/traffic-signal.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/trunk-close.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/trunk-open.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/uncheck.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/up.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/upload-icon.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/weather.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/wind.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/runtime-sync.ts 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-active-trip-scale.ts 196 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-bluetooth-access.ts 148 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-map-markers.ts 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-modify-destination-preview-route.ts 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-order-actions.shared.ts 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-order-actions.ts 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-order-cancel.ts 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-page-restore.ts 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-route-manager.shared.ts 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-route-request.ts 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-vehicle-speed-scale.ts 203 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-web-socket-events.ts 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/index.tsx 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/select-map-site/components/select-map-site-view.tsx 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/select-map-site/index.less 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/Subtract.png
Binary files differ
src/robotaxi/assets/T-down-no.png
Binary files differ
src/robotaxi/assets/T-down.png
Binary files differ
src/robotaxi/assets/T-switch-close.png
Binary files differ
src/robotaxi/assets/T-switch-open.png
Binary files differ
src/robotaxi/assets/T-up-no.png
Binary files differ
src/robotaxi/assets/T-up.png
Binary files differ
src/robotaxi/assets/active-star.png
Binary files differ
src/robotaxi/assets/address.png
Binary files differ
src/robotaxi/assets/arrow.png
Binary files differ
src/robotaxi/assets/back-home.png
Binary files differ
src/robotaxi/assets/back-icon.png
Binary files differ
src/robotaxi/assets/blue-right-icon.png
Binary files differ
src/robotaxi/assets/bluetooth-icon.png
Binary files differ
src/robotaxi/assets/cancel-btn.png
Binary files differ
src/robotaxi/assets/cancel-trip.png
Binary files differ
src/robotaxi/assets/ccc-icon-right.png
Binary files differ
src/robotaxi/assets/change-coupon.png
Binary files differ
src/robotaxi/assets/change-destination.png
Binary files differ
src/robotaxi/assets/change-site-up.png
Binary files differ
src/robotaxi/assets/check.png
Binary files differ
src/robotaxi/assets/clear-icon.png
Binary files differ
src/robotaxi/assets/close-icon.png
Binary files differ
src/robotaxi/assets/cloudy.png
Binary files differ
src/robotaxi/assets/complaint-btn.png
Binary files differ
src/robotaxi/assets/contact-service.png
Binary files differ
src/robotaxi/assets/coupon-arrow-right.png
Binary files differ
src/robotaxi/assets/coupon-arrow.png
Binary files differ
src/robotaxi/assets/coupon-select.png
Binary files differ
src/robotaxi/assets/coupon-selected.png
Binary files differ
src/robotaxi/assets/coupon.png
Binary files differ
src/robotaxi/assets/delete-icon.png
Binary files differ
src/robotaxi/assets/dotted-line.png
Binary files differ
src/robotaxi/assets/down.png
Binary files differ
src/robotaxi/assets/favorite.png
Binary files differ
src/robotaxi/assets/fog.png
Binary files differ
src/robotaxi/assets/line-right.png
Binary files differ
src/robotaxi/assets/lock-close.png
Binary files differ
src/robotaxi/assets/lock-open.png
Binary files differ
src/robotaxi/assets/map.png
Binary files differ
src/robotaxi/assets/marker-site.png
Binary files differ
src/robotaxi/assets/marketing-close.png
Binary files differ
src/robotaxi/assets/marketing-coupon-topBg.png
Binary files differ
src/robotaxi/assets/marketing-single-left.png
Binary files differ
src/robotaxi/assets/marketing-single-right.png
Binary files differ
src/robotaxi/assets/marketing-single-top-left.png
Binary files differ
src/robotaxi/assets/marketing-single-top-right.png
Binary files differ
src/robotaxi/assets/mine-all.png
Binary files differ
src/robotaxi/assets/mine-arrow.png
Binary files differ
src/robotaxi/assets/mine-back.png
Binary files differ
src/robotaxi/assets/mine-cancelled.png
Binary files differ
src/robotaxi/assets/mine-finished.png
Binary files differ
src/robotaxi/assets/mine-line.png
Binary files differ
src/robotaxi/assets/mine-pay.png
Binary files differ
src/robotaxi/assets/more.png
Binary files differ
src/robotaxi/assets/no-star.png
Binary files differ
src/robotaxi/assets/open-box-bg.png
Binary files differ
src/robotaxi/assets/order-to-detail.png
Binary files differ
src/robotaxi/assets/origin-icon.png
Binary files differ
src/robotaxi/assets/over-gps-icon.png
Binary files differ
src/robotaxi/assets/pack-up.png
Binary files differ
src/robotaxi/assets/personal-center.png
Binary files differ
src/robotaxi/assets/pointer.png
Binary files differ
src/robotaxi/assets/rain.png
Binary files differ
src/robotaxi/assets/rainstorm.png
Binary files differ
src/robotaxi/assets/right-icon.png
Binary files differ
src/robotaxi/assets/right.png
Binary files differ
src/robotaxi/assets/route-state-dot.png
Binary files differ
src/robotaxi/assets/route-state-icon.png
Binary files differ
src/robotaxi/assets/sandDust.png
Binary files differ
src/robotaxi/assets/select-city.png
Binary files differ
src/robotaxi/assets/select-line.png
Binary files differ
src/robotaxi/assets/select-type-line.png
Binary files differ
src/robotaxi/assets/share-line.png
Binary files differ
src/robotaxi/assets/share-trip.png
Binary files differ
src/robotaxi/assets/skip-arrows.png
Binary files differ
src/robotaxi/assets/sleet.png
Binary files differ
src/robotaxi/assets/snow.png
Binary files differ
src/robotaxi/assets/submit-cancel-btn.png
Binary files differ
src/robotaxi/assets/suggest.png
Binary files differ
src/robotaxi/assets/summary-icon.png
Binary files differ
src/robotaxi/assets/sunny.png
Binary files differ
src/robotaxi/assets/thundershower.png
Binary files differ
src/robotaxi/assets/tip-blue-icon.png
Binary files differ
src/robotaxi/assets/tip-default-icon.png
Binary files differ
src/robotaxi/assets/tooltip-bg.png
Binary files differ
src/robotaxi/assets/traffic-signal.png
Binary files differ
src/robotaxi/assets/trunk-close.png
Binary files differ
src/robotaxi/assets/trunk-open.png
Binary files differ
src/robotaxi/assets/uncheck.png
Binary files differ
src/robotaxi/assets/up.png
Binary files differ
src/robotaxi/assets/upload-icon.png
Binary files differ
src/robotaxi/assets/weather.png
Binary files differ
src/robotaxi/assets/wind.png
Binary files differ
src/robotaxi/pages/begin-route/hooks/runtime-sync.ts
@@ -26,8 +26,8 @@
  duration: number | null;
  /** 车辆朝向(0-359) */
  direction: number | null;
  /** 当前速度(km/h,已由原始 m/s 统一换算) */
  speedKmh: number | null;
  /** 当前速度(m/s)。 */
  speedMps: number | null;
  /** 消息时间戳(用于乱序保护) */
  timestamp: number | null;
  /** 已行驶轨迹点(servedPoints) */
@@ -67,14 +67,14 @@
}
/**
 * 将后端下发的速度(m/s)统一换算为页面消费的 km/h。
 * 将后端下发的速度统一规整为 m/s。
 */
export function normalizeSpeedKmh(rawSpeed: any) {
export function normalizeSpeedMps(rawSpeed: any) {
  const speedMetersPerSecond = Number(rawSpeed);
  if (!Number.isFinite(speedMetersPerSecond)) {
    return null;
  }
  return speedMetersPerSecond * 3.6;
  return Math.max(0, speedMetersPerSecond);
}
/**
@@ -83,13 +83,13 @@
export function normalizeSocketRouteSnapshot(payload: any): RouteSnapshot {
  const route = safeParse(payload?.content?.route, {});
  const timestamp = Number(payload?.data?.timestamp);
  const speedKmh = normalizeSpeedKmh(route?.speed);
  const speedMps = normalizeSpeedMps(route?.speed);
  return {
    points: normalizeRoutePoints(payload?.points),
    distance: normalizeDistanceKm(Number(route?.distance)),
    duration: normalizeDurationSeconds(Number(route?.duration)),
    direction: normalizeDirection(payload?.content?.direction),
    speedKmh,
    speedMps,
    timestamp: Number.isFinite(timestamp) ? timestamp : null,
    vehicleMoveAlongPoints: normalizeRoutePoints(payload?.servedPoints),
    forceFullRedraw: payload?.forceFullRedraw === true,
src/robotaxi/pages/begin-route/hooks/use-active-trip-scale.ts
@@ -1,6 +1,7 @@
import Taro from '@tarojs/taro';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useCallback, useMemo } from 'react';
import { TripStatus } from '../constants/trip-status';
import useVehicleSpeedScale from './use-vehicle-speed-scale';
type RoutePoint = {
  latitude: number;
@@ -8,14 +9,23 @@
};
type Deps = {
  /** 当前行程状态。 */
  tripStatus: TripStatus;
  speedKmh: number | null;
  /** 当前速度,单位 m/s。 */
  speedMps: number | null;
  /** 当前地图比例尺。 */
  mapScale: number;
  /** 设置地图比例尺。 */
  setMapScale: (value: number) => void;
  /** 动态缩放生效后同步车辆视口中心。 */
  syncActiveTripViewportToVehicle: () => void;
  /** 计算 includePoints 所需底部留白。 */
  computeBottomPadding: (points: any[], viewportType: string) => number;
  /** 读取顶部安全区高度。 */
  resolveSafeTopInset: () => number;
  /** 暂停动态缩放。 */
  holdDynamicScale: (durationMs?: number) => void;
  /** 读取动态缩放暂停截止时间。 */
  getDynamicScaleHoldUntil: () => number;
};
@@ -23,149 +33,26 @@
const USER_GESTURE_CAUSED_BY = new Set(['gesture', 'drag', 'scale']); // 需要暂停速度缩放的用户手势来源
const USER_GESTURE_HOLD_MS = 3000; // 用户手势后暂停速度缩放时长
const ORDER_UPDATE_LINE_HOLD_MS = 2500; // ORDER_UPDATE_LINE 全路线入屏后暂停速度缩放时长
const SCALE_RESUME_BUFFER_MS = 80; // 冷却窗口结束后的恢复缓冲时间
const SCALE_STEP = 0.5; // 比例尺离散步进,避免频繁抖动
const MIN_SCALE_CHANGE = 0.25; // 触发 setMapScale 的最小比例尺变化
const SPEED_SMOOTHING_SAMPLE_SIZE = 3; // 速度平滑采样数量
const SCALE_BAND_HYSTERESIS_KMH = 4; // 档位切换滞回,避免边界抖动
const MAX_SCALE_STEP_PER_TICK = 1; // 单次缩放最大变化,避免突然大幅跳变
const ORDER_UPDATE_LINE_SIDE_PADDING = 100; // ORDER_UPDATE_LINE 路线入屏左右留白
const ORDER_UPDATE_LINE_TOP_PADDING = 24; // ORDER_UPDATE_LINE 路线入屏最小顶部留白
const ORDER_UPDATE_LINE_TOP_EXTRA_PADDING = 32; // ORDER_UPDATE_LINE 顶部安全区附加留白
type ScaleBand = {
  minSpeedKmh: number;
  maxSpeedKmh: number;
  scale: number;
};
const DYNAMIC_SCALE_BANDS: ScaleBand[] = [
  { minSpeedKmh: 0, maxSpeedKmh: 10, scale: 18 },
  { minSpeedKmh: 10, maxSpeedKmh: 20, scale: 17.5 },
  { minSpeedKmh: 20, maxSpeedKmh: 30, scale: 17 },
  { minSpeedKmh: 30, maxSpeedKmh: 40, scale: 16 },
  { minSpeedKmh: 40, maxSpeedKmh: 50, scale: 15.5 },
  { minSpeedKmh: 50, maxSpeedKmh: 60, scale: 15 },
  { minSpeedKmh: 60, maxSpeedKmh: Number.POSITIVE_INFINITY, scale: 14 }
]; // 基于车速的动态缩放档位,覆盖低速精细、中速平衡、高速宏观三类视角
/**
 * 将数值限制在给定区间内。
 */
const clamp = (value: number, min: number, max: number) => {
  if (value < min) return min;
  if (value > max) return max;
  return value;
};
/**
 * 根据速度命中所属缩放档位。
 */
const resolveScaleBandIndex = (speedKmh: number) => {
  const safeSpeed = Math.max(0, Number(speedKmh) || 0);
  const bandIndex = DYNAMIC_SCALE_BANDS.findIndex((band) => safeSpeed >= band.minSpeedKmh && safeSpeed < band.maxSpeedKmh);
  return bandIndex >= 0 ? bandIndex : DYNAMIC_SCALE_BANDS.length - 1;
};
/**
 * 基于上一次档位应用滞回,避免速度在边界附近频繁切换。
 */
const resolveStableScaleBandIndex = (speedKmh: number, previousBandIndex: number | null) => {
  const targetBandIndex = resolveScaleBandIndex(speedKmh);
  if (previousBandIndex === null || previousBandIndex === targetBandIndex) {
    return targetBandIndex;
  }
  let nextBandIndex = previousBandIndex;
  if (targetBandIndex > previousBandIndex) {
    while (nextBandIndex < targetBandIndex) {
      const fasterBand = DYNAMIC_SCALE_BANDS[nextBandIndex + 1];
      if (speedKmh >= fasterBand.minSpeedKmh + SCALE_BAND_HYSTERESIS_KMH) {
        nextBandIndex += 1;
        continue;
      }
      break;
    }
    return nextBandIndex;
  }
  while (nextBandIndex > targetBandIndex) {
    const currentBand = DYNAMIC_SCALE_BANDS[nextBandIndex];
    if (speedKmh < currentBand.minSpeedKmh - SCALE_BAND_HYSTERESIS_KMH) {
      nextBandIndex -= 1;
      continue;
    }
    break;
  }
  return nextBandIndex;
};
/**
 * 对最近几帧速度做均值平滑,降低单帧抖动对缩放的影响。
 */
const resolveSmoothedSpeed = (samples: number[], nextSpeedKmh: number) => {
  const nextSamples = [...samples, Math.max(0, nextSpeedKmh)].slice(-SPEED_SMOOTHING_SAMPLE_SIZE);
  const total = nextSamples.reduce((sum, item) => sum + item, 0);
  return {
    nextSamples,
    smoothedSpeedKmh: total / nextSamples.length
  };
};
/**
 * 将目标比例尺限制为渐进变化,避免视角一步跳太远。
 */
const resolveNextScale = (currentScale: number, targetScale: number) => {
  const scaleDelta = targetScale - currentScale;
  if (Math.abs(scaleDelta) <= MAX_SCALE_STEP_PER_TICK) {
    return targetScale;
  }
  return currentScale + Math.sign(scaleDelta) * MAX_SCALE_STEP_PER_TICK;
};
/**
 * 将速度换算成目标比例尺,速度越高视角越远。
 */
const resolveScaleBySpeed = (speedKmh: number, previousBandIndex: number | null) => {
  const nextBandIndex = resolveStableScaleBandIndex(speedKmh, previousBandIndex);
  return {
    targetScale: DYNAMIC_SCALE_BANDS[nextBandIndex].scale,
    bandIndex: nextBandIndex
  };
};
/**
 * 接驾中/行程中动态缩放:由速度驱动比例尺,并保留 ORDER_UPDATE_LINE 的一次性全路线入屏。
 * 接驾中/行程中动态缩放入口:速度缩放单独交给专用 Hook,这里仅保留视口编排职责。
 */
export default function useActiveTripScale(deps: Deps) {
  const [resumeVersion, setResumeVersion] = useState(0); // 冷却结束后触发重新评估
  const resumeTimerRef = useRef<any>(null);
  const speedSamplesRef = useRef<number[]>([]);
  const scaleBandIndexRef = useRef<number | null>(null);
  const isActiveStatus = useMemo(() => {
    return ACTIVE_TRIP_STATUSES.includes(deps.tripStatus);
  }, [deps.tripStatus]);
  /**
   * 清理冷却恢复定时器。
   */
  const clearResumeTimer = () => {
    if (!resumeTimerRef.current) {
      return;
    }
    clearTimeout(resumeTimerRef.current);
    resumeTimerRef.current = null;
  };
  /**
   * 在冷却结束后触发一次动态缩放重算。
   */
  const scheduleResume = useCallback((delayMs: number) => {
    clearResumeTimer();
    const safeDelayMs = Math.max(0, Math.ceil(delayMs));
    resumeTimerRef.current = setTimeout(() => {
      setResumeVersion((value) => value + 1);
      resumeTimerRef.current = null;
    }, safeDelayMs);
  }, []);
  useVehicleSpeedScale({
    tripStatus: deps.tripStatus,
    speedMps: deps.speedMps,
    mapScale: deps.mapScale,
    setMapScale: deps.setMapScale,
    syncActiveTripViewportToVehicle: deps.syncActiveTripViewportToVehicle,
    getDynamicScaleHoldUntil: deps.getDynamicScaleHoldUntil
  });
  /**
   * 处理地图手势,手动缩放/拖拽后短暂暂停速度接管。
@@ -176,8 +63,7 @@
      return;
    }
    deps.holdDynamicScale(USER_GESTURE_HOLD_MS);
    scheduleResume(USER_GESTURE_HOLD_MS + SCALE_RESUME_BUFFER_MS);
  }, [deps.holdDynamicScale, scheduleResume]);
  }, [deps.holdDynamicScale]);
  /**
   * ORDER_UPDATE_LINE 渲染完成后,自动执行一次全路线入屏。
@@ -202,43 +88,7 @@
        });
      }, 0);
    });
    scheduleResume(ORDER_UPDATE_LINE_HOLD_MS + SCALE_RESUME_BUFFER_MS);
  }, [deps.computeBottomPadding, deps.holdDynamicScale, deps.resolveSafeTopInset, isActiveStatus, scheduleResume]);
  useEffect(() => {
    if (!isActiveStatus) {
      clearResumeTimer();
      speedSamplesRef.current = [];
      scaleBandIndexRef.current = null;
      return;
    }
    const speedKmh = Number(deps.speedKmh);
    if (!Number.isFinite(speedKmh)) {
      return;
    }
    const holdUntil = Number(deps.getDynamicScaleHoldUntil());
    const now = Date.now();
    if (Number.isFinite(holdUntil) && holdUntil > now) {
      scheduleResume(holdUntil - now + SCALE_RESUME_BUFFER_MS);
      return;
    }
    const { nextSamples, smoothedSpeedKmh } = resolveSmoothedSpeed(speedSamplesRef.current, speedKmh);
    speedSamplesRef.current = nextSamples;
    const { targetScale, bandIndex } = resolveScaleBySpeed(smoothedSpeedKmh, scaleBandIndexRef.current);
    scaleBandIndexRef.current = bandIndex;
    const nextScale = resolveNextScale(deps.mapScale, targetScale);
    if (Math.abs(nextScale - deps.mapScale) < MIN_SCALE_CHANGE) {
      return;
    }
    deps.setMapScale(clamp(nextScale, 14, 18));
    deps.syncActiveTripViewportToVehicle();
  }, [deps.getDynamicScaleHoldUntil, deps.mapScale, deps.setMapScale, deps.speedKmh, deps.syncActiveTripViewportToVehicle, isActiveStatus, resumeVersion, scheduleResume]);
  useEffect(() => {
    return () => {
      clearResumeTimer();
    };
  }, []);
  }, [deps.computeBottomPadding, deps.holdDynamicScale, deps.resolveSafeTopInset, isActiveStatus]);
  return {
    handleRegionChange,
src/robotaxi/pages/begin-route/hooks/use-bluetooth-access.ts
@@ -21,6 +21,20 @@
const SYSTEM_OFF_ERROR_RE = /(not available|10001|未开启|开关未打开|bluetooth adapter is not available)/i;
const ENV_UNSUPPORTED_ERROR_RE = /(暂不支持|not support|devtools|Mac 以外)/i;
const PERMISSION_DENY_ERROR_RE = /deny|auth deny|authorize|permission|scope/i;
const BLUETOOTH_ACCESS_LOG_PREFIX = '[蓝牙权限]';
const BLUETOOTH_AUTH_SYNC_DELAY_MS = 300; // 授权完成后首轮刷新延迟,给小程序权限态一点同步时间
const BLUETOOTH_AUTH_PROTECTION_WINDOW_MS = 5000; // 授权成功后的保护窗口,避免瞬时 permission_deny 覆盖 UI
/**
 * 输出蓝牙权限链路日志,便于排查授权、系统蓝牙和环境问题。
 */
function logBluetoothAccess(message: string, payload?: Record<string, unknown>) {
  if (payload) {
    console.log(BLUETOOTH_ACCESS_LOG_PREFIX, message, payload);
    return;
  }
  console.log(BLUETOOTH_ACCESS_LOG_PREFIX, message);
}
/**
 * 统一分类蓝牙打开失败原因。
@@ -46,11 +60,18 @@
async function openBluetoothAdapterSafe(): Promise<BluetoothOpenResult> {
  try {
    await Taro.openBluetoothAdapter();
    logBluetoothAccess('openBluetoothAdapter 成功');
    return { opened: true, errorType: 'unknown', error: null };
  } catch (error: any) {
    const errorType = classifyBluetoothError(error);
    logBluetoothAccess('openBluetoothAdapter 失败', {
      errorType,
      errCode: error?.errCode,
      errMsg: error?.errMsg
    });
    return {
      opened: false,
      errorType: classifyBluetoothError(error),
      errorType,
      error
    };
  }
@@ -71,6 +92,7 @@
  const openSettingPollingTimerRef = useRef<ReturnType<typeof setInterval> | null>(null); // 去开启后的短轮询定时器
  const openSettingPollingCountRef = useRef(0); // 去开启后的短轮询计数
  const startBluetoothAuthRef = useRef(startBluetoothAuth); // 蓝牙鉴权动作引用,避免函数引用变化导致状态 effect 反复触发
  const recentAuthorizedAtRef = useRef(0); // 最近一次确认蓝牙授权成功的时间戳,用于保护首次授权后的 UI 状态
  const stopOpenSettingPollingRef = useRef<() => void>(() => {});
  const refreshBluetoothStateRef = useRef<() => Promise<void>>(async () => {});
  const startOpenSettingPollingRef = useRef<() => void>(() => {});
@@ -84,11 +106,19 @@
    try {
      const setting = await Taro.getSetting();
      const authSetting = setting?.authSetting || {};
      logBluetoothAccess('读取小程序权限设置', {
        bluetoothScope: authSetting?.[BLUETOOTH_SCOPE],
        authSetting
      });
      if (Object.prototype.hasOwnProperty.call(authSetting, BLUETOOTH_SCOPE)) {
        return authSetting[BLUETOOTH_SCOPE] === true ? 'authorized' : 'denied';
      }
    } catch (error) {
      logBluetoothAccess('读取小程序权限设置失败', {
        error
      });
    }
    logBluetoothAccess('小程序蓝牙权限状态未知');
    return 'unknown';
  }, []);
@@ -96,38 +126,51 @@
   * 已确认拿到小程序蓝牙权限后,统一收口后续初始化与兜底提示。
   */
  const finishBluetoothAuthorization = useCallback(async () => {
    logBluetoothAccess('进入授权完成收口流程', {
      tripStatus
    });
    recentAuthorizedAtRef.current = Date.now();
    Taro.setStorageSync(BLUETOOTH_AUTH_KEY, true);
    setShowAuthBluetoothPopup(false);
    setStatus('authorized');
    setStatusReady(true);
    startBluetoothAuth();
    startOpenSettingPollingRef.current();
    const openResult = await openBluetoothAdapterSafe();
    logBluetoothAccess('授权完成后再次初始化蓝牙适配器', {
      opened: openResult.opened,
      errorType: openResult.errorType,
      errCode: openResult.error?.errCode,
      errMsg: openResult.error?.errMsg
    });
    if (openResult.opened) {
      await refreshBluetoothState();
      setTimeout(() => {
        refreshBluetoothStateRef.current();
      }, BLUETOOTH_AUTH_SYNC_DELAY_MS);
      return;
    }
    if (openResult.errorType === 'permission_deny') {
      setStatus('unauthorized');
      try {
        await Taro.openSetting();
      } catch (openSettingError) {
        console.error('打开设置失败:', openSettingError);
      }
      Taro.showToast({ title: '请先授权小程序蓝牙权限', icon: 'none' });
      logBluetoothAccess('授权成功后权限态仍在同步,保护窗口内维持已授权态', {
        protectionWindowMs: BLUETOOTH_AUTH_PROTECTION_WINDOW_MS
      });
      return;
    }
    if (openResult.errorType === 'system_off') {
      recentAuthorizedAtRef.current = 0;
      stopOpenSettingPollingRef.current();
      setStatus('system_off');
      Taro.showToast({ title: '请先开启系统蓝牙', icon: 'none' });
      return;
    }
    Taro.showToast({ title: '蓝牙初始化失败,请稍后重试', icon: 'none' });
    await refreshBluetoothState();
  }, [refreshBluetoothState, startBluetoothAuth]);
    setTimeout(() => {
      refreshBluetoothStateRef.current();
    }, BLUETOOTH_AUTH_SYNC_DELAY_MS);
  }, [startBluetoothAuth, tripStatus]);
  useEffect(() => {
    startBluetoothAuthRef.current = startBluetoothAuth;
@@ -151,12 +194,26 @@
   */
  const refreshBluetoothStateImpl = async () => {
    let hasMiniappAuth = Boolean(Taro.getStorageSync(BLUETOOTH_AUTH_KEY));
    const isAuthorizeProtectionActive = (Date.now() - recentAuthorizedAtRef.current) < BLUETOOTH_AUTH_PROTECTION_WINDOW_MS;
    logBluetoothAccess('开始刷新蓝牙状态', {
      cachedMiniappAuth: hasMiniappAuth,
      tripStatus,
      isAuthorizeProtectionActive
    });
    try {
      const setting = await Taro.getSetting();
      logBluetoothAccess('刷新蓝牙状态时读取设置', {
        bluetoothScope: setting?.authSetting?.['scope.bluetooth'],
        authSetting: setting?.authSetting || {}
      });
      if (setting?.authSetting?.['scope.bluetooth']) {
        hasMiniappAuth = true;
        recentAuthorizedAtRef.current = 0;
      }
    } catch (error) {
      logBluetoothAccess('刷新蓝牙状态时读取设置失败', {
        error
      });
    }
    let systemBluetoothEnabled = false;
@@ -165,23 +222,48 @@
      systemBluetoothEnabled = true;
    } else if (openResult.errorType === 'system_off') {
      systemBluetoothEnabled = false;
      recentAuthorizedAtRef.current = 0;
    } else if (openResult.errorType === 'permission_deny') {
      systemBluetoothEnabled = true;
      hasMiniappAuth = false;
      if (!isAuthorizeProtectionActive) {
        hasMiniappAuth = false;
      } else {
        logBluetoothAccess('授权保护窗口内忽略瞬时 permission_deny', {
          errCode: openResult.error?.errCode,
          errMsg: openResult.error?.errMsg
        });
      }
    } else {
      // 开发工具限制和未知异常按“系统已开启”兜底,避免误判成“去开启”。
      systemBluetoothEnabled = true;
    }
    logBluetoothAccess('蓝牙状态刷新结果', {
      systemBluetoothEnabled,
      hasMiniappAuth,
      opened: openResult.opened,
      errorType: openResult.errorType,
      errCode: openResult.error?.errCode,
      errMsg: openResult.error?.errMsg
    });
    if (!systemBluetoothEnabled) {
      setStatus('system_off');
      setStatusReady(true);
      logBluetoothAccess('蓝牙状态判定完成', {
        nextStatus: 'system_off'
      });
      return;
    }
    const nextStatus = hasMiniappAuth ? 'authorized' : 'unauthorized';
    setStatus(nextStatus);
    setStatusReady(true);
    stopOpenSettingPollingRef.current();
    if (nextStatus !== 'authorized') {
      recentAuthorizedAtRef.current = 0;
    }
    logBluetoothAccess('蓝牙状态判定完成', {
      nextStatus
    });
    if (nextStatus === 'authorized') {
      startBluetoothAuthRef.current?.();
    }
@@ -202,8 +284,12 @@
  const startOpenSettingPollingImpl = () => {
    stopOpenSettingPollingRef.current();
    openSettingPollingCountRef.current = 0;
    logBluetoothAccess('启动设置页返回后的短轮询');
    openSettingPollingTimerRef.current = setInterval(() => {
      openSettingPollingCountRef.current += 1;
      logBluetoothAccess('轮询刷新蓝牙状态', {
        pollingCount: openSettingPollingCountRef.current
      });
      refreshBluetoothStateRef.current();
      if (openSettingPollingCountRef.current >= 10) {
        stopOpenSettingPollingRef.current();
@@ -219,6 +305,10 @@
  const startBluetoothStateWatchImpl = () => {
    if (adapterStateHandlerRef.current) return;
    const onBluetoothStateChange = (res: any) => {
      logBluetoothAccess('收到蓝牙适配器状态变更', {
        available: res?.available,
        discovering: res?.discovering
      });
      if (res?.available === false) {
        setStatus('system_off');
        return;
@@ -250,6 +340,9 @@
  // 仅在 4/5 状态维护蓝牙 UI;其他状态强制收起弹窗
  useEffect(() => {
    if ([TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup].includes(tripStatus)) {
      logBluetoothAccess('进入蓝牙 UI 维护态', {
        tripStatus
      });
      setStatusReady(false);
      refreshBluetoothState();
      startBluetoothStateWatchRef.current();
@@ -257,6 +350,9 @@
        stopBluetoothStateWatchRef.current();
      };
    }
    logBluetoothAccess('离开蓝牙 UI 维护态,重置弹窗与监听', {
      tripStatus
    });
    setStatusReady(false);
    stopBluetoothStateWatchRef.current();
    stopOpenSettingPollingRef.current();
@@ -266,6 +362,7 @@
  // 跳转系统蓝牙设置,并在返回后触发一次状态刷新
  const handleOpenSystemBluetoothSetting = useCallback(async () => {
    logBluetoothAccess('用户点击去开启系统蓝牙');
    try {
      const openSystemBluetoothSetting = (Taro as any).openSystemBluetoothSetting;
      if (typeof openSystemBluetoothSetting === 'function') {
@@ -274,8 +371,12 @@
        await Taro.openSetting();
      }
    } catch (error) {
      logBluetoothAccess('打开系统蓝牙设置失败', {
        error
      });
      Taro.showToast({ title: '请手动开启系统蓝牙', icon: 'none' });
    } finally {
      logBluetoothAccess('系统蓝牙设置流程结束,准备刷新状态');
      startOpenSettingPollingRef.current();
      setTimeout(() => {
        refreshBluetoothState();
@@ -295,15 +396,24 @@
  // 执行小程序蓝牙授权,成功后展示成功弹窗
  const handleAuthorizeBluetooth = useCallback(async () => {
    const miniappBluetoothAuthState = await getMiniappBluetoothAuthState();
    logBluetoothAccess('用户触发蓝牙授权', {
      miniappBluetoothAuthState
    });
    if (miniappBluetoothAuthState === 'denied') {
      try {
        await Taro.openSetting();
      } catch (openSettingError) {
        console.error('打开设置失败:', openSettingError);
        logBluetoothAccess('蓝牙权限已拒绝,打开设置失败', {
          openSettingError
        });
        Taro.showToast({ title: '请先授权小程序蓝牙权限', icon: 'none' });
        return;
      }
      const latestAuthState = await getMiniappBluetoothAuthState();
      logBluetoothAccess('从设置页返回后重新读取蓝牙权限', {
        latestAuthState
      });
      if (latestAuthState !== 'authorized') {
        Taro.showToast({ title: '请先授权小程序蓝牙权限', icon: 'none' });
        return;
@@ -314,16 +424,26 @@
    try {
      await Taro.authorize({ scope: BLUETOOTH_SCOPE });
      logBluetoothAccess('调用 authorize(scope.bluetooth) 成功');
    } catch (authorizeError) {
      console.log('授权蓝牙失败:', authorizeError);
      logBluetoothAccess('调用 authorize(scope.bluetooth) 失败', {
        authorizeError
      });
      try {
        await Taro.openSetting();
      } catch (openSettingError) {
        console.log('打开设置失败:', openSettingError);
        logBluetoothAccess('authorize 失败后打开设置失败', {
          openSettingError
        });
        Taro.showToast({ title: '请先授权小程序蓝牙权限', icon: 'none' });
        return;
      }
      const latestAuthState = await getMiniappBluetoothAuthState();
      logBluetoothAccess('authorize 失败后从设置页返回,重新读取蓝牙权限', {
        latestAuthState
      });
      if (latestAuthState !== 'authorized') {
        Taro.showToast({ title: '请先授权小程序蓝牙权限', icon: 'none' });
        return;
@@ -336,6 +456,10 @@
  // 蓝牙卡片点击分发:未开蓝牙->去开启,未授权->弹授权,已授权->弹成功态说明
  const handleBluetoothCardClick = useCallback(() => {
    logBluetoothAccess('点击蓝牙卡片', {
      status,
      tripStatus
    });
    if (status === 'system_off') {
      handleOpenSystemBluetoothSetting();
      return;
src/robotaxi/pages/begin-route/hooks/use-map-markers.ts
@@ -3,8 +3,6 @@
import { TripStatus } from '../constants/trip-status';
const CHANGE_SITE_UP_ICON_URL = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/change-site-up.png'; // 行程中修改目的地时的上车点图标
const DYNAMIC_MARKER_POSITION_EPSILON = 0.000001; // 动态 marker 位置变化阈值,避免无意义重复刷新
/**
 * 生成 marker 签名,供平移态判断结构是否实际变化。
 */
@@ -16,7 +14,7 @@
      shouldIgnoreDynamicCarPosition ? 'translate' : Number(marker.latitude).toFixed(6),
      shouldIgnoreDynamicCarPosition ? 'translate' : Number(marker.longitude).toFixed(6),
      marker.name || '',
      shouldIgnoreDynamicCarPosition ? 'translate' : (typeof marker.rotate === 'number' ? marker.rotate : ''),
      typeof marker.rotate === 'number' ? marker.rotate : '',
      marker?.customCallout?.anchorY ?? '',
    ].join('|');
  })
@@ -178,7 +176,7 @@
  // 应用 marker 状态并处理“行驶中”阶段的刷新标记
  const applyMarkerState = (nextMarkers: any[], nextMarkersSignature: string, shouldSkipUpdate: boolean, carMarker: any) => {
    if (shouldSkipUpdate) {
      // 平移态下保留已创建车辆 marker 的当前位置和角度,只同步其他 marker 结构变化
      // 平移态下车辆位置完全交给 translateMarker 托管,React 仅同步非车辆 marker 的结构变化
      setMarkers((prevMarkers: any[]) => {
        const prevMarkersSignature = buildMarkersSignature(prevMarkers, deps.vehicleMarkerId, shouldUsePureTranslateCarMarker);
        const targetCarIndex = prevMarkers.findIndex((item: any) => item.id === deps.vehicleMarkerId);
@@ -198,25 +196,15 @@
          return prevMarkers;
        }
        const prevCar = prevMarkers[targetCarIndex] || {};
        const nextCar = nextMarkers[nextCarIndex] || {};
        const shouldRefreshCarPosition = (
          Math.abs(Number(prevCar.latitude) - Number(nextCar.latitude)) > DYNAMIC_MARKER_POSITION_EPSILON
          || Math.abs(Number(prevCar.longitude) - Number(nextCar.longitude)) > DYNAMIC_MARKER_POSITION_EPSILON
          || (
            typeof nextCar.rotate === 'number'
            && Number(nextCar.rotate) !== Number(prevCar.rotate)
          )
        );
        if (prevMarkersSignature === nextMarkersSignature && !shouldRefreshCarPosition) {
        if (prevMarkersSignature === nextMarkersSignature) {
          return prevMarkers;
        }
        markersSignatureRef.current = nextMarkersSignature;
        const mergedMarkers = [...nextMarkers];
        mergedMarkers[nextCarIndex] = {
          ...mergedMarkers[nextCarIndex],
          latitude: shouldRefreshCarPosition ? mergedMarkers[nextCarIndex].latitude : prevCar.latitude,
          longitude: shouldRefreshCarPosition ? mergedMarkers[nextCarIndex].longitude : prevCar.longitude,
          // 平移态优先使用最新车辆朝向,动画丢帧时仍由 React 坐标兜底刷新当前位置。
          latitude: prevCar.latitude,
          longitude: prevCar.longitude,
          rotate: typeof mergedMarkers[nextCarIndex]?.rotate === 'number'
            ? mergedMarkers[nextCarIndex].rotate
            : prevCar.rotate
src/robotaxi/pages/begin-route/hooks/use-modify-destination-preview-route.ts
@@ -14,6 +14,7 @@
  effectiveEndSite: any;
  startSite: any;
  vehicleLocation: any;
  pageDidShowVersion: number;
  requestRouteAndFare: (
    originLat: number,
    originLng: number,
@@ -74,6 +75,9 @@
  }
  const startPointId = deps.startSite?.id ?? cachedStartSite?.id ?? '';
  if (startPointId === '' || startPointId === null || startPointId === undefined) {
    return null;
  }
  return {
    originLat,
    originLng,
@@ -111,12 +115,13 @@
      {
        startPointId: routeContext.startPointId,
        force: true
      }
      },
    );
  }, [
    deps.tripStatus,
    deps.isModifyDestinationReviewVisible,
    deps.hasLocalDestinationOverride,
    deps.pageDidShowVersion,
    deps.effectiveEndSite?.id,
    deps.effectiveEndSite?.latitude,
    deps.effectiveEndSite?.longitude,
@@ -174,7 +179,7 @@
      {
        startPointId: routeContext.startPointId,
        force: true
      }
      },
    );
    Taro.showToast({
      title: '报价已更新,请再次确认修改',
src/robotaxi/pages/begin-route/hooks/use-order-actions.shared.ts
@@ -14,6 +14,25 @@
export const LOGIN_PAGE_URL = '/pages/user/login?hideBack=1';
/** 自动返回首页的延迟时长。 */
export const HOME_RELAUNCH_DELAY_MS = 1500;
/** 待确认取消上下文有效期,单位毫秒。 */
export const PENDING_CANCEL_CONTEXT_TTL_MS = 15 * 1000;
/** 待确认取消上下文来源。 */
export type PendingCancelSource = 'pickup_timeout';
/** 待确认取消上下文。 */
export type PendingCancelContext = {
  /** 取消关联订单 ID。 */
  orderId: string;
  /** 取消前页面真实行程状态。 */
  tripStatus: TripStatus;
  /** 期望展示的取消弹窗类型。 */
  cancelType: number;
  /** 取消来源。 */
  source: PendingCancelSource;
  /** 上下文创建时间戳。 */
  createdAt: number;
};
/** 订单动作依赖。 */
export type OrderActionsDeps = {
@@ -37,6 +56,8 @@
  callSubmitting: boolean;
  /** 取消类型。 */
  cancelType: number;
  /** 待确认取消上下文。 */
  pendingCancelContextRef: React.MutableRefObject<PendingCancelContext | null>;
  /** 设置行程状态。 */
  setTripStatus: (value: TripStatus) => void;
  /** 设置订单 ID。 */
src/robotaxi/pages/begin-route/hooks/use-order-actions.ts
@@ -20,6 +20,7 @@
    handleConfirmCancel: cancelActions.handleConfirmCancel,
    handleOpenCancelPopupWithType: cancelActions.handleOpenCancelPopupWithType,
    handleOpenCancelPopupByTripStatus: cancelActions.handleOpenCancelPopupByTripStatus,
    handlePickupTimeoutAutoCancel: cancelActions.handlePickupTimeoutAutoCancel,
    handleConfirmOverRoute: paymentActions.handleConfirmOverRoute,
    handleFinishCountdownTimeout: paymentActions.handleFinishCountdownTimeout,
    handlePay: paymentActions.handlePay,
src/robotaxi/pages/begin-route/hooks/use-order-cancel.ts
@@ -14,6 +14,8 @@
  handleOpenCancelPopupWithType: (type: number) => void;
  /** 根据状态打开取消弹窗。 */
  handleOpenCancelPopupByTripStatus: (status: TripStatus, displayInfo?: any) => void;
  /** 上车倒计时超时后发起自动取消。 */
  handlePickupTimeoutAutoCancel: () => void;
  /** 确认取消。 */
  handleConfirmCancel: () => Promise<void>;
};
@@ -63,6 +65,21 @@
    clearActiveOrderRuntime();
    deps.setCancelType(type);
    restartPlanningRoute();
  };
  /**
   * 上车倒计时超时后发起自动取消,并记录取消前真实状态供 socket 确认时复用。
   */
  const handlePickupTimeoutAutoCancel = () => {
    const targetOrderId = String(deps.orderId || Taro.getStorageSync('orderId') || '');
    deps.pendingCancelContextRef.current = {
      orderId: targetOrderId,
      tripStatus: deps.tripStatus,
      cancelType: 4,
      source: 'pickup_timeout',
      createdAt: Date.now()
    };
    handleOpenCancelPopupWithType(4);
  };
  /**
@@ -125,6 +142,7 @@
    restartPlanningRoute,
    handleOpenCancelPopupWithType,
    handleOpenCancelPopupByTripStatus,
    handlePickupTimeoutAutoCancel,
    handleConfirmCancel
  };
}
src/robotaxi/pages/begin-route/hooks/use-page-restore.ts
@@ -4,8 +4,13 @@
import { getOrderActive, } from '../../../api/home';
import { queryVehicleByOrderId } from '../../../api/begin-route';
import { toTripStatus, TripStatus } from '../constants/trip-status';
import { MODIFY_DESTINATION_SESSION_KEY } from '../constants/modify-destination';
import { MODIFY_DESTINATION_CANDIDATE_KEY, MODIFY_DESTINATION_SESSION_KEY } from '../constants/modify-destination';
import { readPassedRouteCache } from './passed-route-cache';
type RequestRouteOptions = {
  startPointId?: string | number;
  force?: boolean;
};
type Deps = {
  wsHost: string;
@@ -25,14 +30,32 @@
    originLng: number,
    destLat: number,
    destLng: number,
    startPointId: string | number,
    force?: boolean
    options: RequestRouteOptions
  ) => void;
  routeRequestOrderRoute: (orderId: string, status: number) => void;
  routeUpdateWalkingRouteForStart: (startSite: any) => void;
  getCurrentTripStatus: () => TripStatus;
  getCurrentLongitude: () => number | null;
  getCurrentLatitude: () => number | null;
  getCurrentVehicleLocation: () => { latitude: number; longitude: number } | null;
};
/**
 * 判断当前是否处于可恢复的修改目的地预览态。
 */
const isPreviewRouteRestoreActive = (cachedOrderId: string, cachedTripStatus: TripStatus) => {
  const modifyDestinationSession = Taro.getStorageSync(MODIFY_DESTINATION_SESSION_KEY);
  const modifyDestinationCandidate = Taro.getStorageSync(MODIFY_DESTINATION_CANDIDATE_KEY);
  const isPreviewSessionActive = !!modifyDestinationSession?.active;
  const isSessionOrderMatched = !modifyDestinationSession?.orderId
    || !cachedOrderId
    || String(modifyDestinationSession.orderId) === String(cachedOrderId);
  const hasValidCandidate = Number.isFinite(Number(modifyDestinationCandidate?.latitude))
    && Number.isFinite(Number(modifyDestinationCandidate?.longitude));
  if (!isPreviewSessionActive || !isSessionOrderMatched || !hasValidCandidate) {
    return false;
  }
  return [TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup, TripStatus.InService].includes(cachedTripStatus);
};
/**
@@ -113,7 +136,10 @@
    const originLng = s?.longitude || deps.getCurrentLongitude();
    const originLat = s?.latitude || deps.getCurrentLatitude();
    if (cachedTripStatus === TripStatus.PublishRoute && originLng && originLat && e?.longitude && e?.latitude) {
      deps.routeRequestRouteAndFare(originLat, originLng, e.latitude, e.longitude, s?.id, true);
      deps.routeRequestRouteAndFare(originLat, originLng, e.latitude, e.longitude, {
        startPointId: s?.id,
        force: true
      });
    }
    if (deps.getCurrentTripStatus() === TripStatus.PublishRoute && s?.latitude && s?.longitude) {
      deps.routeUpdateWalkingRouteForStart(s);
@@ -136,7 +162,7 @@
  const restorePageStateFromStorage = () => {
    const s = Taro.getStorageSync('startSite');
    const e = Taro.getStorageSync('endSite');
    const cachedOrderId = Taro.getStorageSync('orderId');
    const cachedOrderId = String(Taro.getStorageSync('orderId') || '');
    const cachedTripStatusStorage = Taro.getStorageSync('tripStatus');
    const cachedTripStatus = toTripStatus(cachedTripStatusStorage, deps.getCurrentTripStatus());
    deps.setTripStatus(cachedTripStatus);
@@ -147,7 +173,8 @@
    }
    syncCachedOrderVehicle(cachedOrderId);
    syncSitesFromActiveOrder(cachedTripStatusStorage, s, e);
    if (cachedOrderId && [TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup, TripStatus.InService, TripStatus.ArrivedDestination, TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(cachedTripStatus)) {
    const shouldRestorePreviewRoute = isPreviewRouteRestoreActive(cachedOrderId, cachedTripStatus);
    if (!shouldRestorePreviewRoute && cachedOrderId && [TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup, TripStatus.InService, TripStatus.ArrivedDestination, TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(cachedTripStatus)) {
      deps.routeRequestOrderRoute(cachedOrderId, cachedTripStatus);
    }
    syncLocationAndInitRoute(s, e, cachedTripStatus);
src/robotaxi/pages/begin-route/hooks/use-route-manager.shared.ts
@@ -103,8 +103,8 @@
    distanceMeters: number | null;
    /** 时长,单位秒。 */
    durationSeconds: number | null;
    /** 速度,单位 km/h。 */
    speedKmh: number | null;
    /** 速度,单位 m/s。 */
    speedMps: number | null;
  }) => void;
  /** 设置当前用户定位。 */
  setCurrentUserLocation?: (location: CurrentUserLocation) => void;
src/robotaxi/pages/begin-route/hooks/use-route-request.ts
@@ -5,7 +5,7 @@
import { ensureSelectedCityByLocation } from '../../../utils/city';
import { normalizeDistanceKm, normalizeDurationSeconds } from '../../../utils/format';
import { TripStatus } from '../constants/trip-status';
import { normalizeSpeedKmh, type RouteSnapshot } from './runtime-sync';
import { normalizeSpeedMps, type RouteSnapshot } from './runtime-sync';
import {
  Deps,
  RequestRouteAndFareOptions,
@@ -121,7 +121,7 @@
  distance: normalizeDistanceKm(Number(response?.distance)),
  duration: normalizeDurationSeconds(Number(response?.duration)),
  direction,
  speedKmh: normalizeSpeedKmh(response?.speed),
  speedMps: normalizeSpeedMps(response?.speed),
  timestamp: Date.now(),
  vehicleMoveAlongPoints: [],
  forceFullRedraw: true,
@@ -507,9 +507,7 @@
          response?.endLongitude,
          response?.endLatitude ?? response?.endlatitude
        );
        if (beforeUserSite) {
          deps.setBeforeUserEndSite?.(beforeUserSite);
        }
        deps.setBeforeUserEndSite?.(beforeUserSite);
        const rawPoints = Array.isArray(response?.polyline) ? response.polyline : [];
        const points = rawPoints
@@ -556,7 +554,7 @@
        deps.setOrderRouteMetrics?.({
          distanceMeters: Number.isFinite(Number(response?.distance)) ? Number(response?.distance) : null,
          durationSeconds: Number.isFinite(Number(response?.duration)) ? Number(response?.duration) : null,
          speedKmh: normalizeSpeedKmh(response?.speed)
          speedMps: normalizeSpeedMps(response?.speed)
        });
        const normalizedDirection = resolveDirectionValue(response);
src/robotaxi/pages/begin-route/hooks/use-vehicle-speed-scale.ts
New file
@@ -0,0 +1,203 @@
import { useEffect, useRef } from 'react';
import { TripStatus } from '../constants/trip-status';
type SpeedScaleBandKey = 'low' | 'medium' | 'high';
type SpeedScaleBand = {
  /** 档位标识。 */
  key: SpeedScaleBandKey;
  /** 档位最小速度,单位 m/s。 */
  minSpeedMps: number;
  /** 档位最大速度,单位 m/s。 */
  maxSpeedMps: number;
  /** 档位对应比例尺。 */
  scale: number;
};
type Deps = {
  /** 当前行程状态。 */
  tripStatus: TripStatus;
  /** 当前速度,单位 m/s。 */
  speedMps: number | null;
  /** 当前地图比例尺。 */
  mapScale: number;
  /** 设置地图比例尺。 */
  setMapScale: (value: number) => void;
  /** 动态缩放生效后同步车辆视口中心。 */
  syncActiveTripViewportToVehicle: () => void;
  /** 读取动态缩放暂停截止时间。 */
  getDynamicScaleHoldUntil: () => number;
};
const ACTIVE_TRIP_STATUSES = [TripStatus.VehicleApproaching, TripStatus.InService]; // 允许速度缩放的行程状态
const SCALE_MIN = 15; // 允许的最小比例尺
const SCALE_MAX = 18; // 允许的最大比例尺
const SCALE_APPLY_THRESHOLD = 0.25; // 触发 setMapScale 的最小变化阈值
const SPEED_SCALE_STABLE_DURATION_MS = 10000; // 新档位需要持续命中 10 秒才执行
const HOLD_RESUME_BUFFER_MS = 80; // 冷却结束后的恢复缓冲
const SPEED_SCALE_BANDS: SpeedScaleBand[] = [
  { key: 'low', minSpeedMps: 0, maxSpeedMps: 5, scale: 18 },
  { key: 'medium', minSpeedMps: 5, maxSpeedMps: 10, scale: 16.5 },
  { key: 'high', minSpeedMps: 10, maxSpeedMps: Number.POSITIVE_INFINITY, scale: 15 }
]; // 低速更精细,中速平衡,高速拉远
/**
 * 将比例尺限制在安全区间内。
 */
const clamp = (value: number, min: number, max: number) => {
  if (value < min) {
    return min;
  }
  if (value > max) {
    return max;
  }
  return value;
};
/**
 * 根据速度命中对应档位。
 */
const resolveScaleBandBySpeed = (speedMps: number) => {
  const safeSpeedMps = Math.max(0, Number(speedMps) || 0);
  return SPEED_SCALE_BANDS.find((band) => (
    safeSpeedMps >= band.minSpeedMps && safeSpeedMps < band.maxSpeedMps
  )) || SPEED_SCALE_BANDS[SPEED_SCALE_BANDS.length - 1];
};
/**
 * 根据档位标识获取档位配置。
 */
const getScaleBandByKey = (key: SpeedScaleBandKey | null) => {
  if (!key) {
    return null;
  }
  return SPEED_SCALE_BANDS.find((band) => band.key === key) || null;
};
/**
 * 车辆速度驱动缩放:按 m/s 三段划分,并要求候选档位持续 10 秒才切换。
 */
export default function useVehicleSpeedScale(deps: Deps) {
  const applyTimerRef = useRef<any>(null);
  const latestSpeedMpsRef = useRef<number | null>(deps.speedMps);
  const latestMapScaleRef = useRef(deps.mapScale);
  const appliedBandKeyRef = useRef<SpeedScaleBandKey | null>(null);
  const pendingBandKeyRef = useRef<SpeedScaleBandKey | null>(null);
  const pendingBandStartedAtRef = useRef(0);
  /**
   * 清理候选档位应用定时器。
   */
  const clearApplyTimer = () => {
    if (!applyTimerRef.current) {
      return;
    }
    clearTimeout(applyTimerRef.current);
    applyTimerRef.current = null;
  };
  /**
   * 清空当前候选档位。
   */
  const clearPendingBand = () => {
    pendingBandKeyRef.current = null;
    pendingBandStartedAtRef.current = 0;
    clearApplyTimer();
  };
  /**
   * 安排下一次候选档位检查。
   */
  const schedulePendingApply = (delayMs: number) => {
    clearApplyTimer();
    applyTimerRef.current = setTimeout(() => {
      const latestSpeedMps = Number(latestSpeedMpsRef.current);
      const pendingBandKey = pendingBandKeyRef.current;
      const appliedBandKey = appliedBandKeyRef.current;
      if (!Number.isFinite(latestSpeedMps) || !pendingBandKey) {
        clearPendingBand();
        return;
      }
      const holdUntil = Number(deps.getDynamicScaleHoldUntil());
      const now = Date.now();
      if (Number.isFinite(holdUntil) && holdUntil > now) {
        schedulePendingApply(holdUntil - now + HOLD_RESUME_BUFFER_MS);
        return;
      }
      const nextBand = resolveScaleBandBySpeed(latestSpeedMps);
      if (nextBand.key !== pendingBandKey) {
        clearPendingBand();
        return;
      }
      if (nextBand.key === appliedBandKey) {
        clearPendingBand();
        return;
      }
      const stableDurationMs = now - Number(pendingBandStartedAtRef.current || 0);
      if (stableDurationMs < SPEED_SCALE_STABLE_DURATION_MS) {
        schedulePendingApply(SPEED_SCALE_STABLE_DURATION_MS - stableDurationMs);
        return;
      }
      appliedBandKeyRef.current = nextBand.key;
      clearPendingBand();
      if (Math.abs(nextBand.scale - latestMapScaleRef.current) < SCALE_APPLY_THRESHOLD) {
        return;
      }
      deps.setMapScale(clamp(nextBand.scale, SCALE_MIN, SCALE_MAX));
      deps.syncActiveTripViewportToVehicle();
    }, Math.max(0, Math.ceil(delayMs)));
  };
  useEffect(() => {
    latestSpeedMpsRef.current = deps.speedMps;
  }, [deps.speedMps]);
  useEffect(() => {
    latestMapScaleRef.current = deps.mapScale;
  }, [deps.mapScale]);
  useEffect(() => {
    const isActiveStatus = ACTIVE_TRIP_STATUSES.includes(deps.tripStatus);
    if (!isActiveStatus) {
      clearPendingBand();
      appliedBandKeyRef.current = null;
      return;
    }
    const speedMps = Number(deps.speedMps);
    if (!Number.isFinite(speedMps)) {
      clearPendingBand();
      return;
    }
    const nextBand = resolveScaleBandBySpeed(speedMps);
    const appliedBand = getScaleBandByKey(appliedBandKeyRef.current);
    if (appliedBand && nextBand.key === appliedBand.key) {
      clearPendingBand();
      return;
    }
    if (pendingBandKeyRef.current !== nextBand.key) {
      pendingBandKeyRef.current = nextBand.key;
      pendingBandStartedAtRef.current = Date.now();
    }
    const now = Date.now();
    const holdUntil = Number(deps.getDynamicScaleHoldUntil());
    const stableElapsedMs = now - Number(pendingBandStartedAtRef.current || 0);
    const stableRemainingMs = Math.max(0, SPEED_SCALE_STABLE_DURATION_MS - stableElapsedMs);
    const holdRemainingMs = Number.isFinite(holdUntil) && holdUntil > now
      ? holdUntil - now + HOLD_RESUME_BUFFER_MS
      : 0;
    schedulePendingApply(Math.max(stableRemainingMs, holdRemainingMs));
  }, [
    deps.getDynamicScaleHoldUntil,
    deps.speedMps,
    deps.syncActiveTripViewportToVehicle,
    deps.setMapScale,
    deps.tripStatus
  ]);
  useEffect(() => {
    return () => {
      clearPendingBand();
    };
  }, []);
}
src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx
@@ -71,6 +71,8 @@
  handleOpenCancelPopupByTripStatus: (status: TripStatus, durationDisplay?: any) => void;
  /** 取消超时 */
  handleOpenCancelPopupWithType: (type: number) => void;
  /** 上车倒计时超时自动取消 */
  handlePickupTimeoutAutoCancel: () => void;
  /** 行程状态设置 */
  setTripStatus: (value: TripStatus) => void;
  /** 完成倒计时启用 */
@@ -423,7 +425,7 @@
          handleModifyDestination={deps.handleModifyDestination}
          showModifyDestination={deps.showModifyDestination}
          handleShareTrip={deps.handleShareTrip}
          handleCancelTimeout={() => deps.handleOpenCancelPopupWithType(4)}
          handleCancelTimeout={deps.handlePickupTimeoutAutoCancel}
          handleFinishTimeout={deps.handleFinishCountdownTimeout}
          resetDetailVersion={deps.resetDetailVersion}
          priceUpdateVersion={deps.priceUpdateVersion}
src/robotaxi/pages/begin-route/hooks/use-web-socket-events.ts
@@ -5,13 +5,15 @@
import { clearOrderTripStorage, safeParse } from '../../../../utils/Robotaxi-public-method';
import { normalizeDistanceKm, normalizeDurationSeconds } from '../../../../robotaxi/utils/format';
import { toTripStatus, TripStatus } from '../constants/trip-status';
import { normalizeSocketRouteSnapshot, normalizeSpeedKmh } from './runtime-sync';
import { PENDING_CANCEL_CONTEXT_TTL_MS, PendingCancelContext } from './use-order-actions.shared';
import { normalizeSocketRouteSnapshot, normalizeSpeedMps } from './runtime-sync';
import { MODIFY_DESTINATION_SOCKET_STATUS_KEY } from '../constants/modify-destination';
import { clearPassedRouteCache, writePassedRouteCache } from './passed-route-cache';
import { resolveVehiclePoint } from './trip-motion-utils';
type UseWebSocketEventsDeps = {
    tripStatusRef: React.MutableRefObject<TripStatus>;
    pendingCancelContextRef: React.MutableRefObject<PendingCancelContext | null>;
    modifyDestinationPickupSuccessAtRef: React.MutableRefObject<number>;
    modifyDestinationPickupStatusGuardMs: number;
    orderId: string;
@@ -45,7 +47,7 @@
    setOrderRouteMetrics?: (metrics: {
        distanceMeters: number | null;
        durationSeconds: number | null;
        speedKmh: number | null;
        speedMps: number | null;
    }) => void;
    setBeforeUserEndSite: (site: { longitude: number; latitude: number } | null) => void;
    isModifyDestinationReviewVisible?: boolean;
@@ -87,6 +89,39 @@
    if (!incomingOrderId && !currentOrderId) return true;
    if (!incomingOrderId || !currentOrderId) return false;
    return String(incomingOrderId) === String(currentOrderId);
};
/**
 * 获取仍在有效期内的待确认取消上下文;过期则立即清空,避免脏上下文影响后续订单。
 */
const getValidPendingCancelContext = (deps: UseWebSocketEventsDeps) => {
    const pendingCancelContext = deps.pendingCancelContextRef.current;
    if (!pendingCancelContext) {
        return null;
    }
    if (Date.now() - pendingCancelContext.createdAt > PENDING_CANCEL_CONTEXT_TTL_MS) {
        deps.pendingCancelContextRef.current = null;
        return null;
    }
    return pendingCancelContext;
};
/**
 * 判断当前 socket 取消消息是否可复用本地记录的取消前上下文。
 */
const isPendingCancelContextMatched = (payload: any, pendingCancelContext: PendingCancelContext | null) => {
    if (!pendingCancelContext) {
        return false;
    }
    const socketStatus = Number(payload?.status);
    if (socketStatus !== 0) {
        return false;
    }
    const incomingOrderId = String(payload?.content?.orderId || payload?.orderId || '');
    if (!incomingOrderId || !pendingCancelContext.orderId) {
        return true;
    }
    return incomingOrderId === pendingCancelContext.orderId;
};
const resetRouteRuntimeState = (deps: UseWebSocketEventsDeps) => {
@@ -161,9 +196,18 @@
        .catch(() => { });
};
const handleCancelPopupPush = (payload: any, changeStatusOld: TripStatus, deps: UseWebSocketEventsDeps) => {
const handleCancelPopupPush = (
    payload: any,
    changeStatusOld: TripStatus,
    deps: UseWebSocketEventsDeps,
    pendingCancelContext: PendingCancelContext | null
) => {
    if (!payload?.showCancelPopup) return;
    if (changeStatusOld === TripStatus.VehicleApproaching || changeStatusOld === TripStatus.VehicleArrivedPickup) {
    const matchedPendingCancelContext = isPendingCancelContextMatched(payload, pendingCancelContext);
    if (matchedPendingCancelContext) {
        deps.setCancelType(pendingCancelContext?.cancelType || 4);
        deps.pendingCancelContextRef.current = null;
    } else if (changeStatusOld === TripStatus.VehicleApproaching || changeStatusOld === TripStatus.VehicleArrivedPickup) {
        deps.setOrderId('');
        deps.setVehicleInfo({});
        clearOrderTripStorage();
@@ -196,7 +240,8 @@
    lastFetchedVehicleOrderIdRef: any
) => {
    const changeStatusOld = deps.tripStatusRef.current;
    const currentOrderId = deps.orderId || Taro.getStorageSync('orderId');
    const pendingCancelContext = getValidPendingCancelContext(deps);
    const currentOrderId = deps.orderId || Taro.getStorageSync('orderId') || pendingCancelContext?.orderId || '';
    const incomingOrderId = payload?.content?.orderId || payload?.orderId;
    if (!isSameOrder(incomingOrderId, currentOrderId)) {
        return;
@@ -206,7 +251,7 @@
    const effectiveOrderId = resolveOrderId(payload, currentOrderId);
    syncVehicleInfo(effectiveOrderId, deps, lastFetchedVehicleOrderIdRef);
    handleStatusSideEffects(mappedTripStatus, changeStatusOld, deps);
    handleCancelPopupPush(payload, changeStatusOld, deps);
    handleCancelPopupPush(payload, changeStatusOld, deps, pendingCancelContext);
    if (payload?.showPassengerNotice) {
        deps.checkAndShowPassengerNotice();
@@ -234,7 +279,7 @@
    const route = safeParse(payload?.content?.route, {});
    const distanceMeters = Number(route?.distance);
    const durationSecondsRaw = Number(route?.duration);
    const speedKmh = normalizeSpeedKmh(route?.speed);
    const speedMps = normalizeSpeedMps(route?.speed);
    const distanceKm = normalizeDistanceKm(distanceMeters);
    if (distanceKm !== null) {
        deps.setRouteDistance(distanceKm);
@@ -246,7 +291,7 @@
    deps.setOrderRouteMetrics?.({
        distanceMeters: Number.isFinite(distanceMeters) ? distanceMeters : null,
        durationSeconds: Number.isFinite(durationSecondsRaw) ? durationSecondsRaw : null,
        speedKmh
        speedMps
    });
};
@@ -264,9 +309,7 @@
const handleOrderRoutePayload = (payload: any, deps: UseWebSocketEventsDeps) => {
    const beforeUserSite = parseBeforeUserEndSite(payload);
    if (beforeUserSite) {
        deps.setBeforeUserEndSite(beforeUserSite);
    }
    deps.setBeforeUserEndSite(beforeUserSite);
    const currentTripStatus = deps.tripStatusRef.current;
    // socket 路线拦截只跟随真实的“修改目的地预览态”,不要被本地终点覆盖态误伤。
    const isModifyDestinationPreviewActive = Boolean(deps.isModifyDestinationReviewVisible);
src/robotaxi/pages/begin-route/index.tsx
@@ -35,6 +35,7 @@
import useTripRuntime from './hooks/use-trip-runtime';
// 订单动作编排
import useOrderActions from './hooks/use-order-actions';
import { PendingCancelContext } from './hooks/use-order-actions.shared';
// 视图委托渲染编排
import useViewDelegates from './hooks/use-view-delegates';
// 修改目的地流程编排
@@ -72,7 +73,7 @@
type OrderRouteMetrics = {
  distanceMeters: number | null;
  durationSeconds: number | null;
  speedKmh: number | null;
  speedMps: number | null;
};
export default function BeginRoute() {
@@ -143,13 +144,14 @@
  const [orderRouteMetrics, setOrderRouteMetrics] = useState<OrderRouteMetrics>({
    distanceMeters: null,
    durationSeconds: null,
    speedKmh: null
    speedMps: null
  });
  const [currentUserLocation, setCurrentUserLocation] = useState<CurrentUserLocation>(null); // 当前用户真实定位,避免被地图中心点覆盖
  const [modifyDestinationSocketStatus, setModifyDestinationSocketStatus] = useState<number | null>(null); // 后端推送的修改目的地展示状态
  const syncSmoothRouteBaselineRef = useRef<((snapshot: any) => void) | null>(null); // 订单路线接口回显后,同步初始化平滑擦线基线
  const lastSocketDirectionAtRef = useRef(0); // 最近一次由 socket 写入车辆朝向的时间戳
  const beforeBackRef = useRef<() => void>(() => { }); // 左上角返回前的兜底清理,避免预览路线残留到下次进页
  const pendingCancelContextRef = useRef<PendingCancelContext | null>(null); // 待确认取消上下文,避免本地状态重置污染后续 socket 取消弹窗类型判断
  const tripStatusRef = useRef(tripStatus); // 行程状态引用
  const lastTripStatusRef = useRef(tripStatus); // 上一次行程状态,用于检测状态变化
@@ -164,7 +166,7 @@
    setOrderRouteMetrics({
      distanceMeters: null,
      durationSeconds: null,
      speedKmh: null
      speedMps: null
    });
    const cachedSocketStatus = Taro.getStorageSync(MODIFY_DESTINATION_SOCKET_STATUS_KEY);
    const cachedOrderId = String(cachedSocketStatus?.orderId || '');
@@ -459,7 +461,7 @@
  } = viewportState; // 视口状态解构
  const activeTripScale = useActiveTripScale({
    tripStatus,
    speedKmh: orderRouteMetrics?.speedKmh,
    speedMps: orderRouteMetrics?.speedMps,
    mapScale,
    setMapScale: viewportState.setMapScale,
    syncActiveTripViewportToVehicle,
@@ -599,7 +601,8 @@
    routeUpdateWalkingRouteForStart,
    getCurrentTripStatus: () => tripStatusRef.current,
    getCurrentLongitude: () => longitude,
    getCurrentLatitude: () => latitude
    getCurrentLatitude: () => latitude,
    getCurrentVehicleLocation: () => vehicleLocation
  }); // 页面恢复与初始化编排
  const router = Taro.useRouter();
  const tripRuntime = useTripRuntime({
@@ -715,6 +718,7 @@
  const { bindSocketListeners, ensureSocketListeners } = useWebSocketEvents({
    tripStatusRef,
    pendingCancelContextRef,
    orderId,
    setOrderId,
    setTripStatus,
@@ -769,6 +773,7 @@
    routeAndFareError,
    callSubmitting,
    cancelType,
    pendingCancelContextRef,
    setTripStatus,
    setOrderId,
    setVehicleInfo,
@@ -793,6 +798,7 @@
    handleConfirmCancel,
    handleOpenCancelPopupWithType,
    handleOpenCancelPopupByTripStatus,
    handlePickupTimeoutAutoCancel,
    handleConfirmOverRoute,
    handleFinishCountdownTimeout,
    handlePay
@@ -826,6 +832,7 @@
    effectiveEndSite,
    startSite,
    vehicleLocation,
    pageDidShowVersion,
    requestRouteAndFare: routeRequestRouteAndFare
  });
  const shareOrderId = orderId || Taro.getStorageSync('orderId') || '';
@@ -1091,6 +1098,7 @@
    isCallDisabled: routeAndFareLoading || routeAndFareError || callSubmitting,
    handleOpenCancelPopupByTripStatus,
    handleOpenCancelPopupWithType,
    handlePickupTimeoutAutoCancel,
    setTripStatus,
    finishCountdownEnabled,
    orderId,
src/robotaxi/pages/select-map-site/components/select-map-site-view.tsx
@@ -92,7 +92,8 @@
          {((deps.showToolTip && deps.selectedSite.name) || deps.outOfServiceRange) && (
            <View className="toolTip">
              {(deps.outOfServiceRange || deps.selectedSite.time) && (
                <View className="toolTip-bg" style={{ backgroundImage: `url(${deps.tooltipBgSrc})` }}>
                <View className="toolTip-bg">
                  <Image className="toolTip-bg-img" src={deps.tooltipBgSrc} />
                  <Text className="toolTip-bg-text">
                    {deps.outOfServiceRange ? '不在运营区' : `限停${deps.selectedSite.time}分钟`}
                  </Text>
src/robotaxi/pages/select-map-site/index.less
@@ -59,15 +59,25 @@
        z-index: 10;
        .toolTip-bg {
            background-image: url('https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/tooltip-bg.png');
            background-size: cover;
            position: relative;
            height: 70px;
            min-width: 155px;
            text-align: center;
            width: fit-content;
            padding: 0 15px 0 3px;
            .toolTip-bg-img {
                position: absolute;
                top: 0;
                left: 0;
                width: 100%;
                height: 100%;
                z-index: -1;
            }
            .toolTip-bg-text {
                position: relative;
                z-index: 1;
                font-size: 22px;
                font-weight: 500;
                letter-spacing: normal;