| src/robotaxi/pages/begin-route/hooks/use-smooth-route-motion.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/robotaxi/pages/begin-route/hooks/use-speed-based-scale.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/robotaxi/pages/begin-route/hooks/web-socket-handlers/create-order-route-socket-handler.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/robotaxi/pages/begin-route/index.tsx | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/robotaxi/pages/site-search/index.tsx | ●●●●● 补丁 | 查看 | 原始文档 | 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) => {