hejianhao
2026-05-20 46df658028d791671e42d22ce82c39c4f9b9cab2
feat(robotaxi): 添加已行驶路线缓存, 优化地图动态缩放并修复逻辑问题

- 新增已行驶路线缓存模块,支持持久化与恢复车辆已行驶的路线点数据
- 重构 useActiveTripScale 钩子,采用车速缩放档位+平滑滞回逻辑实现更自然的地图缩放
- 修复个人中心优惠券API数据获取错误以及socket路线的速度单位转换问题
- 优化定位流程,新增forceNearestSite选项并调整活跃订单上车点处理逻辑
- 更新折线渲染依赖项,在页面返回和路线重置时清理已行驶路线缓存
- 移除useActiveTripScale中未使用的routePoints依赖项
1个文件已添加
12个文件已修改
341 ■■■■■ 已修改文件
src/pages/robotaxi/home/hooks/use-home-bootstrap.ts 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/hooks/use-location-flow.ts 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/hooks/use-navigation.ts 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/passed-route-cache.ts 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/runtime-sync.ts 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-active-trip-scale.ts 121 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-map-viewport.ts 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-page-actions.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-page-restore.ts 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-polyline-render.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-web-socket-events.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/index.tsx 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/personal-center/index.tsx 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/hooks/use-home-bootstrap.ts
@@ -1,7 +1,7 @@
import Taro from '@tarojs/taro';
import wsManager from '../../../../utils/websocket';
import { mapOrderStatusToTripStatus } from '../../../../utils/Robotaxi-public-method';
import { TripStatus, toTripStatus } from '../../../../robotaxi/pages/begin-route/constants/trip-status';
import { SocketOrderStatus, TripStatus, toTripStatus } from '../../../../robotaxi/pages/begin-route/constants/trip-status';
import {
  clearHomeStartSiteDisplayState,
  HOME_START_SITE_SOURCE_ACTIVE_ORDER,
@@ -21,6 +21,17 @@
  hasActiveOrder: boolean;
  /** 当前首页确认上车点。 */
  preferredStartSite: any;
  /** 当前是否应按用户位置吸附最近站点。 */
  shouldUseNearestSiteByOrder: boolean;
};
type GetLocationInfoOptions = {
  /** 是否更新地图中心。 */
  updateCenter?: boolean;
  /** 是否强制刷新城市信息。 */
  forceCityRefresh?: boolean;
  /** 是否强制按用户位置吸附最近站点。 */
  forceNearestSite?: boolean;
};
type Deps = {
@@ -61,7 +72,7 @@
  /** 设置经度 */
  setLongitude: (value: number) => void;
  /** 读取定位并更新站点 */
  getLocationInfo: (updateCenter?: boolean, forceCityRefresh?: boolean) => void;
  getLocationInfo: (options?: GetLocationInfoOptions) => void;
  /** 处理切城标记 */
  consumeCitySwitchFlag: () => { switched: boolean; shouldMoveToCityCenter: boolean };
  /** 跳过下一次居中 */
@@ -100,6 +111,13 @@
  };
  /**
   * 判断当前活跃订单是否应按用户位置吸附最近站点。
   */
  const shouldUseNearestSiteByActiveOrderStatus = (status: unknown) => (
    Number(status) >= SocketOrderStatus.PendingDeparture
  );
  /**
   * 将活跃订单起点映射为首页缓存站点。
   */
  const buildStartSiteByActiveOrder = (activeOrder: any) => ({
@@ -131,7 +149,12 @@
    deps.setNeedLogin(!otp);
    if (!otp) {
      deps.setPreferredStartSite(null);
      return { hasLogin: false, hasActiveOrder: false, preferredStartSite: null } as AuthOrderSyncResult;
      return {
        hasLogin: false,
        hasActiveOrder: false,
        preferredStartSite: null,
        shouldUseNearestSiteByOrder: false
      } as AuthOrderSyncResult;
    }
    wsManager.connect(deps.wsHost);
    const agreement = await deps.getAgreement();
@@ -162,33 +185,51 @@
      deps.setTripStatus(0);
      deps.setOrderData({});
      deps.setPreferredStartSite(preferredStartSite);
      return { hasLogin: true, hasActiveOrder: false, preferredStartSite } as AuthOrderSyncResult;
      return {
        hasLogin: true,
        hasActiveOrder: false,
        preferredStartSite,
        shouldUseNearestSiteByOrder: false
      } as AuthOrderSyncResult;
    }
    deps.setOrderData(activeOrder || {});
    const mappedStatus = toHomeTripStatus(mapOrderStatusToTripStatus(activeOrder.status || 0));
    const startSite = buildStartSiteByActiveOrder(activeOrder);
    const endSite = buildEndSiteByActiveOrder(activeOrder);
    const preferredState = readHomeStartSiteDisplayState();
    const shouldUseNearestSiteByOrder = shouldUseNearestSiteByActiveOrderStatus(activeOrder.status);
    const manualSelectedStartSite = isManualSelectedHomeStartSiteDisplayState(preferredState)
      ? resolveHomeStartSiteDisplaySite()
      : null; // 手动选择的上车点优先用于首页回显,避免返回首页时被订单起点覆盖
    const preferredStartSite = manualSelectedStartSite || startSite;
    const preferredStartSite = shouldUseNearestSiteByOrder ? null : (manualSelectedStartSite || startSite);
    deps.setTripStatus(mappedStatus);
    Taro.setStorageSync('tripStatus', mappedStatus);
    Taro.setStorageSync('orderId', activeOrder.id || '');
    Taro.setStorageSync('startSite', preferredStartSite);
    Taro.setStorageSync('endSite', endSite);
    if (!manualSelectedStartSite) {
    if (shouldUseNearestSiteByOrder) {
      Taro.removeStorageSync('startSite');
    } else {
      Taro.setStorageSync('startSite', preferredStartSite);
    }
    if (shouldUseNearestSiteByOrder && preferredState?.source === HOME_START_SITE_SOURCE_ACTIVE_ORDER) {
      clearHomeStartSiteDisplayState();
    }
    if (!shouldUseNearestSiteByOrder && !manualSelectedStartSite) {
      writeHomeStartSiteDisplayState(startSite, HOME_START_SITE_SOURCE_ACTIVE_ORDER);
    }
    deps.setPreferredStartSite(preferredStartSite);
    return { hasLogin: true, hasActiveOrder: true, preferredStartSite } as AuthOrderSyncResult;
    return {
      hasLogin: true,
      hasActiveOrder: true,
      preferredStartSite,
      shouldUseNearestSiteByOrder
    } as AuthOrderSyncResult;
  };
  /**
   * 处理定位与首屏地图状态。
   */
  const processLocationSetting = async (auth: any, startSite: any) => {
  const processLocationSetting = async (auth: any, startSite: any, shouldUseNearestSiteByOrder = false) => {
    const hasStartSite = startSite && startSite.latitude && startSite.longitude;
    deps.setIsLocationAuthDenied(auth === false);
    const switchState = deps.consumeCitySwitchFlag();
@@ -245,6 +286,14 @@
      deps.setLongitude(deps.defaultLongitude);
      return;
    }
    if (shouldUseNearestSiteByOrder) {
      deps.getLocationInfo({
        updateCenter: true,
        forceCityRefresh: true,
        forceNearestSite: true
      });
      return;
    }
    if (auth === undefined) {
      deps.getLocationInfo();
      return;
@@ -253,12 +302,12 @@
    deps.skipNextLocationCenterRef.current = false;
    const hasEntered = Taro.getStorageSync(deps.firstEnterKey) === 'true';
    if (!hasEntered) {
      deps.getLocationInfo(true);
      deps.getLocationInfo({ updateCenter: true });
      Taro.setStorageSync(deps.firstEnterKey, 'true');
      return;
    }
    deps.getLocationInfo(shouldUpdateCenter);
    if (!hasStartSite && auth === true) {
    deps.getLocationInfo({ updateCenter: shouldUpdateCenter });
    if (!hasStartSite && auth === true && !shouldUseNearestSiteByOrder) {
      await deps.toOrigin();
    }
  };
@@ -284,7 +333,7 @@
        Promise.resolve(authSyncTask)
          .then((syncResult) => {
            const startSite = syncResult.preferredStartSite || Taro.getStorageSync('startSite');
            processLocationSetting(auth, startSite);
            processLocationSetting(auth, startSite, syncResult.shouldUseNearestSiteByOrder);
          })
          .catch(() => {
            processLocationSetting(auth, null);
src/pages/robotaxi/home/hooks/use-location-flow.ts
@@ -20,6 +20,15 @@
  onLocationSettled: (lat: number, lng: number, isInside: boolean) => void;
};
type GetLocationInfoOptions = {
  /** 是否更新地图中心。 */
  updateCenter?: boolean;
  /** 是否强制刷新城市信息。 */
  forceCityRefresh?: boolean;
  /** 是否强制按用户位置吸附最近站点。 */
  forceNearestSite?: boolean;
};
/**
 * 定位流程编排:不持有状态,仅组合 map/site/city 状态 hook 完成定位主链路。
 */
@@ -109,7 +118,12 @@
  /**
   * 定位入口:定位 -> 城市就绪 -> 地图数据刷新 -> 吸附判断 -> 地址兜底。
   */
  const getLocationInfo = (updateCenter = true, forceCityRefresh = false) => {
  const getLocationInfo = (options: GetLocationInfoOptions = {}) => {
    const {
      updateCenter = true,
      forceCityRefresh = false,
      forceNearestSite = false
    } = options;
    Taro.getLocation({ type: 'gcj02' })
      .then(async (res) => {
        mapState.setIsLocationAuthDenied(false);
@@ -142,7 +156,7 @@
          );
          if (cityInfo?.code) {
            const { isInside, newMarkers, sites } = await mapState.updateMapData(res.latitude, res.longitude, cityInfo.code);
            const lockedManualStartSite = resolveLockedManualStartSite(cityInfo.code);
            const lockedManualStartSite = forceNearestSite ? null : resolveLockedManualStartSite(cityInfo.code);
            const nearestCandidates = sites.length > 0 ? sites : newMarkers;
            const { adsorbed, marker } = lockedManualStartSite
              ? { adsorbed: false, marker: null }
src/pages/robotaxi/home/hooks/use-navigation.ts
@@ -39,7 +39,14 @@
  /** 设置地图缩放 */
  setMapScale: (v: number) => void;
  /** 主动定位 */
  getLocationInfo: (updateCenter?: boolean, forceCityRefresh?: boolean) => void;
  getLocationInfo: (options?: {
    /** 是否更新地图中心。 */
    updateCenter?: boolean;
    /** 是否强制刷新城市信息。 */
    forceCityRefresh?: boolean;
    /** 是否强制按用户位置吸附最近站点。 */
    forceNearestSite?: boolean;
  }) => void;
  /** 查询指针吸附 */
  queryPointerByCenter: (lat: number, lng: number) => Promise<void>;
};
@@ -95,7 +102,7 @@
        if (res.authSetting['scope.userLocation']) {
          deps.setIsLocationAuthDenied(false);
          deps.setMapScale(14);
          deps.getLocationInfo(true, true);
          deps.getLocationInfo({ updateCenter: true, forceCityRefresh: true });
        }
      }
    });
src/robotaxi/pages/begin-route/hooks/passed-route-cache.ts
New file
@@ -0,0 +1,65 @@
import Taro from '@tarojs/taro';
/** 已走过路线缓存键。 */
export const PASSED_ROUTE_CACHE_KEY = 'beginRoutePassedRouteCache';
/** 已走过路线缓存结构。 */
export type PassedRouteCacheState = {
  /** 订单 ID。 */
  orderId: string;
  /** 已走过路线点。 */
  points: Array<{ latitude: number; longitude: number }>;
  /** 最近更新时间。 */
  updatedAt: number;
};
/**
 * 归一化已走过路线点,仅保留有效经纬度。
 */
const normalizePassedRoutePoints = (points: any[] = []) => {
  return points
    .map((point) => ({
      latitude: Number(point?.latitude),
      longitude: Number(point?.longitude)
    }))
    .filter((point) => Number.isFinite(point.latitude) && Number.isFinite(point.longitude));
};
/**
 * 清理已走过路线缓存。
 */
export const clearPassedRouteCache = () => {
  Taro.removeStorageSync(PASSED_ROUTE_CACHE_KEY);
};
/**
 * 写入已走过路线缓存。
 */
export const writePassedRouteCache = (orderId: string, points: any[]) => {
  const normalizedOrderId = String(orderId || '');
  const normalizedPoints = normalizePassedRoutePoints(points);
  if (!normalizedOrderId || normalizedPoints.length <= 1) {
    return;
  }
  const nextState: PassedRouteCacheState = {
    orderId: normalizedOrderId,
    points: normalizedPoints,
    updatedAt: Date.now()
  };
  Taro.setStorageSync(PASSED_ROUTE_CACHE_KEY, nextState);
};
/**
 * 读取当前订单对应的已走过路线缓存。
 */
export const readPassedRouteCache = (orderId?: string) => {
  const cachedState = Taro.getStorageSync(PASSED_ROUTE_CACHE_KEY) as PassedRouteCacheState | null;
  if (!cachedState || !Array.isArray(cachedState.points)) {
    return [];
  }
  const normalizedOrderId = String(orderId || '');
  if (normalizedOrderId && String(cachedState.orderId || '') !== normalizedOrderId) {
    return [];
  }
  return normalizePassedRoutePoints(cachedState.points);
};
src/robotaxi/pages/begin-route/hooks/runtime-sync.ts
@@ -72,13 +72,13 @@
export function normalizeSocketRouteSnapshot(payload: any): RouteSnapshot {
  const route = safeParse(payload?.content?.route, {});
  const timestamp = Number(payload?.data?.timestamp);
  const speedMps = Number(route?.speed);
  const speedKmh = Number(route?.speed);
  return {
    points: normalizeRoutePoints(payload?.points),
    distance: normalizeDistanceKm(Number(route?.distance)),
    duration: normalizeDurationSeconds(Number(route?.duration)),
    direction: normalizeDirection(payload?.content?.direction),
    speedKmh: Number.isFinite(speedMps) ? speedMps * 3.6 : null,
    speedKmh: Number.isFinite(speedKmh) ? speedKmh : null,
    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
@@ -9,10 +9,10 @@
type Deps = {
  tripStatus: TripStatus;
  routePoints: RoutePoint[];
  speedKmh: number | null;
  mapScale: number;
  setMapScale: (value: number) => void;
  syncActiveTripViewportToVehicle: () => void;
  computeBottomPadding: (points: any[], viewportType: string) => number;
  resolveSafeTopInset: () => number;
  holdDynamicScale: (durationMs?: number) => void;
@@ -24,15 +24,30 @@
const USER_GESTURE_HOLD_MS = 3000; // 用户手势后暂停速度缩放时长
const ORDER_UPDATE_LINE_HOLD_MS = 2500; // ORDER_UPDATE_LINE 全路线入屏后暂停速度缩放时长
const SCALE_RESUME_BUFFER_MS = 80; // 冷却窗口结束后的恢复缓冲时间
const LOW_SPEED_KMH = 0; // 低速缩放区间起点
const HIGH_SPEED_KMH = 60; // 高速缩放区间终点
const MIN_DYNAMIC_SCALE = 14; // 高速时允许的最远视角比例尺
const MAX_DYNAMIC_SCALE = 18; // 低速时允许的最近视角比例尺
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 }
]; // 基于车速的动态缩放档位,覆盖低速精细、中速平衡、高速宏观三类视角
/**
 * 将数值限制在给定区间内。
@@ -44,13 +59,77 @@
};
/**
 * 根据速度命中所属缩放档位。
 */
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) => {
  const safeSpeed = clamp(speedKmh, LOW_SPEED_KMH, HIGH_SPEED_KMH);
  const speedRatio = (safeSpeed - LOW_SPEED_KMH) / (HIGH_SPEED_KMH - LOW_SPEED_KMH || 1);
  const nextScale = MAX_DYNAMIC_SCALE - (MAX_DYNAMIC_SCALE - MIN_DYNAMIC_SCALE) * speedRatio;
  return Math.round(nextScale / SCALE_STEP) * SCALE_STEP;
const resolveScaleBySpeed = (speedKmh: number, previousBandIndex: number | null) => {
  const nextBandIndex = resolveStableScaleBandIndex(speedKmh, previousBandIndex);
  return {
    targetScale: DYNAMIC_SCALE_BANDS[nextBandIndex].scale,
    bandIndex: nextBandIndex
  };
};
/**
@@ -59,6 +138,8 @@
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]);
@@ -96,7 +177,7 @@
    }
    deps.holdDynamicScale(USER_GESTURE_HOLD_MS);
    scheduleResume(USER_GESTURE_HOLD_MS + SCALE_RESUME_BUFFER_MS);
  }, [deps, scheduleResume]);
  }, [deps.holdDynamicScale, scheduleResume]);
  /**
   * ORDER_UPDATE_LINE 渲染完成后,自动执行一次全路线入屏。
@@ -122,11 +203,13 @@
      }, 0);
    });
    scheduleResume(ORDER_UPDATE_LINE_HOLD_MS + SCALE_RESUME_BUFFER_MS);
  }, [deps, isActiveStatus, scheduleResume]);
  }, [deps.computeBottomPadding, deps.holdDynamicScale, deps.resolveSafeTopInset, isActiveStatus, scheduleResume]);
  useEffect(() => {
    if (!isActiveStatus) {
      clearResumeTimer();
      speedSamplesRef.current = [];
      scaleBandIndexRef.current = null;
      return;
    }
    const speedKmh = Number(deps.speedKmh);
@@ -139,12 +222,18 @@
      scheduleResume(holdUntil - now + SCALE_RESUME_BUFFER_MS);
      return;
    }
    const targetScale = resolveScaleBySpeed(speedKmh);
    if (Math.abs(targetScale - deps.mapScale) < MIN_SCALE_CHANGE) {
    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) {
      deps.syncActiveTripViewportToVehicle();
      return;
    }
    deps.setMapScale(targetScale);
  }, [deps, isActiveStatus, resumeVersion, scheduleResume]);
    deps.setMapScale(clamp(nextScale, 14, 18));
    deps.syncActiveTripViewportToVehicle();
  }, [deps.getDynamicScaleHoldUntil, deps.mapScale, deps.setMapScale, deps.speedKmh, deps.syncActiveTripViewportToVehicle, isActiveStatus, resumeVersion, scheduleResume]);
  useEffect(() => {
    return () => {
src/robotaxi/pages/begin-route/hooks/use-map-viewport.ts
@@ -33,6 +33,7 @@
const DROPOFF_MAX_SCALE = 20;
const DEFAULT_DYNAMIC_SCALE_HOLD_MS = 2500; // 一次性视口调整后,暂停速度缩放的默认冷却时长
const ACTIVE_TRIP_VIEWPORT_SYNC_MIN_DELTA = 0.00001; // 车辆中心跟随的最小经纬度变化,约 1 米,避免重复写入中心点状态
export default function useMapViewport(rawDeps?: Partial<Deps>) {
    const deps = { ...defaultDeps, ...(rawDeps || {}) } as Deps;
@@ -290,6 +291,26 @@
            .catch(() => { });
    };
    /**
     * 行程动态缩放生效时,将地图中心跟随到车辆位置,确保车辆留在当前视野内。
     */
    const syncActiveTripViewportToVehicle = useCallback(() => {
        const vehicleLatitude = Number(deps.vehicleLocation?.latitude);
        const vehicleLongitude = Number(deps.vehicleLocation?.longitude);
        if (!Number.isFinite(vehicleLatitude) || !Number.isFinite(vehicleLongitude)) {
            return;
        }
        const currentLatitude = Number(deps.latitude);
        const currentLongitude = Number(deps.longitude);
        const hasLatitudeDelta = !Number.isFinite(currentLatitude) || Math.abs(currentLatitude - vehicleLatitude) >= ACTIVE_TRIP_VIEWPORT_SYNC_MIN_DELTA;
        const hasLongitudeDelta = !Number.isFinite(currentLongitude) || Math.abs(currentLongitude - vehicleLongitude) >= ACTIVE_TRIP_VIEWPORT_SYNC_MIN_DELTA;
        if (!hasLatitudeDelta && !hasLongitudeDelta) {
            return;
        }
        deps.setLatitude(vehicleLatitude);
        deps.setLongitude(vehicleLongitude);
    }, [deps.latitude, deps.longitude, deps.setLatitude, deps.setLongitude, deps.vehicleLocation?.latitude, deps.vehicleLocation?.longitude]);
    useEffect(() => {
        if ([TripStatus.InService, TripStatus.ArrivedDestination].includes(deps.tripStatus)) {
            setShowLocationFlag(false);
@@ -405,6 +426,7 @@
        computeBottomPadding,
        holdDynamicScale,
        getDynamicScaleHoldUntil,
        syncActiveTripViewportToVehicle,
        updateMapViewportPadding,
        toOrigin,
        getLocationInfo
src/robotaxi/pages/begin-route/hooks/use-page-actions.ts
@@ -7,6 +7,7 @@
  MODIFY_DESTINATION_ROUTE_POINTS_KEY,
  MODIFY_DESTINATION_SESSION_KEY
} from '../constants/modify-destination';
import { clearPassedRouteCache } from './passed-route-cache';
type Deps = {
  tripStatus: TripStatus;
@@ -70,6 +71,7 @@
   */
  const toBack = () => {
    clearModifyDestinationStorage();
    clearPassedRouteCache();
    if (deps.tripStatus === TripStatus.PaymentSuccess || deps.tripStatus === TripStatus.PublishRoute) {
      clearOrderTripStorage();
      Taro.removeStorageSync('endSite');
src/robotaxi/pages/begin-route/hooks/use-page-restore.ts
@@ -5,6 +5,7 @@
import { queryVehicleByOrderId } from '../../../api/begin-route';
import { toTripStatus, TripStatus } from '../constants/trip-status';
import { MODIFY_DESTINATION_SESSION_KEY } from '../constants/modify-destination';
import { readPassedRouteCache } from './passed-route-cache';
type Deps = {
  wsHost: string;
@@ -14,6 +15,7 @@
  setTripStatus: (value: TripStatus) => void;
  setStartSite: (value: any) => void;
  setEndSite: (value: any) => void;
  setVehicleMoveAlongPoints: (value: any[]) => void;
  setTrunkStatus: (value: boolean) => void;
  setLatitude: (value: number | null) => void;
  setLongitude: (value: number | null) => void;
@@ -138,6 +140,11 @@
    const cachedTripStatusStorage = Taro.getStorageSync('tripStatus');
    const cachedTripStatus = toTripStatus(cachedTripStatusStorage, deps.getCurrentTripStatus());
    deps.setTripStatus(cachedTripStatus);
    if ([TripStatus.VehicleApproaching, TripStatus.InService].includes(cachedTripStatus)) {
      deps.setVehicleMoveAlongPoints(readPassedRouteCache(String(cachedOrderId || '')));
    } else {
      deps.setVehicleMoveAlongPoints([]);
    }
    syncCachedOrderVehicle(cachedOrderId);
    syncSitesFromActiveOrder(cachedTripStatusStorage, s, e);
    if (cachedOrderId && [TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup, TripStatus.InService, TripStatus.ArrivedDestination, TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(cachedTripStatus)) {
src/robotaxi/pages/begin-route/hooks/use-polyline-render.ts
@@ -365,10 +365,12 @@
    lastPolylineUpdateAtRef.current = 0;
  }, [
    allRoutePoints,
    deps.allowSelectableRoutes,
    deps.isAndroid,
    deps.shouldShowRoute,
    deps.statusRule.showWalking,
    deps.tripStatus,
    deps.vehicleMoveAlongPoints,
    routePoints,
    selectedRouteIndex,
    walkingPoints
src/robotaxi/pages/begin-route/hooks/use-web-socket-events.ts
@@ -7,6 +7,7 @@
import { toTripStatus, TripStatus } from '../constants/trip-status';
import { normalizeSocketRouteSnapshot } from './runtime-sync';
import { MODIFY_DESTINATION_SOCKET_STATUS_KEY } from '../constants/modify-destination';
import { clearPassedRouteCache, writePassedRouteCache } from './passed-route-cache';
type UseWebSocketEventsDeps = {
    tripStatusRef: React.MutableRefObject<TripStatus>;
@@ -76,6 +77,7 @@
    deps.setRoutePoints([]);
    deps.setPolyline([]);
    deps.setVehicleMoveAlongPoints([]);
    clearPassedRouteCache();
    deps.polylineKeyRef.current = '';
    deps.lastPolylineUpdateAtRef.current = 0;
    deps.lastRoutePointRef.current = null;
@@ -239,6 +241,10 @@
        return;
    }
    const routeSnapshot = normalizeSocketRouteSnapshot(payload);
    const currentOrderId = String(deps.orderId || Taro.getStorageSync('orderId') || '');
    if (routeSnapshot.vehicleMoveAlongPoints.length > 1) {
        writePassedRouteCache(currentOrderId, routeSnapshot.vehicleMoveAlongPoints);
    }
    deps.applySmoothRouteSnapshot(routeSnapshot);
    if (routeSnapshot.forceFullRedraw && typeof deps.onOrderUpdateLineRendered === 'function') {
        Taro.nextTick(() => {
src/robotaxi/pages/begin-route/index.tsx
@@ -456,14 +456,15 @@
    toOrigin,
    getLocationInfo,
    computeBottomPadding,
    resolveSafeTopInset
    resolveSafeTopInset,
    syncActiveTripViewportToVehicle
  } = viewportState; // 视口状态解构
  const activeTripScale = useActiveTripScale({
    tripStatus,
    routePoints,
    speedKmh: orderRouteMetrics?.speedKmh,
    mapScale,
    setMapScale: viewportState.setMapScale,
    syncActiveTripViewportToVehicle,
    computeBottomPadding,
    resolveSafeTopInset,
    holdDynamicScale,
@@ -585,6 +586,7 @@
    setTripStatus,
    setStartSite,
    setEndSite,
    setVehicleMoveAlongPoints,
    setTrunkStatus,
    setLatitude,
    setLongitude,
src/robotaxi/pages/personal-center/index.tsx
@@ -69,7 +69,7 @@
  const queryCouponCount = async () => {
    try {
      const res = await getUserouponNum();
      const nextCouponCount = res.data || 0;
      const nextCouponCount = res || 0;
      setCouponCount(nextCouponCount);
    } catch (error) {
      setCouponCount(0);