hejianhao
2026-06-30 40dedb1fb9c05fcf05839223eafeb06d98fcb3f0
Merge branch 'feature_v2.0' into hc/630_ui_ue
5个文件已修改
69 ■■■■■ 已修改文件
src/robotaxi/pages/begin-route/hooks/use-smooth-route-motion.ts 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-speed-based-scale.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/web-socket-handlers/create-order-route-socket-handler.ts 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/index.tsx 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/site-search/index.tsx 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-smooth-route-motion.ts
@@ -36,6 +36,8 @@
  setVehicleMoveAlongPoints: (points: RoutePoint[]) => void;
  /** 节流更新车辆位置 */
  updateVehicleLocationThrottled: (lat: number, lng: number, force?: boolean) => void;
  /** 读取用户手势是否进行中(拖拽/缩放手势 begin→end 之间为 true)。 */
  getUserGestureActive?: () => boolean;
};
type MotionRuntimeState = {
@@ -85,7 +87,7 @@
    lastSocketTimestamp: 0,
    routeReady: false,
  });
  const renderTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
  const renderTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const lastRenderAtRef = useRef(0);
  const prevTripStatusRef = useRef(deps.tripStatus);
  const nativeRouteEraseReadyRef = useRef(false);
@@ -134,7 +136,7 @@
   */
  const stopRenderLoop = () => {
    if (renderTimerRef.current) {
      clearInterval(renderTimerRef.current);
      clearTimeout(renderTimerRef.current);
      renderTimerRef.current = null;
    }
    lastRenderAtRef.current = 0;
@@ -281,15 +283,28 @@
  /**
   * 启动本地补帧循环,在 socket 两帧之间平滑推动车辆和擦线进度。
   * 用递归 setTimeout 自调度而非 setInterval:同一时刻只保留一个待执行定时器,
   * 渲染层繁忙时不会像 setInterval 那样在桥接层堆积回调。下一帧在帧首调度,早退分支也不会中断循环。
   */
  const startRenderLoop = () => {
    if (renderTimerRef.current) {
      return;
    }
    renderTimerRef.current = setInterval(() => {
    const tick = () => {
      // 帧首先排下一帧(早退分支不影响循环延续),由 stopRenderLoop 的 clearTimeout 终止。
      renderTimerRef.current = setTimeout(tick, TripMotionConfig.RENDER_INTERVAL_MS);
      const runtime = runtimeRef.current;
      const routeIndex = runtime.routeIndex;
      if (!runtime.routeReady || !routeIndex) {
        return;
      }
      // 用户手势(拖拽/缩放)进行中暂停补帧:本帧会通过 updateVehicleLocationThrottled setState 触发
      // 页面重渲染,进而把受控 <Map> 的相机重新写回原生地图,与用户手势争抢导致「拖动被弹回」。
      // 整帧跳过(车标/擦线进度一并冻结,保持一致),并刷新 lastRenderAt 避免手势结束后步长突跳;
      // 手势期间 socket 仍可推进 targetDistanceMeters,手势结束后由后续帧平滑追帧。
      if (deps.getUserGestureActive?.()) {
        lastRenderAtRef.current = Date.now();
        return;
      }
      const now = Date.now();
@@ -328,7 +343,8 @@
      if (shouldEraseRoute) {
        eraseRouteToDistance(routeIndex, prevDisplayDistance);
      }
    }, TripMotionConfig.RENDER_INTERVAL_MS);
    };
    renderTimerRef.current = setTimeout(tick, TripMotionConfig.RENDER_INTERVAL_MS);
  };
  /**
src/robotaxi/pages/begin-route/hooks/use-speed-based-scale.ts
@@ -31,7 +31,7 @@
const TOP_PADDING_MIN = 32;
const TOP_EXTRA_PADDING = 50;
const SHORT_ROUTE_FALLBACK_POINT_THRESHOLD = 3;
const USER_MAP_INTERACTION_HOLD_MS = 3000;
const USER_MAP_INTERACTION_HOLD_MS = 8000;
/**
 * 两次 includePoints 之间的最小间隔(毫秒)。
 *
src/robotaxi/pages/begin-route/hooks/web-socket-handlers/create-order-route-socket-handler.ts
@@ -113,6 +113,10 @@
    return;
  }
  deps.updateVehicleLocationThrottled(targetPoint.latitude, targetPoint.longitude, true);
  // 修改目的地确认页期间仍需同步车辆朝向,否则 rebuild marker 后 translateMarker 的 rotate 为 0
  if (typeof routeSnapshot.direction === 'number') {
    deps.setVehicleDirection(routeSnapshot.direction);
  }
};
/**
src/robotaxi/pages/begin-route/index.tsx
@@ -255,6 +255,9 @@
  const markViewportFitSyncing = useCallback(() => {
    globalFitSyncRef.current = true;
  }, []);
  // 用户手势进行中标记:从 regionchange 'begin'(用户拖拽/缩放手势) 到 'end' 之间为 true。
  // 供平滑运动在手势期间暂停车辆位置回灌,避免高频 setState 触发重渲染把受控相机弹回。
  const userGestureActiveRef = useRef(false);
  // 经纬度 setter:更新 state 触发渲染的同时同步记录最新值,保证 ref 始终为最新(兼容函数式更新)
  const setLatitude = useCallback((value: number | null | ((prev: number | null) => number | null)) => {
    setLatitudeState((prev) => {
@@ -1315,7 +1318,8 @@
    setRouteDuration,
    setVehicleDirection,
    setVehicleMoveAlongPoints,
    updateVehicleLocationThrottled: vehicleState.updateVehicleLocationThrottled
    updateVehicleLocationThrottled: vehicleState.updateVehicleLocationThrottled,
    getUserGestureActive: () => userGestureActiveRef.current
  });
  useEffect(() => {
@@ -1743,15 +1747,31 @@
    // 用户手势导致的缩放/拖拽结束后,把原生地图的实际 scale 同步回 React 状态,
    // 避免受控 <Map scale={mapScale}> 与原生实际缩放漂移,影响后续视口编排判断。
    const eventType = String(e?.type || e?.detail?.type || '').toLowerCase();
    if (eventType === 'end' && (causedBy === 'scale' || causedBy === 'drag' || causedBy === 'gesture')) {
        notifyUserMapGesture();
    // 维护用户手势进行中标记:手势 begin 置位、end 复位,供平滑运动在手势期间暂停车辆位置回灌。
    const isUserGesture = causedBy === 'scale' || causedBy === 'drag' || causedBy === 'gesture';
    if (eventType === 'begin') {
      if (isUserGesture) {
        userGestureActiveRef.current = true;
      }
    } else if (eventType === 'end' && isUserGesture) {
      // 仅用户手势(gesture/drag/scale)的 end 才复位;手势期间穿插的程序化 update 事件
      // (causedBy='update') 的 end 不应清除标记,否则会在用户仍在操作时提前恢复补帧导致弹回。
      userGestureActiveRef.current = false;
    }
    if(eventType === 'begin' && isUserGesture) {
      notifyUserMapGesture();
    }
    if (eventType === 'end' && isUserGesture) {
        const actualScale = Number(e?.detail?.scale);
        if (!Number.isFinite(actualScale)) {
          return;
        }
        const rounded = Math.round(actualScale);
        if (rounded !== mapScale) {
        if (rounded !== mapScaleRef.current) {
          recordMapScale(rounded);
        }
@@ -1775,7 +1795,7 @@
      const fittedScale = Number(detail?.scale);
      if (Number.isFinite(fittedScale)) {
        const rounded = Math.floor(fittedScale);
        if (rounded !== mapScale) {
        if (rounded !== mapScaleRef.current) {
          recordMapScale(rounded);
        }
      }
src/robotaxi/pages/site-search/index.tsx
@@ -67,6 +67,9 @@
function SiteSearchPage() {
  const [searchValue, setSearchValue] = useState('') //搜索值
  // 输入框重置 key:仅在清空/编程式重置时自增以重挂载输入框,
  // 输入框采用非受控模式(不回填 value),避免 iOS 下 re-render 回填打断输入法导致丢字/输入不进去
  const [inputResetKey, setInputResetKey] = useState(0)
  const [showAllHistory, setShowAllHistory] = useState(false) // 是否展示全部历史记录
  const [showAllRecommend, setShowAllRecommend] = useState(false) // 是否展示全部推荐站点
  const [distanceConfig, setDistanceConfig] = useState<any>({})
@@ -413,6 +416,8 @@
  const clearSearchValue = () => {
    setSearchValue('');
    clearSearchResults();
    // 非受控输入框:通过自增 key 重挂载来清空原生输入框内容
    setInputResetKey((k) => k + 1);
  }
  const handleCloseTipPopup = () => {
    setShowTipPopup(false);
@@ -532,7 +537,9 @@
      <View className='search-card'>
        <View className={`search-icon ${pageType == 'end' ? 'end-icon' : ''}`}></View>
        <View className='search-input-box'>
          <Input className='search-input' placeholder={pageType == 'start' ? '请输入上车点' : '请输入目的地'} value={searchValue}
          <Input className='search-input' placeholder={pageType == 'start' ? '请输入上车点' : '请输入目的地'}
            key={inputResetKey}
            defaultValue={searchValue}
            onInput={(e) => changeSearchValue(e)}
            onBlur={flushDebouncedSearch}
            onConfirm={(e) => {