fix(robotaxi): 优化行程状态7的地图显示和车辆位置更新
- 在行程状态7(已到达)时,地图视口以下车点为中心并限制缩放等级
- 避免车辆位置在状态7时频繁更新,减少不必要的重绘
- 修复状态7时路线请求和车辆标记的显示逻辑
- 移除WebSocket和HTTP请求中的冗余日志上传代码
- 防止关键行程状态(4/6)被延迟的WebSocket消息回退
| | |
| | | .join(';'); |
| | | const hasDropoffMarker = markersRef.current.some((m: any) => m.id === 2); |
| | | const shouldHoldMarkerByTranslate = [4, 6].includes(deps.tripStatus) && carMarkerInitialized.current && deps.statusRule.showCar && (!deps.statusRule.showDropoff || hasDropoffMarker); |
| | | const shouldSkipUpdate = shouldHoldMarkerByTranslate && (deps.isAndroid || tripStatus6MarkerRefreshedRef.current); |
| | | const shouldHoldMarkerWhenArrived = deps.tripStatus === 7 && carMarkerInitialized.current && deps.statusRule.showCar && (!deps.statusRule.showDropoff || hasDropoffMarker); |
| | | const shouldSkipUpdate = (shouldHoldMarkerByTranslate && (deps.isAndroid || tripStatus6MarkerRefreshedRef.current)) || shouldHoldMarkerWhenArrived; |
| | | applyMarkerState(nextMarkers, nextMarkersSignature, shouldSkipUpdate, carMarker); |
| | | }, [deps.tripStatus, deps.startSite?.name, deps.startSite?.latitude, deps.startSite?.longitude, deps.endSite?.name, deps.endSite?.latitude, deps.endSite?.longitude, deps.vehicleLocation, deps.vehicleDirection, deps.vehicleMarkerId, deps.isAndroid, deps.statusRule.showPickup, deps.statusRule.showCar, deps.statusRule.showDropoff]); |
| | | |
| | |
| | | setLatitude: () => { } |
| | | }; |
| | | |
| | | const DROPOFF_MAX_SCALE = 20; |
| | | |
| | | export default function useMapViewport(rawDeps?: Partial<Deps>) { |
| | | const deps = { ...defaultDeps, ...(rawDeps || {}) } as Deps; |
| | | // 地图底部留白 |
| | |
| | | ? [topPadding, 50, bottomPadding, 50] |
| | | : [topPadding, 100, bottomPadding, 100]; |
| | | Taro.nextTick(() => { |
| | | mapCtx.includePoints({ points, padding }); |
| | | if (viewportType === 'dropoff') { |
| | | mapCtx.moveToLocation({ |
| | | latitude: points[0].latitude, |
| | | longitude: points[0].longitude |
| | | }); |
| | | setTimeout(() => { |
| | | setMapScale(13); |
| | | setMapScale(DROPOFF_MAX_SCALE); |
| | | }, 50); |
| | | return; |
| | | } else { |
| | | mapCtx.includePoints({ points, padding }); |
| | | } |
| | | }); |
| | | }; |
| | |
| | | const getLocationInfo = () => { |
| | | Taro.getLocation({ type: 'gcj02' }) |
| | | .then((res) => { |
| | | if (deps.tripStatus === 7) { |
| | | return; |
| | | } |
| | | deps.setLongitude(res.longitude); |
| | | deps.setLatitude(res.latitude); |
| | | if (deps.tripStatus === 6 && deps.routePoints.length > 0) { |
| | |
| | | }, [deps.tripStatus]); |
| | | |
| | | useEffect(() => { |
| | | const viewportType = deps.statusRule.viewport; |
| | | if (viewportType !== 'dropoff') return; |
| | | const { pickupSite, dropoffSite } = resolveViewportSites(); |
| | | const { points, keyParts } = buildViewportPayload(viewportType, pickupSite, dropoffSite); |
| | | if (points.length === 0) return; |
| | | const nextKey = keyParts.join('|'); |
| | | if (nextKey === viewportKeyRef.current) return; |
| | | viewportKeyRef.current = nextKey; |
| | | updateViewportByPayload(viewportType, points); |
| | | }, [deps.tripStatus, deps.statusRule?.viewport, deps.endSite?.latitude, deps.endSite?.longitude]); |
| | | |
| | | useEffect(() => { |
| | | const { pickupSite, dropoffSite } = resolveViewportSites(); |
| | | const viewportType = deps.statusRule.viewport; |
| | | if (viewportType === 'dropoff') return; |
| | | const { points, keyParts } = buildViewportPayload(viewportType, pickupSite, dropoffSite); |
| | | if (points.length === 0) return; |
| | | if (deps.tripStatus === 6 && viewportType === 'route' && tripStatus6ViewportLockedRef.current) return; |
| | |
| | | * 同步地图定位并按状态初始化路线。 |
| | | */ |
| | | const syncLocationAndInitRoute = (s: any, e: any, cachedTripStatus: number) => { |
| | | if (s && s.latitude && s.longitude) { |
| | | // 状态 7 直接以下车点为中心,避免 getLocationInfo 抢占视口中心 |
| | | if (cachedTripStatus === 7) { |
| | | const dropoffLat = Number(e?.latitude); |
| | | const dropoffLng = Number(e?.longitude); |
| | | if (Number.isFinite(dropoffLat) && Number.isFinite(dropoffLng)) { |
| | | deps.setLatitude(dropoffLat); |
| | | deps.setLongitude(dropoffLng); |
| | | } |
| | | } else if (s && s.latitude && s.longitude) { |
| | | deps.setLatitude(s.latitude); |
| | | deps.setLongitude(s.longitude); |
| | | } else { |
| | |
| | | // 请求订单实时路线/轨迹 |
| | | const requestOrderRoute = (targetOrderId: string, status: number) => { |
| | | const routeType = status <= 5 ? 1 : 2; |
| | | const requestRoute = [7, 8, 9].includes(status) ? queryOrderTrajectory : queryOrderRoute; |
| | | const requestRoute = [8, 9].includes(status) ? queryOrderTrajectory : queryOrderRoute; |
| | | requestRoute({ orderId: targetOrderId, routeType }) |
| | | .then((res: any) => { |
| | | const rawPoints = Array.isArray(res?.polyline) ? res.polyline : []; |
| | |
| | | deps.updateVehicleLocationThrottled(last.latitude, last.longitude, true); |
| | | } else { |
| | | applyRoutePoints(points); |
| | | const first = points[0]; |
| | | deps.updateVehicleLocationThrottled(first.latitude, first.longitude, true); |
| | | const targetPoint = status === 7 |
| | | ? points[points.length - 1] |
| | | : points[0]; |
| | | deps.updateVehicleLocationThrottled(targetPoint.latitude, targetPoint.longitude, true); |
| | | if ([4, 6].includes(status)) { |
| | | currentFullRouteRef.current = [...points]; |
| | | lastErasedIndexRef.current = 0; |
| | | orderRouteInitializedRef.current = true; |
| | | } |
| | | } |
| | | } else if (status === 7) { |
| | | const cachedEndSite = Taro.getStorageSync('endSite') || {}; |
| | | const fallbackLat = Number(cachedEndSite?.latitude); |
| | | const fallbackLng = Number(cachedEndSite?.longitude); |
| | | if (Number.isFinite(fallbackLat) && Number.isFinite(fallbackLng)) { |
| | | deps.updateVehicleLocationThrottled(fallbackLat, fallbackLng, true); |
| | | } |
| | | } |
| | | const distanceKm = normalizeDistanceKm(Number(res?.distance)); |
| | | if (distanceKm !== null) { |
| | |
| | | isAndroid: boolean; |
| | | /** 行程状态 */ |
| | | tripStatus: number; |
| | | /** 地图缩放等级 */ |
| | | mapScale: number; |
| | | /** 地图 markers */ |
| | | markers: any[]; |
| | | /** 是否显示等待雷达 */ |
| | |
| | | </CoverView> |
| | | </CoverView> |
| | | )} |
| | | {(item.id === 2 && [7].includes(deps.tripStatus) && !deps.isAndroid) && ( |
| | | {(item.id === 2 && [7].includes(deps.tripStatus) && !deps.isAndroid && deps.mapScale >= 20) && ( |
| | | <CoverView className='custom-callout-content'> |
| | | 已到达 |
| | | </CoverView> |
| | |
| | | return null; |
| | | } |
| | | const currentTripStatus = Number(deps.tripStatusRef.current); |
| | | // 行程进行关键态不允许被回退,避免 4/6 被延迟状态覆盖导致路线链路重置 |
| | | if ([4, 6].includes(currentTripStatus) && mappedTripStatus < currentTripStatus) { |
| | | return currentTripStatus; |
| | | } |
| | | if (currentTripStatus === mappedTripStatus) { |
| | | return mappedTripStatus; |
| | | } |
| | |
| | | if (!targetPoint) { |
| | | return; |
| | | } |
| | | const currentTripStatus = Number(deps.tripStatusRef.current); |
| | | if (currentTripStatus === 7) { |
| | | const lastPoint = deps.lastVehiclePointRef.current; |
| | | const lastUpdateAt = Number(deps.lastVehicleUpdateAtRef.current || 0); |
| | | const now = Date.now(); |
| | | const latDiff = Math.abs(Number(lastPoint?.latitude || 0) - targetPoint.latitude); |
| | | const lngDiff = Math.abs(Number(lastPoint?.longitude || 0) - targetPoint.longitude); |
| | | const almostSamePoint = latDiff < 0.000003 && lngDiff < 0.000003; |
| | | if (almostSamePoint || (now - lastUpdateAt < 1200)) { |
| | | return; |
| | | } |
| | | } |
| | | deps.updateVehicleLocationThrottled(targetPoint.latitude, targetPoint.longitude, true); |
| | | }; |
| | | |
| | |
| | | Taro.nextTick(() => { |
| | | const updated = deps.applyRoutePoints(points); |
| | | if (!updated) return; |
| | | if (![4, 5, 6].includes(deps.tripStatusRef.current)) return; |
| | | if (![4, 5, 6, 7].includes(deps.tripStatusRef.current)) return; |
| | | syncVehiclePointWithRoute(points, deps, servedPoints); |
| | | }); |
| | | }; |
| | |
| | | const points = payload?.points || []; |
| | | const servedPoints = Array.isArray(payload?.servedPoints) ? payload.servedPoints : []; |
| | | const forceFullRedraw = payload?.forceFullRedraw === true; |
| | | const currentTripStatus = Number(deps.tripStatusRef.current); |
| | | updateRouteSummary(payload, deps); |
| | | if ([4, 6].includes(deps.tripStatusRef.current)) { |
| | | if ([4, 6].includes(currentTripStatus)) { |
| | | deps.setVehicleMoveAlongPoints(servedPoints); |
| | | } |
| | | if (forceFullRedraw) { |
| | | handleForceFullRouteRedraw(points, deps, servedPoints); |
| | | return; |
| | | } |
| | | const isEraseStatus = [4, 6].includes(deps.tripStatusRef.current); |
| | | const isEraseStatus = ![7, 8, 9].includes(currentTripStatus); |
| | | if (isEraseStatus) { |
| | | handleEraseRouteStatus(points, servedPoints, deps); |
| | | return; |
| | |
| | | orderRouteHandlerRef.current = (payload) => { |
| | | handleOrderRoutePayload(payload, depsRef.current); |
| | | }; |
| | | // 处理重进页面时 useDidShow 先于 handler 初始化的场景,确保已连接状态下也能完成监听绑定 |
| | | if (wsManager.isConnectedStatus()) { |
| | | bindSocketListeners(); |
| | | } |
| | | |
| | | return () => { |
| | | if (authSuccessBound.current && authSuccessHandlerRef.current) { |
| | |
| | | const { renderMapOverlays, renderTripCard } = useViewDelegates({ |
| | | isAndroid, |
| | | tripStatus, |
| | | mapScale, |
| | | markers, |
| | | shouldShowWaitingRadar, |
| | | waitingRadarVisible, |
| | |
| | | getStorage, |
| | | objectSort, |
| | | } from '../utils/tools'; |
| | | import { _ROBOTAXI_HOST, _XP_SECRET, _UID } from '../constants/api'; |
| | | import { _XP_SECRET, _UID } from '../constants/api'; |
| | | import { jumpTo } from './jump'; |
| | | |
| | | const CODE_SUCCESS = 200 |
| | | const MINI_LOG_UPLOAD_PATH = '/operation/base/v1/mini-program/miniLog/uploadLog'; |
| | | const CODE_AUTH_EXPIRED_ARRAY = [12010301, 12010302, 12010303, 12010304, 12010305, 12010306, 12010320, 12010002]; |
| | | // 12010301, "登录状态已过期,请重新登录", "otp缓存已过期"), |
| | | // 12010302, "登录状态已过期,请重新登录", "otp已过期"), |
| | |
| | | header |
| | | }) |
| | | .then(async (res) => { |
| | | if (!isMiniLogUploadUrl(url)) { |
| | | uploadMiniLog( |
| | | JSON.stringify({ |
| | | type: 'http_response', |
| | | url, |
| | | request: payload, |
| | | response: res?.data, |
| | | timestamp: Date.now() |
| | | }) |
| | | ); |
| | | } |
| | | // res为包含cookies、header的经过小程序封装的数据 |
| | | // 第一层data为接口返回数据,包含接口状态码 |
| | | // 第二层data一般为业务数据 |
| | |
| | | return data |
| | | }) |
| | | .catch((err) => { |
| | | if (!isMiniLogUploadUrl(url)) { |
| | | uploadMiniLog( |
| | | JSON.stringify({ |
| | | type: 'http_error', |
| | | url, |
| | | request: payload, |
| | | response: err, |
| | | timestamp: Date.now() |
| | | }) |
| | | ); |
| | | } |
| | | console.log('request err:', url); |
| | | console.log(err); |
| | | |
| | |
| | | return Promise.reject(errData) |
| | | }); |
| | | } |
| | | |
| | | const isMiniLogUploadUrl = (url: string = '') => { |
| | | return typeof url === 'string' && url.includes(MINI_LOG_UPLOAD_PATH); |
| | | }; |
| | | |
| | | export const uploadMiniLog = async (logContent: string) => { |
| | | if (!logContent) return; |
| | | try { |
| | | const header = await buildHeader({ method: 'POST', payload: logContent }, false); |
| | | if (!header) return; |
| | | const uploadUrl = `${_ROBOTAXI_HOST}${MINI_LOG_UPLOAD_PATH}`; |
| | | Taro.request({ |
| | | url: uploadUrl, |
| | | method: 'POST', |
| | | data: logContent, |
| | | header: { |
| | | ...header, |
| | | 'Content-Type': 'application/json' |
| | | } |
| | | }).catch(() => { }); |
| | | } catch (_) { } |
| | | }; |
| | | |
| | | async function buildHeader(options, needLogin) { |
| | | const { method = 'POST', payload, extraHeader = {} } = options; |
| | |
| | | import { getStorage } from './tools'; |
| | | import { _UID } from '../constants/api'; |
| | | import { safeParse } from './Robotaxi-public-method'; |
| | | import { uploadMiniLog } from './request'; |
| | | |
| | | type MessageCallback = (data: any) => void; |
| | | |
| | |
| | | try { |
| | | data = safeParse(res.data as string, {}); |
| | | console.log('收到 WebSocket 消息:', res.data); |
| | | if (!this.isHeartbeatMessage(data)) { |
| | | const messageContent = typeof res.data === 'string' ? res.data : JSON.stringify(res.data); |
| | | uploadMiniLog(messageContent); |
| | | } |
| | | } catch (e) { |
| | | console.warn('解析 WebSocket data 失败:', res.data); |
| | | this.notifyListeners('message', res.data); |
| | |
| | | try { |
| | | data = safeParse(res.data as string, {}); |
| | | console.log('收到 WebSocket 消息:', res.data); |
| | | if (!this.isHeartbeatMessage(data)) { |
| | | const messageContent = typeof res.data === 'string' ? res.data : JSON.stringify(res.data); |
| | | uploadMiniLog(messageContent); |
| | | } |
| | | } catch (e) { |
| | | console.warn('解析 WebSocket data 失败:', res.data); |
| | | this.notifyListeners('message', res.data); |