fix(share-trip): 修复共享行程页多项路由与显示异常
- 新增路由标识校验,切换路线时自动清空旧运行时状态避免轨迹残影
- 调整下车点展示逻辑,仅在到达接客点后渲染下车标记
- 优化车辆标记动画状态管理,修复标记切换时的动画异常
- 修复行程完成后仍显示路线摘要的问题
- 优化地图视口同步逻辑,修复安卓端视口偏移异常
- 重构路线方向计算与快照生成逻辑,添加调试日志
| | |
| | | duration: number | null; |
| | | direction: number | null; |
| | | timestamp: number | null; |
| | | routeKey: string; |
| | | vehicleMoveAlongPoints: RoutePoint[]; |
| | | forceFullRedraw: boolean; |
| | | eraseVehiclePointMode: VehiclePointMode; |
| | |
| | | duration: any; |
| | | direction: any; |
| | | timestamp?: any; |
| | | routeKey?: any; |
| | | forceFullRedraw?: boolean; |
| | | }): RouteSnapshot { |
| | | const timestamp = Number(payload?.timestamp); |
| | |
| | | duration: normalizeDurationSeconds(Number(payload?.duration)), |
| | | direction: normalizeDirection(payload?.direction), |
| | | timestamp: Number.isFinite(timestamp) ? timestamp : Date.now(), |
| | | routeKey: String(payload?.routeKey || ''), |
| | | vehicleMoveAlongPoints: normalizeRoutePoints(payload?.servedPoints), |
| | | forceFullRedraw: payload?.forceFullRedraw === true, |
| | | eraseVehiclePointMode: 'first_then_served_last', |
| | |
| | | const markersSignatureRef = useRef<string>(""); |
| | | const carMarkerInitialized = useRef(false); |
| | | const tripStatus6MarkerRefreshedRef = useRef(false); |
| | | const readyNotifiedMarkerIdRef = useRef<number | null>(null); |
| | | const shouldUsePureTranslateCarMarker = |
| | | [TripStatus.VehicleApproaching, TripStatus.InService].includes(deps.tripStatus) |
| | | && deps.statusRule.showCar; |
| | |
| | | useAndroidNativeTrip1Callout: boolean |
| | | ) => { |
| | | if (!(dropoffSite?.latitude && dropoffSite?.longitude)) return null; |
| | | // 分享行程只有从 5/6 及之后的状态才渲染下车点,避免 4 态提前入列引发地图多次缩放。 |
| | | if (deps.tripStatus < TripStatus.VehicleArrivedPickup) return null; |
| | | const dropoffName = |
| | | [TripStatus.ArrivedDestination].includes(deps.tripStatus) ? '已到达' : dropoffSite.name || '目的地'; |
| | | return { |
| | |
| | | markersSignatureRef.current = nextMarkersSignature; |
| | | const mergedMarkers = [...nextMarkers]; |
| | | const nextCar = mergedMarkers[nextCarIndex] || {}; |
| | | const shouldRefreshCarMarkerStructure = |
| | | buildCarMarkerStructureSignature(prevCar) !== buildCarMarkerStructureSignature(nextCar); |
| | | const latestVehicleLatitude = Number(deps.vehicleLocation?.latitude); |
| | | const latestVehicleLongitude = Number(deps.vehicleLocation?.longitude); |
| | | if (!shouldRefreshCarMarkerStructure && !shouldClearCarRotate) { |
| | | return prevMarkers; |
| | | } |
| | | mergedMarkers[nextCarIndex] = { |
| | | ...nextCar, |
| | | latitude: Number.isFinite(latestVehicleLatitude) |
| | |
| | | useEffect(() => { |
| | | const hasVehicleMarker = deps.statusRule.showCar && markers.some((item: any) => item.id === deps.vehicleMarkerId); |
| | | if (!hasVehicleMarker) { |
| | | readyNotifiedMarkerIdRef.current = null; |
| | | return; |
| | | } |
| | | if (readyNotifiedMarkerIdRef.current === deps.vehicleMarkerId) { |
| | | return; |
| | | } |
| | | readyNotifiedMarkerIdRef.current = deps.vehicleMarkerId; |
| | | Taro.nextTick(() => { |
| | | deps.onVehicleMarkerReady(); |
| | | }); |
| | |
| | | ].join("|"); |
| | | }; |
| | | |
| | | const buildOrderDetailRouteSignature = (routePoints: any[], pickupSite: any, dropoffSite: any) => { |
| | | const firstPoint = routePoints[0]; |
| | | const lastPoint = routePoints[routePoints.length - 1]; |
| | | return [ |
| | | routePoints.length, |
| | | firstPoint?.latitude ?? "", |
| | | firstPoint?.longitude ?? "", |
| | | lastPoint?.latitude ?? "", |
| | | lastPoint?.longitude ?? "", |
| | | pickupSite?.latitude ?? "", |
| | | pickupSite?.longitude ?? "", |
| | | dropoffSite?.latitude ?? "", |
| | | dropoffSite?.longitude ?? "", |
| | | ].join("|"); |
| | | }; |
| | | |
| | | export default function useMapViewport(rawDeps?: Partial<Deps>) { |
| | | const deps = { ...defaultDeps, ...(rawDeps || {}) } as Deps; |
| | | const [mapBottomPadding, setMapBottomPadding] = useState(280); |
| | |
| | | const mapRectRef = useRef<any>(null); |
| | | const viewportKeyRef = useRef<string>(""); |
| | | const orderDetailSyncTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null); |
| | | const completedOrderDetailViewportKeyRef = useRef<string>(""); |
| | | const dynamicScaleHoldUntilRef = useRef(0); |
| | | const lastActiveTripViewportSyncAtRef = useRef(0); |
| | | const activeTripViewportSyncingRef = useRef(false); |
| | |
| | | if (pickupPoint) points.push(pickupPoint); |
| | | if (dropoffPoint) points.push(dropoffPoint); |
| | | keyParts.push( |
| | | buildPointsSignature(deps.routePoints), |
| | | buildOrderDetailRouteSignature(deps.routePoints, pickupSite, dropoffSite), |
| | | pickupSite?.latitude, |
| | | pickupSite?.longitude, |
| | | dropoffSite?.latitude, |
| | | dropoffSite?.longitude, |
| | | Math.round(mapBottomPadding) |
| | | dropoffSite?.longitude |
| | | ); |
| | | } |
| | | return { points, keyParts }; |
| | | }; |
| | | |
| | | const syncOrderDetailViewportState = (mapCtx: ReturnType<typeof Taro.createMapContext>) => { |
| | | mapCtx.getCenterLocation({ |
| | | success: (res) => { |
| | | if (typeof deps.setLatitude === "function" && Number.isFinite(res?.latitude)) { |
| | | deps.setLatitude(res.latitude); |
| | | } |
| | | if (typeof deps.setLongitude === "function" && Number.isFinite(res?.longitude)) { |
| | | deps.setLongitude(res.longitude); |
| | | } |
| | | } |
| | | }); |
| | | mapCtx.getScale({ |
| | | success: (res) => { |
| | | if (Number.isFinite(res?.scale)) { |
| | | setMapScale(res.scale); |
| | | } |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | const updateViewportByPayload = useCallback((viewportType: string, points: any[]) => { |
| | |
| | | clearTimeout(orderDetailSyncTimerRef.current); |
| | | } |
| | | orderDetailSyncTimerRef.current = setTimeout(() => { |
| | | syncOrderDetailViewportState(mapCtx); |
| | | // 分享页完整路线视口不再把地图中心反写回受控经纬度。 |
| | | // 安卓真机在 includePoints 后读取 getCenterLocation 偶发返回偏移中心, |
| | | // 再写回 latitude/longitude 会把地图锁到一块没有点线的位置。 |
| | | mapCtx.getScale({ |
| | | success: (res) => { |
| | | if (Number.isFinite(res?.scale)) { |
| | | setMapScale(res.scale); |
| | | } |
| | | } |
| | | }); |
| | | orderDetailSyncTimerRef.current = null; |
| | | }, 360); |
| | | } |
| | |
| | | ]); |
| | | |
| | | useEffect(() => { |
| | | viewportKeyRef.current = ""; |
| | | completedOrderDetailViewportKeyRef.current = ""; |
| | | }, [ |
| | | deps.tripStatus, |
| | | ]); |
| | | |
| | | useEffect(() => { |
| | | const viewportType = deps.statusRule.viewport; |
| | | const { pickupSite, dropoffSite } = resolveViewportSites(); |
| | | const { points, keyParts } = buildViewportPayload(viewportType, pickupSite, dropoffSite); |
| | | if (points.length === 0) { |
| | | return; |
| | | } |
| | | if (deps.tripStatus >= TripStatus.ArrivedDestination && viewportType === "order_detail") { |
| | | return; |
| | | } |
| | | const nextKey = keyParts.join("|"); |
| | |
| | | ]); |
| | | |
| | | useEffect(() => { |
| | | if (deps.tripStatus < TripStatus.ArrivedDestination) { |
| | | return; |
| | | } |
| | | if (deps.statusRule.viewport !== "order_detail") { |
| | | return; |
| | | } |
| | | const { pickupSite, dropoffSite } = resolveViewportSites(); |
| | | const points: ViewportPoint[] = []; |
| | | const pickupPoint = buildSiteViewportPoint(pickupSite); |
| | | const dropoffPoint = buildSiteViewportPoint(dropoffSite); |
| | | if (deps.routePoints.length > 0) { |
| | | points.push(...deps.routePoints); |
| | | } |
| | | if (pickupPoint) { |
| | | points.push(pickupPoint); |
| | | } |
| | | if (dropoffPoint) { |
| | | points.push(dropoffPoint); |
| | | } |
| | | if (points.length === 0) { |
| | | return; |
| | | } |
| | | const nextKey = buildOrderDetailRouteSignature(deps.routePoints, pickupSite, dropoffSite); |
| | | if (nextKey === completedOrderDetailViewportKeyRef.current) { |
| | | return; |
| | | } |
| | | completedOrderDetailViewportKeyRef.current = nextKey; |
| | | if (orderDetailSyncTimerRef.current) { |
| | | clearTimeout(orderDetailSyncTimerRef.current); |
| | | orderDetailSyncTimerRef.current = null; |
| | | } |
| | | const mapCtx = Taro.createMapContext("map"); |
| | | const bottomPadding = computeBottomPadding(points, "order_detail"); |
| | | const topPadding = Math.max(60, resolveSafeTopInset() + 32) + 24; |
| | | const orderDetailBottomPadding = Math.max(200, bottomPadding - 56); |
| | | Taro.nextTick(() => { |
| | | mapCtx.includePoints({ |
| | | points, |
| | | padding: [topPadding, 50, orderDetailBottomPadding, 50] |
| | | }); |
| | | }); |
| | | }, [ |
| | | deps.tripStatus, |
| | | deps.statusRule.viewport, |
| | | deps.routePoints, |
| | | deps.startSite?.latitude, |
| | | deps.startSite?.longitude, |
| | | deps.endSite?.latitude, |
| | | deps.endSite?.longitude, |
| | | mapBottomPadding, |
| | | computeBottomPadding |
| | | ]); |
| | | |
| | | useEffect(() => { |
| | | return () => { |
| | | if (orderDetailSyncTimerRef.current) { |
| | | clearTimeout(orderDetailSyncTimerRef.current); |
| | |
| | | polylineKeyRef: renderState.polylineKeyRef, |
| | | lastPolylineUpdateAtRef: renderState.lastPolylineUpdateAtRef, |
| | | applyRoutePoints: renderState.applyRoutePoints, |
| | | resetRouteRuntimeState: renderState.resetRouteRuntimeState, |
| | | erasePassedRouteSegments: renderState.erasePassedRouteSegments, |
| | | }; |
| | | } |
| | |
| | | }; |
| | | |
| | | /** |
| | | * 计算两点方向角(0-360)。 |
| | | */ |
| | | const getDirectionByPoints = (from: LocationPoint, to?: LocationPoint) => { |
| | | if (!from || !to) return 0; |
| | | const deltaLng = to.longitude - from.longitude; |
| | | const deltaLat = to.latitude - from.latitude; |
| | | const angle = Math.atan2(deltaLng, deltaLat) * (180 / Math.PI); |
| | | return ((angle % 360) + 360) % 360; |
| | | }; |
| | | |
| | | /** |
| | | * 朝向值归一化到 0-360,非法值返回 null。 |
| | | */ |
| | | const normalizeDirection = (value: any) => { |
| | |
| | | ?? getFieldValue(routeResponse?.content, ['duration']) |
| | | ?? getFieldValue(routeResponse?.data, ['duration']) |
| | | ?? getFieldValue(routeResponse, ['duration']); |
| | | const direction = normalizeDirection( |
| | | routeResponse?.direction ?? |
| | | routeResponse?.content?.direction ?? |
| | | routeMeta?.direction ?? |
| | | routeResponse?.vehicleDirection, |
| | | const apiDirection = normalizeDirection(routeResponse?.direction); |
| | | const contentDirection = normalizeDirection(routeResponse?.content?.direction); |
| | | const resolvedDirection = typeof apiDirection === 'number' |
| | | ? apiDirection |
| | | : typeof contentDirection === 'number' |
| | | ? contentDirection |
| | | : null; |
| | | const routeKey = String( |
| | | getFieldValue(routeMeta, ['routeID', 'routeId']) |
| | | ?? getFieldValue(routeResponse?.content, ['routeID', 'routeId']) |
| | | ?? getFieldValue(routeResponse?.data, ['routeID', 'routeId']) |
| | | ?? getFieldValue(routeResponse, ['routeID', 'routeId']) |
| | | ?? '', |
| | | ); |
| | | const fallbackDirection = typeof direction === 'number' |
| | | ? direction |
| | | : getDirectionByPoints(extractedRoutePoints[0], extractedRoutePoints[1]); |
| | | console.log('[share-trip] route direction resolved', { |
| | | routeKey, |
| | | apiDirection, |
| | | contentDirection, |
| | | resolvedDirection, |
| | | servedPointsLength: servedPoints.length, |
| | | routePointsLength: extractedRoutePoints.length, |
| | | }); |
| | | const routeSnapshot = buildShareRouteSnapshot({ |
| | | points: extractedRoutePoints, |
| | | servedPoints, |
| | | distance: distanceValue, |
| | | duration: durationValue, |
| | | direction: fallbackDirection, |
| | | direction: resolvedDirection, |
| | | routeKey, |
| | | timestamp: routeResponse?.timestamp |
| | | ?? routeResponse?.content?.timestamp |
| | | ?? routeResponse?.data?.timestamp |
| | |
| | | import { normalizeDistanceKm } from '../../../utils/format'; |
| | | import { |
| | | buildRouteIndex, |
| | | getDistanceMeters, |
| | | getPointByDistance, |
| | | getRouteCursorByDistance, |
| | | projectPointToRoute, |
| | |
| | | |
| | | type MotionRuntimeState = { |
| | | routeIndex: RouteIndex | null; |
| | | routeKey: string; |
| | | baseRemainingDistanceMeters: number; |
| | | displayDistanceMeters: number; |
| | | targetDistanceMeters: number; |
| | |
| | | ERASE_STEP_METERS = 6, |
| | | PROJECT_CLOSE_THRESHOLD_METERS = 10, |
| | | PROJECT_SOFT_THRESHOLD_METERS = 25, |
| | | REBASE_ROUTE_DEVIATION_METERS = 30, |
| | | } |
| | | |
| | | const ROUTE_POLYLINE_ID = 2001; |
| | |
| | | export default function useSmoothRouteMotion(deps: Deps) { |
| | | const runtimeRef = useRef<MotionRuntimeState>({ |
| | | routeIndex: null, |
| | | routeKey: '', |
| | | baseRemainingDistanceMeters: 0, |
| | | displayDistanceMeters: 0, |
| | | targetDistanceMeters: 0, |
| | |
| | | pendingRouteEraseDistanceRef.current = null; |
| | | runtimeRef.current = { |
| | | routeIndex: null, |
| | | routeKey: '', |
| | | baseRemainingDistanceMeters: 0, |
| | | displayDistanceMeters: 0, |
| | | targetDistanceMeters: 0, |
| | |
| | | return Number(snapshot.timestamp) <= runtimeRef.current.lastSocketTimestamp; |
| | | }; |
| | | |
| | | const initializeBaselineRoute = (snapshot: RouteSnapshot) => { |
| | | const initializeBaselineRoute = (snapshot: RouteSnapshot, options?: { clearPassedRoute?: boolean }) => { |
| | | const shouldClearPassedRoute = options?.clearPassedRoute === true; |
| | | const points = sanitizeRoutePoints(snapshot.points); |
| | | const routeIndex = buildRouteIndex(points); |
| | | const remainingDistanceMeters = toMeters(snapshot.distance) ?? routeIndex.totalDistance; |
| | | resetRuntimeState(); |
| | | runtimeRef.current = { |
| | | routeIndex, |
| | | routeKey: String(snapshot.routeKey || ''), |
| | | baseRemainingDistanceMeters: remainingDistanceMeters, |
| | | displayDistanceMeters: 0, |
| | | targetDistanceMeters: 0, |
| | |
| | | syncRemainingDistance(remainingDistanceMeters); |
| | | deps.resetRoutePolylineCache(); |
| | | deps.applyRoutePoints(points); |
| | | if (snapshot.vehicleMoveAlongPoints.length > 0) { |
| | | deps.setVehicleMoveAlongPoints(snapshot.vehicleMoveAlongPoints); |
| | | } |
| | | // 换线重建时先清空旧的已行驶轨迹,避免浅色轨迹沿着旧路线残留一小段。 |
| | | deps.setVehicleMoveAlongPoints(shouldClearPassedRoute ? [] : snapshot.vehicleMoveAlongPoints); |
| | | syncStaticVehiclePoint(snapshot, true); |
| | | }; |
| | | |
| | |
| | | const runtime = runtimeRef.current; |
| | | const firstPoint = snapshot.points?.[0] || null; |
| | | const remainingDistanceMeters = toMeters(snapshot.distance); |
| | | const nextRouteKey = String(snapshot.routeKey || ''); |
| | | if (!runtime.routeReady || !runtime.routeIndex) { |
| | | initializeBaselineRoute(snapshot); |
| | | startRenderLoop(); |
| | | return; |
| | | } |
| | | if (nextRouteKey && runtime.routeKey && nextRouteKey !== runtime.routeKey) { |
| | | console.log('[share-trip] route baseline rebuilt by routeKey change', { |
| | | prevRouteKey: runtime.routeKey, |
| | | nextRouteKey, |
| | | }); |
| | | initializeBaselineRoute(snapshot, { clearPassedRoute: true }); |
| | | startRenderLoop(); |
| | | return; |
| | | } |
| | |
| | | } else { |
| | | nextTargetDistance = Math.max(runtime.targetDistanceMeters, projectedDistance); |
| | | } |
| | | const projectedPoint = getPointByDistance(runtime.routeIndex, projectedDistance); |
| | | const routeDeviationMeters = getDistanceMeters(firstPoint, projectedPoint); |
| | | if (routeDeviationMeters > TripMotionConfig.REBASE_ROUTE_DEVIATION_METERS) { |
| | | console.log('[share-trip] route baseline rebuilt by geometry deviation', { |
| | | routeDeviationMeters, |
| | | projectedDistance, |
| | | nextTargetDistance, |
| | | nextRouteKey, |
| | | }); |
| | | initializeBaselineRoute(snapshot, { clearPassedRoute: true }); |
| | | startRenderLoop(); |
| | | return; |
| | | } |
| | | } |
| | | runtime.targetDistanceMeters = Math.max(runtime.targetDistanceMeters, nextTargetDistance); |
| | | runtime.lastSocketTimestamp = Number.isFinite(snapshot.timestamp) |
| | | ? Number(snapshot.timestamp) |
| | | : runtime.lastSocketTimestamp; |
| | | if (snapshot.vehicleMoveAlongPoints.length > 0) { |
| | | deps.setVehicleMoveAlongPoints(snapshot.vehicleMoveAlongPoints); |
| | | } |
| | | deps.setVehicleMoveAlongPoints(snapshot.vehicleMoveAlongPoints); |
| | | startRenderLoop(); |
| | | }; |
| | | |
| | | const applyStaticRouteSnapshot = (snapshot: RouteSnapshot) => { |
| | | stopRenderLoop(); |
| | | deps.applyRoutePoints(sanitizeRoutePoints(snapshot.points)); |
| | | if (snapshot.vehicleMoveAlongPoints.length > 0) { |
| | | deps.setVehicleMoveAlongPoints(snapshot.vehicleMoveAlongPoints); |
| | | } |
| | | deps.setVehicleMoveAlongPoints(snapshot.vehicleMoveAlongPoints); |
| | | syncStaticVehiclePoint(snapshot, false); |
| | | }; |
| | | |
| | | const applyRouteSnapshot = (snapshot: RouteSnapshot) => { |
| | | const currentTripStatus = Number(deps.tripStatusRef.current); |
| | | const shouldUseSmoothMotion = isSmoothMotionTripStatus(currentTripStatus); |
| | | syncRouteSummary(snapshot, !shouldUseSmoothMotion); |
| | | if (isStaleSnapshot(snapshot)) { |
| | | return; |
| | | } |
| | | syncRouteSummary(snapshot, !shouldUseSmoothMotion); |
| | | if (snapshot.forceFullRedraw) { |
| | | initializeBaselineRoute(snapshot); |
| | | if (!shouldUseSmoothMotion) { |
| | |
| | | } | null>(null); |
| | | const vehicleMarkerReadyRef = useRef(false); |
| | | const vehicleMarkerReadyTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null); |
| | | const vehicleMarkerIdRef = useRef<number>(vehicleMarkerId); |
| | | const translateSessionIdRef = useRef(0); |
| | | |
| | | const clearVehicleMarkerReadyTimer = () => { |
| | | if (vehicleMarkerReadyTimerRef.current) { |
| | |
| | | } |
| | | }; |
| | | |
| | | // 重置当前车辆 marker 对应的平移上下文,避免沿用旧 marker 的角度与 ready 状态。 |
| | | const resetTranslateRuntimeState = () => { |
| | | isTranslatingRef.current = false; |
| | | pendingTranslateRef.current = null; |
| | | lastAppliedTranslateTaskRef.current = null; |
| | | vehicleMarkerReadyRef.current = false; |
| | | clearVehicleMarkerReadyTimer(); |
| | | translateSessionIdRef.current += 1; |
| | | }; |
| | | |
| | | const flushTranslateTask = useCallback(() => { |
| | | if (!vehicleMarkerReadyRef.current) return; |
| | | if (isTranslatingRef.current) return; |
| | | if (!pendingTranslateRef.current) return; |
| | | const task = pendingTranslateRef.current; |
| | | const translateSessionId = translateSessionIdRef.current; |
| | | pendingTranslateRef.current = null; |
| | | isTranslatingRef.current = true; |
| | | let finished = false; |
| | | let fallbackTimer: ReturnType<typeof setTimeout> | null = null; |
| | | const finishTranslate = () => { |
| | | if (finished) return; |
| | | if (translateSessionId !== translateSessionIdRef.current) return; |
| | | finished = true; |
| | | if (fallbackTimer) { |
| | | clearTimeout(fallbackTimer); |
| | |
| | | rotate: task.rotate, |
| | | duration: task.duration, |
| | | animationEnd: () => { |
| | | if (translateSessionId !== translateSessionIdRef.current) { |
| | | return; |
| | | } |
| | | lastAppliedTranslateTaskRef.current = { |
| | | latitude: task.latitude, |
| | | longitude: task.longitude, |
| | |
| | | finishTranslate(); |
| | | }, |
| | | fail: () => { |
| | | lastAppliedTranslateTaskRef.current = { |
| | | latitude: task.latitude, |
| | | longitude: task.longitude, |
| | | rotate: task.rotate |
| | | }; |
| | | finishTranslate(); |
| | | if (translateSessionId !== translateSessionIdRef.current) { |
| | | return; |
| | | } |
| | | // marker 尚未正确挂载时不要把本次旋转记成已完成,等待 marker ready 后重试。 |
| | | pendingTranslateRef.current = task; |
| | | vehicleMarkerReadyRef.current = false; |
| | | isTranslatingRef.current = false; |
| | | if (fallbackTimer) { |
| | | clearTimeout(fallbackTimer); |
| | | } |
| | | } |
| | | }); |
| | | }); |
| | |
| | | }, [statusRule.translateCar, vehicleLocation, vehicleDirection, vehicleMarkerId, flushTranslateTask]); |
| | | |
| | | useEffect(() => { |
| | | if (!statusRule.translateCar) { |
| | | vehicleMarkerIdRef.current = vehicleMarkerId; |
| | | return; |
| | | } |
| | | if (vehicleMarkerIdRef.current === vehicleMarkerId) { |
| | | return; |
| | | } |
| | | vehicleMarkerIdRef.current = vehicleMarkerId; |
| | | resetTranslateRuntimeState(); |
| | | }, [statusRule.translateCar, vehicleMarkerId]); |
| | | |
| | | useEffect(() => { |
| | | if (statusRule.translateCar) return; |
| | | isTranslatingRef.current = false; |
| | | pendingTranslateRef.current = null; |
| | | lastAppliedTranslateTaskRef.current = null; |
| | | vehicleMarkerReadyRef.current = false; |
| | | clearVehicleMarkerReadyTimer(); |
| | | resetTranslateRuntimeState(); |
| | | }, [statusRule.translateCar]); |
| | | |
| | | useEffect(() => () => { |
| | |
| | | [TripStatus.Publishing]: { showPickup: true, showDropoff: false, showCar: false, showRoute: false, showWalking: false, viewport: 'pickup', translateCar: false }, |
| | | [TripStatus.CallingCar]: { showPickup: true, showDropoff: false, showCar: false, showRoute: false, showWalking: false, viewport: 'pickup', translateCar: false }, |
| | | [TripStatus.VehicleApproaching]: { showPickup: true, showDropoff: false, showCar: true, showRoute: true, showWalking: false, viewport: 'route', translateCar: true }, |
| | | [TripStatus.VehicleArrivedPickup]: { showPickup: true, showDropoff: false, showCar: true, showRoute: false, showWalking: false, viewport: 'car_user', translateCar: true }, |
| | | [TripStatus.VehicleArrivedPickup]: { showPickup: true, showDropoff: true, showCar: true, showRoute: false, showWalking: false, viewport: 'car_user', translateCar: true }, |
| | | [TripStatus.InService]: { showPickup: false, showDropoff: true, showCar: true, showRoute: true, showWalking: false, viewport: 'route', translateCar: true }, |
| | | [TripStatus.ArrivedDestination]: { showPickup: false, showDropoff: true, showCar: false, showRoute: true, showWalking: false, viewport: 'order_detail', translateCar: false }, |
| | | [TripStatus.TripFinished]: { showPickup: true, showDropoff: true, showCar: false, showRoute: true, showWalking: false, viewport: 'order_detail', translateCar: false }, |
| | |
| | | handleMapTap, |
| | | polylineKeyRef, |
| | | lastPolylineUpdateAtRef, |
| | | resetRouteRuntimeState, |
| | | } = useRouteManager({ |
| | | isAndroid, |
| | | orderId, |
| | |
| | | }, [displayRouteDuration, routeDistance]); |
| | | |
| | | const resetRoutePolylineCache = () => { |
| | | // 分享页切换新路线基线时,先把旧路线/擦线/浅色已行驶轨迹整套运行时清空, |
| | | // 避免地图短暂保留旧路线残影,再按新快照重绘。 |
| | | resetRouteRuntimeState(); |
| | | polylineKeyRef.current = ''; |
| | | lastPolylineUpdateAtRef.current = 0; |
| | | }; |
| | |
| | | setLatitude, |
| | | }); // 视口状态容器 |
| | | const routeSummaryMarker = useMemo(() => { |
| | | const shouldShowRouteSummary = !statusRule.showCar && shouldShowRoute && tripStatus !== TripStatus.ArrivedDestination; |
| | | // 到达目的地及后续完单态不再展示路线摘要气泡,避免出现“1分钟0米”的残留提示。 |
| | | const shouldShowRouteSummary = !statusRule.showCar |
| | | && shouldShowRoute |
| | | && tripStatus < TripStatus.ArrivedDestination; |
| | | if (!shouldShowRouteSummary || !routeSummaryText || !Array.isArray(routePoints) || routePoints.length === 0) { |
| | | return null; |
| | | } |