hejianhao
2026-05-21 0b5f55fbbdbd12964acec9a63bd582403036910a
fix(begin-route): 修复速度与时长显示错误,优化页面渲染

统一后端返回的速度单位转换逻辑,将m/s转为km/h并全局复用专用函数。修复时长显示错误,使用格式化函数正确处理秒级时长,避免将秒误显示为分钟。简化路线polyline渲染逻辑,移除冗余的旧路线叠加判断代码。优化车辆marker刷新策略,添加位置变化阈值减少无意义重绘。修复车辆移动动画的回调逻辑,添加fail处理防止动画卡住导致状态异常。
7个文件已修改
116 ■■■■■ 已修改文件
src/robotaxi/pages/begin-route/components/calling-car-modules/fare-detail-card.tsx 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/runtime-sync.ts 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-map-markers.ts 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-route-request.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-vehicle-tracking.ts 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-web-socket-events.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/index.tsx 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car-modules/fare-detail-card.tsx
@@ -134,7 +134,7 @@
                            </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>
                            ))}
src/robotaxi/pages/begin-route/hooks/runtime-sync.ts
@@ -26,7 +26,7 @@
  duration: number | null;
  /** 车辆朝向(0-359) */
  direction: number | null;
  /** 当前速度(km/h) */
  /** 当前速度(km/h,已由原始 m/s 统一换算) */
  speedKmh: number | null;
  /** 消息时间戳(用于乱序保护) */
  timestamp: number | null;
@@ -67,18 +67,29 @@
}
/**
 * 将后端下发的速度(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,
src/robotaxi/pages/begin-route/hooks/use-map-markers.ts
@@ -3,6 +3,7 @@
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;
@@ -179,16 +180,25 @@
          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
src/robotaxi/pages/begin-route/hooks/use-route-request.ts
@@ -5,7 +5,7 @@
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,
@@ -121,7 +121,7 @@
  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,
@@ -551,7 +551,7 @@
        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);
src/robotaxi/pages/begin-route/hooks/use-vehicle-tracking.ts
@@ -38,23 +38,34 @@
    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();
        }
      });
    });
  };
src/robotaxi/pages/begin-route/hooks/use-web-socket-events.ts
@@ -5,7 +5,7 @@
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';
@@ -224,7 +224,7 @@
    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);
@@ -236,7 +236,7 @@
    deps.setOrderRouteMetrics?.({
        distanceMeters: Number.isFinite(distanceMeters) ? distanceMeters : null,
        durationSeconds: Number.isFinite(durationSecondsRaw) ? durationSecondsRaw : null,
        speedKmh: Number.isFinite(speedKmh) ? speedKmh : null
        speedKmh
    });
};
src/robotaxi/pages/begin-route/index.tsx
@@ -382,29 +382,7 @@
    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;