refactor(robotaxi/begin-route): 重构行程视口适配逻辑,移除冗余车速代码,优化蓝牙卡片显示
| | |
| | | duration?: NumericLike; |
| | | /** 方向角。 */ |
| | | direction?: NumericLike; |
| | | /** 行驶速度。 */ |
| | | speed?: NumericLike; |
| | | } |
| | | |
| | | /** 订单轨迹响应 content。 */ |
| | |
| | | distance?: NumericLike; |
| | | /** 路线时长。 */ |
| | | duration?: NumericLike; |
| | | /** 行驶速度。 */ |
| | | speed?: NumericLike; |
| | | /** 方向角。 */ |
| | | direction?: NumericLike; |
| | | /** 行程状态。 */ |
| | |
| | | }; |
| | | |
| | | export default function BluetoothOperateCard({ description, actionText = '', onClick }: Props) { |
| | | const iconSrc = actionText === '去开启' |
| | | ? "https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/close-bluetooth.png" |
| | | : "https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/bluetooth-icon.png"; |
| | | |
| | | return ( |
| | | <View className="bluetooth-operate-card" onClick={onClick}> |
| | | <View className="bluetooth-operate-card-left"> |
| | | <Image src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/bluetooth-icon.png" /> |
| | | <Image src={iconSrc} /> |
| | | <View>{description}</View> |
| | | </View> |
| | | {!!actionText && ( |
| | |
| | | duration: number | null; |
| | | /** 车辆朝向(0-359) */ |
| | | direction: number | null; |
| | | /** 当前速度(m/s)。 */ |
| | | speedMps: number | null; |
| | | /** 消息时间戳(用于乱序保护) */ |
| | | timestamp: number | null; |
| | | /** 已行驶轨迹点(servedPoints) */ |
| | |
| | | eraseVehiclePointMode: VehiclePointMode; |
| | | /** 非擦线态车辆点策略 */ |
| | | normalVehiclePointMode: VehiclePointMode; |
| | | /** 当前车速 (m/s) */ |
| | | speed: number | null; |
| | | }; |
| | | |
| | | /** |
| | |
| | | } |
| | | |
| | | /** |
| | | * 将后端下发的速度统一规整为 m/s。 |
| | | */ |
| | | export function normalizeSpeedMps(rawSpeed: any) { |
| | | const speedMetersPerSecond = Number(rawSpeed); |
| | | if (!Number.isFinite(speedMetersPerSecond)) { |
| | | return null; |
| | | } |
| | | return Math.max(0, speedMetersPerSecond); |
| | | } |
| | | |
| | | /** |
| | | * 归一化 socket 路线消息,统一路线点、概要和车辆移动点结构。 |
| | | */ |
| | | export function normalizeSocketRouteSnapshot(payload: any): RouteSnapshot { |
| | | const route = safeParse(payload?.content?.route, {}); |
| | | const timestamp = Number(payload?.data?.timestamp); |
| | | const speedMps = normalizeSpeedMps(route?.speed); |
| | | return { |
| | | points: normalizeRoutePoints(payload?.points), |
| | | distance: normalizeDistanceKm(Number(route?.distance)), |
| | | duration: normalizeDurationSeconds(Number(route?.duration)), |
| | | direction: normalizeDirection(payload?.content?.direction), |
| | | speedMps, |
| | | timestamp: Number.isFinite(timestamp) ? timestamp : null, |
| | | vehicleMoveAlongPoints: normalizeRoutePoints(payload?.servedPoints), |
| | | forceFullRedraw: payload?.forceFullRedraw === true, |
| | | eraseVehiclePointMode: 'first_then_served_last', |
| | | normalVehiclePointMode: 'served_last_then_first', |
| | | speed: Number.isFinite(Number(route?.speed)) ? Number(route?.speed) : null, |
| | | }; |
| | | } |
| | | |
| | |
| | | } |
| | | return Math.max(searchStartDistance, bestProjectedDistance); |
| | | } |
| | | |
| | | /** |
| | | * 根据最大距离截取路线片段(用于车速分级自适应缩放视口)。 |
| | | * 从起点开始,累加线段距离,直到超过 maxDistanceMeters,截取这部分点返回。 |
| | | */ |
| | | export function sliceRoutePointsByDistance(points: RoutePoint[], maxDistanceMeters: number): RoutePoint[] { |
| | | if (!Array.isArray(points) || points.length <= 1) { |
| | | return points; |
| | | } |
| | | const result: RoutePoint[] = [points[0]]; |
| | | let accumulatedDistance = 0; |
| | | for (let index = 1; index < points.length; index += 1) { |
| | | const prev = points[index - 1]; |
| | | const curr = points[index]; |
| | | const segmentDistance = getDistanceMeters(prev, curr); |
| | | if (accumulatedDistance + segmentDistance > maxDistanceMeters) { |
| | | const ratio = (maxDistanceMeters - accumulatedDistance) / segmentDistance; |
| | | result.push(interpolatePoint(prev, curr, ratio)); |
| | | break; |
| | | } else { |
| | | accumulatedDistance += segmentDistance; |
| | | result.push(curr); |
| | | } |
| | | } |
| | | return result; |
| | | } |
| | |
| | | import Taro from '@tarojs/taro'; |
| | | import { useCallback, useMemo } from 'react'; |
| | | import { useCallback, useMemo, useEffect, useRef } from 'react'; |
| | | import { TripStatus } from '../constants/trip-status'; |
| | | import useVehicleSpeedScale from './use-vehicle-speed-scale'; |
| | | import { sliceRoutePointsByDistance } from './trip-motion-utils'; |
| | | |
| | | type RoutePoint = { |
| | | latitude: number; |
| | |
| | | type Deps = { |
| | | /** 当前行程状态。 */ |
| | | tripStatus: TripStatus; |
| | | /** 当前速度,单位 m/s。 */ |
| | | speedMps: number | null; |
| | | /** 当前地图比例尺。 */ |
| | | mapScale: number; |
| | | /** 设置地图比例尺。 */ |
| | | setMapScale: (value: number) => void; |
| | | /** 动态缩放生效后同步车辆视口中心。 */ |
| | | syncActiveTripViewportToVehicle: () => void; |
| | | /** 车辆速度 (m/s) */ |
| | | vehicleSpeed: number | null; |
| | | /** 当前路线点集 */ |
| | | routePoints: RoutePoint[]; |
| | | /** 获取速度缩放暂停截止时间 */ |
| | | getDynamicScaleHoldUntil: () => number; |
| | | /** 计算 includePoints 所需底部留白。 */ |
| | | computeBottomPadding: (points: any[], viewportType: string) => number; |
| | | /** 读取顶部安全区高度。 */ |
| | | resolveSafeTopInset: () => number; |
| | | /** 暂停动态缩放。 */ |
| | | holdDynamicScale: (durationMs?: number) => void; |
| | | /** 读取动态缩放暂停截止时间。 */ |
| | | getDynamicScaleHoldUntil: () => number; |
| | | }; |
| | | |
| | | const ACTIVE_TRIP_STATUSES = [TripStatus.VehicleApproaching, TripStatus.InService]; // 启用速度缩放的行程状态 |
| | | const USER_GESTURE_CAUSED_BY = new Set(['gesture', 'drag', 'scale']); // 需要暂停速度缩放的用户手势来源 |
| | | const USER_GESTURE_HOLD_MS = 3000; // 用户手势后暂停速度缩放时长 |
| | | const ORDER_UPDATE_LINE_HOLD_MS = 2500; // ORDER_UPDATE_LINE 全路线入屏后暂停速度缩放时长 |
| | | const ACTIVE_TRIP_STATUSES = [TripStatus.VehicleApproaching, TripStatus.InService]; // 启用视口编排的行程状态 |
| | | const USER_GESTURE_CAUSED_BY = new Set(['gesture', 'drag', 'scale']); // 需要暂停动态缩放的用户手势来源 |
| | | const USER_GESTURE_HOLD_MS = 3000; // 用户手势后暂停动态缩放时长 |
| | | const ORDER_UPDATE_LINE_HOLD_MS = 2500; // ORDER_UPDATE_LINE 全路线入屏后暂停动态缩放时长 |
| | | const ORDER_UPDATE_LINE_SIDE_PADDING = 100; // ORDER_UPDATE_LINE 路线入屏左右留白 |
| | | const ORDER_UPDATE_LINE_TOP_PADDING = 24; // ORDER_UPDATE_LINE 路线入屏最小顶部留白 |
| | | const ORDER_UPDATE_LINE_TOP_EXTRA_PADDING = 32; // ORDER_UPDATE_LINE 顶部安全区附加留白 |
| | | const DYNAMIC_SCALE_INTERVAL_MS = 8000; // 动态缩放巡航周期 |
| | | |
| | | /** |
| | | * 接驾中/行程中动态缩放入口:速度缩放单独交给专用 Hook,这里仅保留视口编排职责。 |
| | | * 接驾中/行程中视口编排入口:仅保留视口编排职责。 |
| | | */ |
| | | export default function useActiveTripScale(deps: Deps) { |
| | | const isActiveStatus = useMemo(() => { |
| | | return ACTIVE_TRIP_STATUSES.includes(deps.tripStatus); |
| | | }, [deps.tripStatus]); |
| | | |
| | | useVehicleSpeedScale({ |
| | | tripStatus: deps.tripStatus, |
| | | speedMps: deps.speedMps, |
| | | mapScale: deps.mapScale, |
| | | setMapScale: deps.setMapScale, |
| | | syncActiveTripViewportToVehicle: deps.syncActiveTripViewportToVehicle, |
| | | getDynamicScaleHoldUntil: deps.getDynamicScaleHoldUntil |
| | | }); |
| | | const depsRef = useRef(deps); |
| | | useEffect(() => { |
| | | depsRef.current = deps; |
| | | }, [deps]); |
| | | |
| | | /** |
| | | * 处理地图手势,手动缩放/拖拽后短暂暂停速度接管。 |
| | | * 基于车速区间进行路线截取,推导视口缩放。 |
| | | */ |
| | | const tryApplySpeedBasedScale = useCallback(() => { |
| | | const currentDeps = depsRef.current; |
| | | if (currentDeps.tripStatus !== TripStatus.InService) { |
| | | return; // 仅在行程中开启速度级缩放 |
| | | } |
| | | const now = Date.now(); |
| | | if (currentDeps.getDynamicScaleHoldUntil() > now) { |
| | | return; // 处于手势或强行全览冷却期 |
| | | } |
| | | const speed = currentDeps.vehicleSpeed || 0; |
| | | const points = currentDeps.routePoints; |
| | | if (!Array.isArray(points) || points.length < 2) { |
| | | return; |
| | | } |
| | | |
| | | let targetDistance = 5000; // 高速段 5公里 |
| | | if (speed < 8.3) { // 约 30km/h |
| | | targetDistance = 300; // 低速段 300米 |
| | | } else if (speed < 16.6) { // 约 60km/h |
| | | targetDistance = 1500; // 中速段 1.5公里 |
| | | } |
| | | |
| | | const slicedPoints = sliceRoutePointsByDistance(points, targetDistance); |
| | | if (slicedPoints.length < 2) { |
| | | return; |
| | | } |
| | | |
| | | const mapCtx = Taro.createMapContext('map'); |
| | | const bottomPadding = currentDeps.computeBottomPadding(slicedPoints, 'route'); |
| | | const topPadding = Math.max(ORDER_UPDATE_LINE_TOP_PADDING, currentDeps.resolveSafeTopInset() + ORDER_UPDATE_LINE_TOP_EXTRA_PADDING); |
| | | mapCtx.includePoints({ |
| | | points: slicedPoints, |
| | | padding: [topPadding, ORDER_UPDATE_LINE_SIDE_PADDING, bottomPadding, ORDER_UPDATE_LINE_SIDE_PADDING] |
| | | }); |
| | | }, []); |
| | | |
| | | useEffect(() => { |
| | | if (!isActiveStatus) return; |
| | | const timer = setInterval(() => { |
| | | tryApplySpeedBasedScale(); |
| | | }, DYNAMIC_SCALE_INTERVAL_MS); |
| | | return () => clearInterval(timer); |
| | | }, [isActiveStatus, tryApplySpeedBasedScale]); |
| | | |
| | | /** |
| | | * 处理地图手势,手动缩放/拖拽后短暂暂停视口同步。 |
| | | */ |
| | | const handleRegionChange = useCallback((event: any) => { |
| | | const causedBy = String(event?.causedBy || event?.detail?.causedBy || '').toLowerCase(); |
| | |
| | | distanceMeters: number | null; |
| | | /** 预估行程时长,单位秒。 */ |
| | | durationSeconds: number | null; |
| | | /** 预估速度,单位米/秒。 */ |
| | | speedMps: number | null; |
| | | }; |
| | | /** 路线备选项。 */ |
| | | routeOptions?: any[]; |
| | |
| | | distanceMeters: number | null; |
| | | /** 时长,单位秒。 */ |
| | | durationSeconds: number | null; |
| | | /** 速度,单位 m/s。 */ |
| | | speedMps: number | null; |
| | | }) => void; |
| | | /** 设置当前用户定位。 */ |
| | | setCurrentUserLocation?: (location: CurrentUserLocation) => void; |
| | |
| | | distance: normalizeDistanceKm(Number(response?.distance)), |
| | | duration: normalizeDurationSeconds(Number(response?.duration)), |
| | | direction, |
| | | speedMps: normalizeSpeedMps(response?.speed), |
| | | timestamp: Date.now(), |
| | | vehicleMoveAlongPoints: [], |
| | | forceFullRedraw: true, |
| | |
| | | if (syncOrderRouteMetrics) { |
| | | deps.setOrderRouteMetrics?.({ |
| | | distanceMeters: Number.isFinite(Number(route?.distance)) ? Number(route?.distance) : null, |
| | | durationSeconds: Number.isFinite(Number(route?.duration)) ? Number(route?.duration) : null, |
| | | speedMps: normalizeSpeedMps(route?.speed) |
| | | durationSeconds: Number.isFinite(Number(route?.duration)) ? Number(route?.duration) : null |
| | | }); |
| | | } |
| | | |
| | |
| | | |
| | | deps.setOrderRouteMetrics?.({ |
| | | distanceMeters: Number.isFinite(Number(response?.distance)) ? Number(response?.distance) : null, |
| | | durationSeconds: Number.isFinite(Number(response?.duration)) ? Number(response?.duration) : null, |
| | | speedMps: normalizeSpeedMps(response?.speed) |
| | | durationSeconds: Number.isFinite(Number(response?.duration)) ? Number(response?.duration) : null |
| | | }); |
| | | |
| | | const normalizedDirection = resolveDirectionValue(response); |
| | |
| | | orderRouteMetrics: { |
| | | distanceMeters: number | null; |
| | | durationSeconds: number | null; |
| | | speedMps: number | null; |
| | | }; |
| | | setOrderId: (orderId: string) => void; |
| | | setTripStatus: (status: TripStatus) => void; |
| | |
| | | setVehicleMoveAlongPoints: (points: any[]) => void; |
| | | setRouteDistance: (value: any) => void; |
| | | setRouteDuration: (value: number) => void; |
| | | setVehicleSpeed?: (speed: number) => void; |
| | | setModifyDestinationSocketStatus: (value: number | null) => void; |
| | | setOrderRouteMetrics?: (metrics: { |
| | | distanceMeters: number | null; |
| | | durationSeconds: number | null; |
| | | speedMps: number | null; |
| | | }) => void; |
| | | setBeforeUserEndSite: (site: { longitude: number; latitude: number } | null) => void; |
| | | isModifyDestinationReviewVisible?: boolean; |
| | |
| | | const route = safeParse(payload?.content?.route, {}); |
| | | const distanceMeters = Number(route?.distance); |
| | | const durationSecondsRaw = Number(route?.duration); |
| | | const speedMps = normalizeSpeedMps(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, |
| | | speedMps |
| | | durationSeconds: Number.isFinite(durationSecondsRaw) ? durationSecondsRaw : null |
| | | }); |
| | | }; |
| | | |
| | |
| | | return; |
| | | } |
| | | const routeSnapshot = normalizeSocketRouteSnapshot(payload); |
| | | if (typeof routeSnapshot.speed === 'number') { |
| | | currentDeps.setVehicleSpeed?.(routeSnapshot.speed); |
| | | } |
| | | const shouldOnlySyncModifyPreviewVehicle = isModifyDestinationPreviewActive && currentTripStatus === TripStatus.InService; |
| | | const shouldFreezeModifyPreviewRoute = isModifyDestinationPreviewActive && currentTripStatus !== TripStatus.InService; |
| | | if (isModifyDestinationPreviewActive) { |
| | |
| | | type OrderRouteMetrics = { |
| | | distanceMeters: number | null; |
| | | durationSeconds: number | null; |
| | | speedMps: number | null; |
| | | }; |
| | | |
| | | /** |
| | |
| | | const [showCouponSelectPopup, setShowCouponSelectPopup] = useState(false); // 优惠券选择弹窗展示状态 |
| | | const [showModifyDestinationFailPopup, setShowModifyDestinationFailPopup] = useState(false); // 修改目的地失败弹窗 |
| | | const [modifyDestinationFailMessage, setModifyDestinationFailMessage] = useState(''); // 修改目的地失败文案(默认空,避免闪文案) |
| | | const [vehicleSpeed, setVehicleSpeed] = useState<number | null>(null); // 车辆当前速度 (m/s) |
| | | const [orderRouteMetrics, setOrderRouteMetrics] = useState<OrderRouteMetrics>({ |
| | | distanceMeters: null, |
| | | durationSeconds: null, |
| | | speedMps: null |
| | | durationSeconds: null |
| | | }); |
| | | const [currentUserLocation, setCurrentUserLocation] = useState<CurrentUserLocation>(null); // 当前用户真实定位,避免被地图中心点覆盖 |
| | | const [modifyDestinationSocketStatus, setModifyDestinationSocketStatus] = useState<number | null>(null); // 后端推送的修改目的地展示状态 |
| | |
| | | useEffect(() => { |
| | | setOrderRouteMetrics({ |
| | | distanceMeters: null, |
| | | durationSeconds: null, |
| | | speedMps: null |
| | | durationSeconds: null |
| | | }); |
| | | const cachedSocketStatus = Taro.getStorageSync(MODIFY_DESTINATION_SOCKET_STATUS_KEY); |
| | | const cachedOrderId = String(cachedSocketStatus?.orderId || ''); |
| | |
| | | } = viewportState; // 视口状态解构 |
| | | const activeTripScale = useActiveTripScale({ |
| | | tripStatus, |
| | | speedMps: orderRouteMetrics?.speedMps, |
| | | mapScale, |
| | | setMapScale: viewportState.setMapScale, |
| | | syncActiveTripViewportToVehicle, |
| | | vehicleSpeed, |
| | | routePoints, |
| | | getDynamicScaleHoldUntil, |
| | | computeBottomPadding, |
| | | resolveSafeTopInset, |
| | | holdDynamicScale, |
| | | getDynamicScaleHoldUntil |
| | | }); // 行程中速度缩放与 ORDER_UPDATE_LINE 视口编排 |
| | | holdDynamicScale |
| | | }); // 行程中 ORDER_UPDATE_LINE 视口编排 |
| | | const arrivedMarkerReady = tripStatus !== TripStatus.ArrivedDestination || typeof vehicleDirection === 'number'; |
| | | const arrivedTooltipReady = tripStatus !== TripStatus.ArrivedDestination || (arrivedMarkerReady && mapScale >= 20); |
| | | const markerState = useMapMarkers({ |
| | |
| | | setVehicleMoveAlongPoints, |
| | | setRouteDistance, |
| | | setRouteDuration, |
| | | setVehicleSpeed, |
| | | setModifyDestinationSocketStatus, |
| | | setOrderRouteMetrics, |
| | | setBeforeUserEndSite, |