fix(begin-route): 修复速度与时长显示错误,优化页面渲染
统一后端返回的速度单位转换逻辑,将m/s转为km/h并全局复用专用函数。修复时长显示错误,使用格式化函数正确处理秒级时长,避免将秒误显示为分钟。简化路线polyline渲染逻辑,移除冗余的旧路线叠加判断代码。优化车辆marker刷新策略,添加位置变化阈值减少无意义重绘。修复车辆移动动画的回调逻辑,添加fail处理防止动画卡住导致状态异常。
| | |
| | | </View> |
| | | {fareBreakdown.duration.filter((item: any) => item?.money || item?.duration).map((item: any, index: number) => ( |
| | | <View key={`duration-${index}`} className="particulars-detail-item-content"> |
| | | <View>{getTypeLabel(item?.type)}({formatNumber(item?.duration, 0)}分钟)</View> |
| | | <View>{getTypeLabel(item?.type)}({formatTripDurationFromSeconds(item?.duration)})</View> |
| | | <View>{formatMoney(item?.money)}元</View> |
| | | </View> |
| | | ))} |
| | |
| | | duration: number | null; |
| | | /** 车辆朝向(0-359) */ |
| | | direction: number | null; |
| | | /** 当前速度(km/h) */ |
| | | /** 当前速度(km/h,已由原始 m/s 统一换算) */ |
| | | speedKmh: number | null; |
| | | /** 消息时间戳(用于乱序保护) */ |
| | | timestamp: number | null; |
| | |
| | | } |
| | | |
| | | /** |
| | | * 将后端下发的速度(m/s)统一换算为页面消费的 km/h。 |
| | | */ |
| | | export function normalizeSpeedKmh(rawSpeed: any) { |
| | | const speedMetersPerSecond = Number(rawSpeed); |
| | | if (!Number.isFinite(speedMetersPerSecond)) { |
| | | return null; |
| | | } |
| | | return speedMetersPerSecond * 3.6; |
| | | } |
| | | |
| | | /** |
| | | * 归一化 socket 路线消息,统一路线点、概要和车辆移动点结构。 |
| | | */ |
| | | export function normalizeSocketRouteSnapshot(payload: any): RouteSnapshot { |
| | | const route = safeParse(payload?.content?.route, {}); |
| | | const timestamp = Number(payload?.data?.timestamp); |
| | | const speedKmh = Number(route?.speed); |
| | | const speedKmh = normalizeSpeedKmh(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(speedKmh) ? speedKmh : null, |
| | | speedKmh, |
| | | timestamp: Number.isFinite(timestamp) ? timestamp : null, |
| | | vehicleMoveAlongPoints: normalizeRoutePoints(payload?.servedPoints), |
| | | forceFullRedraw: payload?.forceFullRedraw === true, |
| | |
| | | import { TripStatus } from '../constants/trip-status'; |
| | | |
| | | const CHANGE_SITE_UP_ICON_URL = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/change-site-up.png'; // 行程中修改目的地时的上车点图标 |
| | | const DYNAMIC_MARKER_POSITION_EPSILON = 0.000001; // 动态 marker 位置变化阈值,避免无意义重复刷新 |
| | | |
| | | type Deps = { |
| | | tripStatus: TripStatus; |
| | |
| | | return prevMarkers; |
| | | } |
| | | const prevCar = prevMarkers[targetCarIndex] || {}; |
| | | if (markersSignatureRef.current === nextMarkersSignature) { |
| | | const nextCar = nextMarkers[nextCarIndex] || {}; |
| | | const shouldRefreshCarPosition = ( |
| | | Math.abs(Number(prevCar.latitude) - Number(nextCar.latitude)) > DYNAMIC_MARKER_POSITION_EPSILON |
| | | || Math.abs(Number(prevCar.longitude) - Number(nextCar.longitude)) > DYNAMIC_MARKER_POSITION_EPSILON |
| | | || ( |
| | | typeof nextCar.rotate === 'number' |
| | | && Number(nextCar.rotate) !== Number(prevCar.rotate) |
| | | ) |
| | | ); |
| | | if (markersSignatureRef.current === nextMarkersSignature && !shouldRefreshCarPosition) { |
| | | return prevMarkers; |
| | | } |
| | | markersSignatureRef.current = nextMarkersSignature; |
| | | const mergedMarkers = [...nextMarkers]; |
| | | mergedMarkers[nextCarIndex] = { |
| | | ...mergedMarkers[nextCarIndex], |
| | | latitude: prevCar.latitude, |
| | | longitude: prevCar.longitude, |
| | | // 平移态仅保留当前位置,朝向始终以最新车辆状态为准,避免旧 marker 角度覆盖最新 socket 方向。 |
| | | latitude: shouldRefreshCarPosition ? mergedMarkers[nextCarIndex].latitude : prevCar.latitude, |
| | | longitude: shouldRefreshCarPosition ? mergedMarkers[nextCarIndex].longitude : prevCar.longitude, |
| | | // 平移态优先使用最新车辆朝向,动画丢帧时仍由 React 坐标兜底刷新当前位置。 |
| | | rotate: typeof mergedMarkers[nextCarIndex]?.rotate === 'number' |
| | | ? mergedMarkers[nextCarIndex].rotate |
| | | : prevCar.rotate |
| | |
| | | import { ensureSelectedCityByLocation } from '../../../utils/city'; |
| | | import { normalizeDistanceKm, normalizeDurationSeconds } from '../../../utils/format'; |
| | | import { TripStatus } from '../constants/trip-status'; |
| | | import type { RouteSnapshot } from './runtime-sync'; |
| | | import { normalizeSpeedKmh, type RouteSnapshot } from './runtime-sync'; |
| | | import { |
| | | Deps, |
| | | RequestRouteAndFareOptions, |
| | |
| | | distance: normalizeDistanceKm(Number(response?.distance)), |
| | | duration: normalizeDurationSeconds(Number(response?.duration)), |
| | | direction, |
| | | speedKmh: Number.isFinite(Number(response?.speed)) ? Number(response?.speed) : null, |
| | | speedKmh: normalizeSpeedKmh(response?.speed), |
| | | timestamp: Date.now(), |
| | | vehicleMoveAlongPoints: [], |
| | | forceFullRedraw: true, |
| | |
| | | deps.setOrderRouteMetrics?.({ |
| | | distanceMeters: Number.isFinite(Number(response?.distance)) ? Number(response?.distance) : null, |
| | | durationSeconds: Number.isFinite(Number(response?.duration)) ? Number(response?.duration) : null, |
| | | speedKmh: Number.isFinite(Number(response?.speed)) ? Number(response?.speed) : null |
| | | speedKmh: normalizeSpeedKmh(response?.speed) |
| | | }); |
| | | |
| | | const normalizedDirection = resolveDirectionValue(response); |
| | |
| | | const task = pendingTranslateRef.current; |
| | | pendingTranslateRef.current = null; |
| | | isTranslatingRef.current = true; |
| | | const mapCtx = Taro.createMapContext('map'); |
| | | mapCtx.translateMarker({ |
| | | markerId: task.markerId, |
| | | destination: { |
| | | latitude: task.latitude, |
| | | longitude: task.longitude, |
| | | }, |
| | | autoRotate: false, |
| | | moveWithRotate: true, |
| | | rotate: task.rotate, |
| | | duration: TRANSLATE_DURATION_MS, |
| | | complete: () => { |
| | | isTranslatingRef.current = false; |
| | | if (pendingTranslateRef.current) { |
| | | flushTranslateTask(); |
| | | } |
| | | let finished = false; |
| | | const finishTranslate = () => { |
| | | if (finished) return; |
| | | finished = true; |
| | | isTranslatingRef.current = false; |
| | | if (pendingTranslateRef.current) { |
| | | flushTranslateTask(); |
| | | } |
| | | }; |
| | | Taro.nextTick(() => { |
| | | const mapCtx = Taro.createMapContext('map'); |
| | | mapCtx.translateMarker({ |
| | | markerId: task.markerId, |
| | | destination: { |
| | | latitude: task.latitude, |
| | | longitude: task.longitude, |
| | | }, |
| | | autoRotate: false, |
| | | moveWithRotate: true, |
| | | rotate: task.rotate, |
| | | duration: TRANSLATE_DURATION_MS, |
| | | fail: () => { |
| | | finishTranslate(); |
| | | }, |
| | | complete: () => { |
| | | finishTranslate(); |
| | | } |
| | | }); |
| | | }); |
| | | }; |
| | | |
| | |
| | | import { clearOrderTripStorage, safeParse } from '../../../../utils/Robotaxi-public-method'; |
| | | import { normalizeDistanceKm, normalizeDurationSeconds } from '../../../../robotaxi/utils/format'; |
| | | import { toTripStatus, TripStatus } from '../constants/trip-status'; |
| | | import { normalizeSocketRouteSnapshot } from './runtime-sync'; |
| | | import { normalizeSocketRouteSnapshot, normalizeSpeedKmh } from './runtime-sync'; |
| | | import { MODIFY_DESTINATION_SOCKET_STATUS_KEY } from '../constants/modify-destination'; |
| | | import { clearPassedRouteCache, writePassedRouteCache } from './passed-route-cache'; |
| | | import { resolveVehiclePoint } from './trip-motion-utils'; |
| | |
| | | const route = safeParse(payload?.content?.route, {}); |
| | | const distanceMeters = Number(route?.distance); |
| | | const durationSecondsRaw = Number(route?.duration); |
| | | const speedKmh = Number(route?.speed); |
| | | const speedKmh = normalizeSpeedKmh(route?.speed); |
| | | const distanceKm = normalizeDistanceKm(distanceMeters); |
| | | if (distanceKm !== null) { |
| | | deps.setRouteDistance(distanceKm); |
| | |
| | | deps.setOrderRouteMetrics?.({ |
| | | distanceMeters: Number.isFinite(distanceMeters) ? distanceMeters : null, |
| | | durationSeconds: Number.isFinite(durationSecondsRaw) ? durationSecondsRaw : null, |
| | | speedKmh: Number.isFinite(speedKmh) ? speedKmh : null |
| | | speedKmh |
| | | }); |
| | | }; |
| | | |
| | |
| | | requestOrderRoute: routeRequestOrderRoute, |
| | | updateWalkingRouteForStart: routeUpdateWalkingRouteForStart |
| | | } = routeState; // 路线状态解构 |
| | | const shouldShowModifyDestinationPreviousRoute = tripStatus === TripStatus.InService; // 仅行程中修改目的地时展示旧目的地浅色对比路线 |
| | | const displayPolyline = useMemo(() => { |
| | | if ( |
| | | !shouldShowModifyDestinationPreviousRoute |
| | | || !(isModifyDestinationReviewVisible || hasLocalDestinationOverride) |
| | | || !Array.isArray(previousRoutePoints) |
| | | || previousRoutePoints.length === 0 |
| | | ) { |
| | | return polyline; |
| | | } |
| | | return [ |
| | | { |
| | | id: 900001, |
| | | points: previousRoutePoints, |
| | | color: '#D7DDEF', |
| | | width: 8, |
| | | dottedLine: false, |
| | | arrowLine: true, |
| | | clickable: false |
| | | }, |
| | | ...polyline |
| | | ]; |
| | | }, [polyline, previousRoutePoints, isModifyDestinationReviewVisible, hasLocalDestinationOverride, shouldShowModifyDestinationPreviousRoute]); |
| | | const displayPolyline = polyline; |
| | | const routeSummaryMarker = useMemo(() => { |
| | | if (!allowSelectableRoutes || !routeDistance || routeDuration === null || !Array.isArray(routePoints) || routePoints.length === 0) { |
| | | return null; |