| src/pages/robotaxi/home/index.tsx | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/robotaxi/pages/begin-route/hooks/runtime-sync.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/robotaxi/pages/begin-route/hooks/use-page-restore.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/robotaxi/pages/begin-route/hooks/use-socket-fallback-polling.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/robotaxi/pages/begin-route/hooks/use-web-socket-events.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/robotaxi/pages/begin-route/index.tsx | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
src/pages/robotaxi/home/index.tsx
@@ -102,6 +102,7 @@ const [orderData, setOrderData] = useState<any>({}); // 订单数据 const [isLogin, setIsLogin] = useState(false); // 是否登录 const [overlayAnchorBottom, setOverlayAnchorBottom] = useState<number | null>(null); // 浮层初始化锚点(距底部) const shouldShowOrderStatusCard = useMemo(() => tripStatus === 3 || Boolean(orderData?.id), [orderData?.id, tripStatus]); // 订单状态卡片展示时机,避免真实数据未返回时先展示默认图 const { boxHeight, originOpacity, @@ -539,12 +540,16 @@ </View> </View> </View> <OrderStatusCard tripStatus={tripStatus} orderData={orderData} onViewDetail={toOrderDetail} orderStatusMap={ORDER_STATUS} /> { shouldShowOrderStatusCard && ( <OrderStatusCard tripStatus={tripStatus} orderData={orderData} onViewDetail={toOrderDetail} orderStatusMap={ORDER_STATUS} /> ) } {/* 运营活动 */} {bannerList.length > 0 && ( <View className="activity"> src/robotaxi/pages/begin-route/hooks/runtime-sync.ts
New file @@ -0,0 +1,429 @@ import Taro from '@tarojs/taro'; import type { MutableRefObject } from 'react'; import { mapOrderStatusToTripStatus, safeParse } from '../../../../utils/Robotaxi-public-method'; import { normalizeDistanceKm, normalizeDurationSeconds } from '../../../utils/format'; export enum TripStatus { CALLING = 3, CAR_ARRIVING = 4, CAR_ARRIVED = 5, IN_TRIP = 6, ARRIVED_DESTINATION = 7, FINISHED = 8, PAID = 9, } export enum VehiclePointMode { FIRST = 'first', LAST = 'last', SERVED_LAST_THEN_FIRST = 'served_last_then_first', FIRST_THEN_SERVED_LAST = 'first_then_served_last', } type RouteDistance = { value: number; unit: 'km' | 'm' }; export type RoutePoint = { latitude: number; longitude: number; }; export type OrderSite = { id: string; name: string; latitude: number; longitude: number; electronicAreaId: string; time: number; }; export type OrderSnapshot = { orderId: string; tripStatus: number | null; startSite?: OrderSite; endSite?: OrderSite; trunkStatus?: boolean; showCancelPopup?: boolean; showPassengerNotice?: boolean; }; export type RouteSnapshot = { points: RoutePoint[]; distance: RouteDistance | null; duration: number | null; direction: number | null; vehicleMoveAlongPoints: RoutePoint[]; forceFullRedraw: boolean; eraseVehiclePointMode: VehiclePointMode; normalVehiclePointMode: VehiclePointMode; }; export type ApplyOrderSnapshotDeps = { orderId: string; tripStatusRef: MutableRefObject<number>; setOrderId: (value: string) => void; setTripStatus: (value: number) => void; setStartSite: (value: OrderSite) => void; setEndSite: (value: OrderSite) => void; setTrunkStatus: (value: boolean) => void; }; export type ApplyRouteSnapshotDeps = { tripStatusRef: MutableRefObject<number>; setRoutePoints: (points: RoutePoint[]) => void; setPolyline: (lines: any[]) => void; setVehicleDirection: (value: number) => void; setVehicleMoveAlongPoints?: (points: RoutePoint[]) => void; setRouteDistance: (value: RouteDistance | null) => void; setRouteDuration: (value: number | null) => void; updateVehicleLocationThrottled: (lat: number, lng: number, force?: boolean) => void; erasePassedRouteSegments: (points: RoutePoint[]) => void; applyRoutePoints: (points: RoutePoint[]) => boolean; polylineKeyRef: MutableRefObject<string>; lastPolylineUpdateAtRef: MutableRefObject<number>; lastRoutePointRef: MutableRefObject<RoutePoint | null>; lastRouteUpdateAtRef: MutableRefObject<number>; lastVehiclePointRef?: MutableRefObject<RoutePoint | null>; currentFullRouteRef: MutableRefObject<RoutePoint[]>; lastErasedIndexRef: MutableRefObject<number>; orderRouteInitializedRef: MutableRefObject<boolean>; }; type ApplyOrderSnapshotOptions = { protectCriticalTripStatus?: boolean; }; const CRITICAL_TRIP_STATUSES = [TripStatus.CAR_ARRIVING, TripStatus.IN_TRIP]; // 关键行程态不允许被旧消息回退 const TERMINAL_ROUTE_STATUSES = [TripStatus.FINISHED, TripStatus.PAID]; // 结束态只同步概要,不再消费路线点 const ROUTE_ERASE_EXCLUDED_STATUSES = [TripStatus.ARRIVED_DESTINATION, TripStatus.FINISHED, TripStatus.PAID]; // 这些状态不走擦线分支 const MOVE_ALONG_ACTIVE_STATUSES = [TripStatus.CAR_ARRIVING, TripStatus.IN_TRIP]; // 这些状态允许同步车辆沿线路径 /** * 将后端点位数组规范成前端统一路线点结构。 */ function normalizeRoutePoints(rawPoints: any): RoutePoint[] { if (!Array.isArray(rawPoints)) { return []; } return rawPoints .map((item: any) => ({ longitude: Number(item?.longitude), latitude: Number(item?.latitude), })) .filter((item: RoutePoint) => Number.isFinite(item.longitude) && Number.isFinite(item.latitude)); } /** * 将方向值归一化到 0-359,避免不同来源出现负值或字符串。 */ function normalizeDirection(rawDirection: any) { const direction = Number(rawDirection); if (!Number.isFinite(direction)) { return null; } return ((direction % 360) + 360) % 360; } /** * 构造统一上车点结构,供页面恢复和掉线轮询复用。 */ export function buildStartSiteFromActiveOrder(activeOrder: any): OrderSite { return { id: String(activeOrder?.startPointId || ''), name: String(activeOrder?.startPointName || ''), latitude: Number(activeOrder?.startLat || 0), longitude: Number(activeOrder?.startLon || 0), electronicAreaId: String(activeOrder?.xpElectronicFenceAreaId || ''), time: Number(activeOrder?.startParkingTimeLimit || 0), }; } /** * 构造统一下车点结构,供页面恢复和掉线轮询复用。 */ export function buildEndSiteFromActiveOrder(activeOrder: any): OrderSite { return { id: String(activeOrder?.endPointId || ''), name: String(activeOrder?.endPointName || ''), latitude: Number(activeOrder?.endLat || 0), longitude: Number(activeOrder?.endLon || 0), electronicAreaId: String(activeOrder?.xpElectronicFenceAreaId || ''), time: Number(activeOrder?.endParkingTimeLimit || 0), }; } /** * 归一化活跃订单接口数据,统一成页面快照结构。 */ export function normalizeActiveOrderSnapshot(activeOrder: any, fallbackOrderId = ''): OrderSnapshot | null { if (!activeOrder) { return null; } return { orderId: String(activeOrder?.id || fallbackOrderId || ''), tripStatus: mapOrderStatusToTripStatus(activeOrder?.status || 0), startSite: buildStartSiteFromActiveOrder(activeOrder), endSite: buildEndSiteFromActiveOrder(activeOrder), trunkStatus: ![0, 2].includes(Number(activeOrder?.openTrunk)), }; } /** * 归一化 socket 订单状态消息,只保留可复用的页面快照字段。 */ export function normalizeSocketOrderStatusSnapshot(payload: any, fallbackOrderId = ''): OrderSnapshot { return { orderId: String(payload?.content?.orderId || payload?.orderId || fallbackOrderId || ''), tripStatus: typeof payload?.tripStatus === 'number' ? payload.tripStatus : null, showCancelPopup: payload?.showCancelPopup === true, showPassengerNotice: payload?.showPassengerNotice === true, }; } /** * 将订单快照落到页面状态与 storage,保证 socket 和 API 走同一套同步规则。 */ export function applyOrderSnapshot( snapshot: OrderSnapshot | null, deps: ApplyOrderSnapshotDeps, options: ApplyOrderSnapshotOptions = {} ) { if (!snapshot) { return { nextOrderId: String(deps.orderId || Taro.getStorageSync('orderId') || ''), nextTripStatus: Number(deps.tripStatusRef.current), }; } const nextOrderId = String(snapshot.orderId || deps.orderId || Taro.getStorageSync('orderId') || ''); const currentTripStatus = Number(deps.tripStatusRef.current); let nextTripStatus = typeof snapshot.tripStatus === 'number' ? snapshot.tripStatus : currentTripStatus; if ( options.protectCriticalTripStatus && CRITICAL_TRIP_STATUSES.includes(currentTripStatus) && nextTripStatus < currentTripStatus ) { nextTripStatus = currentTripStatus; } if (nextOrderId && String(deps.orderId || '') !== nextOrderId) { deps.setOrderId(nextOrderId); Taro.setStorageSync('orderId', nextOrderId); } if (typeof nextTripStatus === 'number' && nextTripStatus !== currentTripStatus) { deps.setTripStatus(nextTripStatus); deps.tripStatusRef.current = nextTripStatus; Taro.setStorageSync('tripStatus', nextTripStatus); } if (snapshot.startSite) { deps.setStartSite(snapshot.startSite); Taro.setStorageSync('startSite', snapshot.startSite); } if (snapshot.endSite) { deps.setEndSite(snapshot.endSite); Taro.setStorageSync('endSite', snapshot.endSite); } if (typeof snapshot.trunkStatus === 'boolean') { deps.setTrunkStatus(snapshot.trunkStatus); } return { nextOrderId, nextTripStatus, }; } /** * 归一化 socket 路线消息,统一路线点、概要和车辆移动点结构。 */ export function normalizeSocketRouteSnapshot(payload: any): RouteSnapshot { const route = safeParse(payload?.content?.route, {}); return { points: normalizeRoutePoints(payload?.points), distance: normalizeDistanceKm(Number(route?.distance)), duration: normalizeDurationSeconds(Number(route?.duration)), direction: normalizeDirection(payload?.content?.direction), vehicleMoveAlongPoints: normalizeRoutePoints(payload?.servedPoints), forceFullRedraw: payload?.forceFullRedraw === true, eraseVehiclePointMode: VehiclePointMode.FIRST_THEN_SERVED_LAST, normalVehiclePointMode: VehiclePointMode.SERVED_LAST_THEN_FIRST, }; } /** * 归一化订单路线接口返回,抹平与 socket 路线消息的结构差异。 */ export function normalizeApiOrderRouteSnapshot(response: any, tripStatus: number): RouteSnapshot { const isArrivedPickup = tripStatus === TripStatus.CAR_ARRIVED; // 到达上车点时车辆应贴近路线终点 const isArrivedDestination = tripStatus === TripStatus.ARRIVED_DESTINATION; // 到达终点时车辆应贴近终点 return { points: normalizeRoutePoints(response?.polyline), distance: normalizeDistanceKm(Number(response?.distance)), duration: normalizeDurationSeconds(Number(response?.duration)), direction: normalizeDirection(response?.direction), vehicleMoveAlongPoints: [], forceFullRedraw: false, eraseVehiclePointMode: isArrivedPickup ? VehiclePointMode.LAST : VehiclePointMode.FIRST, normalVehiclePointMode: isArrivedDestination ? VehiclePointMode.LAST : VehiclePointMode.FIRST, }; } /** * 根据不同来源的策略解析当前应使用的车辆点位。 */ function resolveVehiclePoint( points: RoutePoint[], vehicleMoveAlongPoints: RoutePoint[], mode: VehiclePointMode ) { const firstPoint = points?.[0] || null; const lastPoint = points.length > 0 ? points[points.length - 1] : null; const servedLastPoint = vehicleMoveAlongPoints.length > 0 ? vehicleMoveAlongPoints[vehicleMoveAlongPoints.length - 1] : null; if (mode === VehiclePointMode.LAST) { return lastPoint; } if (mode === VehiclePointMode.SERVED_LAST_THEN_FIRST) { return servedLastPoint || firstPoint; } if (mode === VehiclePointMode.FIRST_THEN_SERVED_LAST) { return firstPoint || servedLastPoint; } return firstPoint; } /** * 同步路线概要,避免不同来源重复写距离、时长、方向更新逻辑。 */ function applyRouteSummary(snapshot: RouteSnapshot, deps: ApplyRouteSnapshotDeps) { if (typeof snapshot.direction === 'number') { deps.setVehicleDirection(snapshot.direction); } if (snapshot.distance !== null) { deps.setRouteDistance(snapshot.distance); } if (snapshot.duration !== null) { deps.setRouteDuration(snapshot.duration); } } /** * 按统一规则同步车辆点位,保留终点态去抖,避免 marker 抖动。 */ function syncVehiclePointWithRoute( snapshot: RouteSnapshot, deps: ApplyRouteSnapshotDeps, mode: VehiclePointMode ) { const targetPoint = resolveVehiclePoint(snapshot.points, snapshot.vehicleMoveAlongPoints, mode); if (!targetPoint) { return; } const currentTripStatus = Number(deps.tripStatusRef.current); if (currentTripStatus === TripStatus.ARRIVED_DESTINATION && deps.lastVehiclePointRef?.current) { const lastPoint = deps.lastVehiclePointRef.current; 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) { return; } } deps.updateVehicleLocationThrottled(targetPoint.latitude, targetPoint.longitude, true); } /** * 强制全量重绘时,先清运行时引用,再完整回填路线。 */ function handleForceFullRouteRedraw(snapshot: RouteSnapshot, deps: ApplyRouteSnapshotDeps) { deps.setRoutePoints([]); deps.setPolyline([]); deps.orderRouteInitializedRef.current = false; deps.currentFullRouteRef.current = []; deps.lastErasedIndexRef.current = 0; deps.polylineKeyRef.current = ''; deps.lastPolylineUpdateAtRef.current = 0; deps.lastRoutePointRef.current = null; deps.lastRouteUpdateAtRef.current = 0; Taro.nextTick(() => { deps.orderRouteInitializedRef.current = snapshot.points.length > 0; deps.currentFullRouteRef.current = [...snapshot.points]; deps.lastErasedIndexRef.current = 0; deps.applyRoutePoints(snapshot.points); if (snapshot.points.length > 0) { syncVehiclePointWithRoute(snapshot, deps, snapshot.normalVehiclePointMode); } }); } /** * 擦线态优先更新车辆点位,再交给前端差分擦线逻辑处理。 */ function handleEraseRouteSnapshot(snapshot: RouteSnapshot, deps: ApplyRouteSnapshotDeps) { const targetPoint = resolveVehiclePoint( snapshot.points, snapshot.vehicleMoveAlongPoints, snapshot.eraseVehiclePointMode ); if (targetPoint) { deps.updateVehicleLocationThrottled(targetPoint.latitude, targetPoint.longitude, true); } if (deps.orderRouteInitializedRef.current && deps.currentFullRouteRef.current.length > 0) { deps.erasePassedRouteSegments(snapshot.points); return; } deps.orderRouteInitializedRef.current = true; deps.currentFullRouteRef.current = [...snapshot.points]; deps.lastErasedIndexRef.current = 0; Taro.nextTick(() => { deps.applyRoutePoints(snapshot.points); }); } /** * 普通路线态直接同步整条路线,并按策略更新车辆位置。 */ function handleNormalRouteSnapshot(snapshot: RouteSnapshot, deps: ApplyRouteSnapshotDeps) { Taro.nextTick(() => { const updated = deps.applyRoutePoints(snapshot.points); if (!updated) { return; } if (![4, 5, 6, 7].includes(Number(deps.tripStatusRef.current))) { return; } syncVehiclePointWithRoute(snapshot, deps, snapshot.normalVehiclePointMode); }); } /** * 将路线快照落到页面状态,让 socket 路线和 API 路线复用同一套渲染规则。 */ export function applyRouteSnapshot(snapshot: RouteSnapshot, deps: ApplyRouteSnapshotDeps) { applyRouteSummary(snapshot, deps); const currentTripStatus = Number(deps.tripStatusRef.current); if (TERMINAL_ROUTE_STATUSES.includes(currentTripStatus)) { return; } if ( MOVE_ALONG_ACTIVE_STATUSES.includes(currentTripStatus) && deps.setVehicleMoveAlongPoints && snapshot.vehicleMoveAlongPoints.length > 0 ) { deps.setVehicleMoveAlongPoints(snapshot.vehicleMoveAlongPoints); } if (snapshot.forceFullRedraw) { handleForceFullRouteRedraw(snapshot, deps); return; } const isEraseStatus = !ROUTE_ERASE_EXCLUDED_STATUSES.includes(currentTripStatus); if (isEraseStatus) { handleEraseRouteSnapshot(snapshot, deps); return; } handleNormalRouteSnapshot(snapshot, deps); } src/robotaxi/pages/begin-route/hooks/use-page-restore.ts
@@ -1,8 +1,11 @@ import Taro from '@tarojs/taro'; import wsManager from '../../../../utils/websocket'; import { mapOrderStatusToTripStatus } from '../../../../utils/Robotaxi-public-method'; import { getOrderActive, } from '../../../api/home'; import { queryVehicleByOrderId } from '../../../api/begin-route'; import { applyOrderSnapshot, normalizeActiveOrderSnapshot } from './runtime-sync'; type Deps = { wsHost: string; @@ -60,30 +63,16 @@ getOrderActive() .then((res: any) => { if (!res) return; deps.setTrunkStatus(![0, 2].includes(res.openTrunk)); const nextStartSite = { id: res.startPointId || '', name: res.startPointName || '', latitude: res.startLat || 0, longitude: res.startLon || 0, electronicAreaId: res.xpElectronicFenceAreaId || '', time: res.startParkingTimeLimit || 0, }; const nextEndSite = { id: res.endPointId || '', name: res.endPointName || '', latitude: res.endLat || 0, longitude: res.endLon || 0, electronicAreaId: res.xpElectronicFenceAreaId || '', time: res.endParkingTimeLimit || 0, }; const nextStatus = mapOrderStatusToTripStatus(res.status || 1); Taro.setStorageSync('tripStatus', nextStatus); deps.setTripStatus(nextStatus); Taro.setStorageSync('startSite', nextStartSite); Taro.setStorageSync('endSite', nextEndSite); deps.setStartSite(nextStartSite); deps.setEndSite(nextEndSite); const orderSnapshot = normalizeActiveOrderSnapshot(res); applyOrderSnapshot(orderSnapshot, { orderId: String(Taro.getStorageSync('orderId') || ''), tripStatusRef: { current: deps.getCurrentTripStatus() }, setOrderId: () => { }, setTripStatus: deps.setTripStatus, setStartSite: deps.setStartSite, setEndSite: deps.setEndSite, setTrunkStatus: deps.setTrunkStatus, }); }) .catch(() => { }); }; src/robotaxi/pages/begin-route/hooks/use-socket-fallback-polling.ts
New file @@ -0,0 +1,301 @@ import Taro, { useDidHide, useDidShow } from '@tarojs/taro'; import { useEffect, useRef, useState } from 'react'; import wsManager from '../../../../utils/websocket'; import { queryVehicleByOrderId } from '../../../api/begin-route'; import { getOrderActive } from '../../../api/home'; import { applyOrderSnapshot, applyRouteSnapshot, normalizeActiveOrderSnapshot, normalizeApiOrderRouteSnapshot, TripStatus } from './runtime-sync'; type Deps = { /** 行程状态 */ tripStatus: number; /** 行程状态引用 */ tripStatusRef: React.MutableRefObject<number>; /** 订单 ID */ orderId: string; /** 设置行程状态 */ setTripStatus: (value: number) => void; /** 设置订单 ID */ setOrderId: (value: string) => void; /** 设置车辆信息 */ setVehicleInfo: (value: any) => void; /** 设置上车点 */ setStartSite: (value: any) => void; /** 设置下车点 */ setEndSite: (value: any) => void; /** 设置后备箱状态 */ setTrunkStatus: (value: boolean) => void; /** 请求订单路线 */ routeRequestOrderRouteApi: (orderId: string, status: number) => Promise<any>; /** 设置路线点 */ setRoutePoints: (points: any[]) => void; /** 设置线段 */ setPolyline: (lines: any[]) => void; /** 设置车辆方向 */ setVehicleDirection: (value: number) => void; /** 设置车辆沿线移动点 */ setVehicleMoveAlongPoints: (points: any[]) => void; /** 设置路线距离 */ setRouteDistance: (value: any) => void; /** 设置路线时长 */ setRouteDuration: (value: number | null) => void; /** 擦除已走路线 */ erasePassedRouteSegments: (points: any[]) => void; /** 应用路线点 */ applyRoutePoints: (points: any[]) => boolean; /** 更新车辆位置 */ updateVehicleLocationThrottled: (lat: number, lng: number, force?: boolean) => void; /** 折线 key */ polylineKeyRef: React.MutableRefObject<string>; /** 最近一次折线更新时间 */ lastPolylineUpdateAtRef: React.MutableRefObject<number>; /** 最近一次路线首点 */ lastRoutePointRef: React.MutableRefObject<any>; /** 最近一次路线更新时间 */ lastRouteUpdateAtRef: React.MutableRefObject<number>; /** 最近一次车辆点 */ lastVehiclePointRef: React.MutableRefObject<any>; /** 当前完整路线 */ currentFullRouteRef: React.MutableRefObject<any[]>; /** 最近一次擦线索引 */ lastErasedIndexRef: React.MutableRefObject<number>; /** 订单路线是否已初始化 */ orderRouteInitializedRef: React.MutableRefObject<boolean>; }; enum FallbackPollingConfig { DISCONNECT_CONFIRM_DELAY = 2000, STATUS_POLL_INTERVAL = 5000, ROUTE_POLL_INTERVAL = 6000, VEHICLE_POLL_INTERVAL = 10000, } const FALLBACK_ACTIVE_STATUSES = [ TripStatus.CALLING, TripStatus.CAR_ARRIVING, TripStatus.CAR_ARRIVED, TripStatus.IN_TRIP, TripStatus.ARRIVED_DESTINATION ]; // 允许启动兜底轮询的状态 const FALLBACK_ROUTE_STATUSES = [ TripStatus.CAR_ARRIVING, TripStatus.CAR_ARRIVED, TripStatus.IN_TRIP, TripStatus.ARRIVED_DESTINATION ]; // 需要补偿路线的状态 /** * begin-route 掉线兜底编排:负责感知 socket 失联,并在行程关键状态下补偿轮询状态与路线。 */ export default function useSocketFallbackPolling(deps: Deps) { const [isSocketUnavailable, setIsSocketUnavailable] = useState(!wsManager.isConnectedStatus()); // socket 是否处于不可用状态 const [isPageVisible, setIsPageVisible] = useState(true); // 页面当前是否可见 const disconnectConfirmTimerRef = useRef<any>(null); // 掉线确认定时器 const lastVehiclePollAtRef = useRef(0); // 最近一次车辆信息轮询时间 const statusPollInFlightRef = useRef(false); // 状态轮询是否进行中 /** * 清理掉线确认定时器。 */ const clearDisconnectConfirmTimer = () => { if (disconnectConfirmTimerRef.current) { clearTimeout(disconnectConfirmTimerRef.current); disconnectConfirmTimerRef.current = null; } }; /** * 延迟确认 socket 是否真的掉线,避免和自动重连抖动互相打架。 */ const confirmSocketUnavailableWithDelay = () => { clearDisconnectConfirmTimer(); disconnectConfirmTimerRef.current = setTimeout(() => { setIsSocketUnavailable(!wsManager.isConnectedStatus()); }, FallbackPollingConfig.DISCONNECT_CONFIRM_DELAY); }; /** * 同步活跃订单快照,保证轮询拿到的数据能驱动页面关键状态。 */ const syncActiveOrderSnapshot = (activeOrder: any) => { const orderSnapshot = normalizeActiveOrderSnapshot(activeOrder, deps.orderId); return applyOrderSnapshot(orderSnapshot, { orderId: deps.orderId, tripStatusRef: deps.tripStatusRef, setOrderId: deps.setOrderId, setTripStatus: deps.setTripStatus, setStartSite: deps.setStartSite, setEndSite: deps.setEndSite, setTrunkStatus: deps.setTrunkStatus, }); }; /** * 轮询车辆信息,掉线期间补齐车牌和车辆信息。 */ const pollVehicleInfo = (targetOrderId: string) => { const now = Date.now(); const shouldSkipVehiclePolling = now - lastVehiclePollAtRef.current < FallbackPollingConfig.VEHICLE_POLL_INTERVAL; if (!targetOrderId || shouldSkipVehiclePolling) { return; } lastVehiclePollAtRef.current = now; queryVehicleByOrderId({ orderId: targetOrderId }) .then((res: any) => { const latestOrderId = String(Taro.getStorageSync('orderId') || deps.orderId || ''); if (!latestOrderId || latestOrderId !== String(targetOrderId)) { return; } deps.setVehicleInfo(res); }) .catch(() => { }); }; /** * 轮询订单状态,并在需要时触发车辆信息补偿同步。 */ const pollOrderStatus = () => { if (statusPollInFlightRef.current) { return; } statusPollInFlightRef.current = true; getOrderActive() .then((activeOrder: any) => { if (!activeOrder) { return; } const orderApplyResult = syncActiveOrderSnapshot(activeOrder); const effectiveOrderId = String(orderApplyResult?.nextOrderId || activeOrder?.id || deps.orderId || ''); pollVehicleInfo(effectiveOrderId); }) .catch(() => { }) .finally(() => { statusPollInFlightRef.current = false; }); }; /** * 轮询订单路线,掉线期间继续刷新车辆位置和轨迹。 */ const pollOrderRoute = () => { const currentTripStatus = Number(deps.tripStatusRef.current); const targetOrderId = String(Taro.getStorageSync('orderId') || deps.orderId || ''); if (!targetOrderId || !FALLBACK_ROUTE_STATUSES.includes(currentTripStatus)) { return; } deps.routeRequestOrderRouteApi(targetOrderId, currentTripStatus) .then((res: any) => { const routeSnapshot = normalizeApiOrderRouteSnapshot(res, currentTripStatus); applyRouteSnapshot(routeSnapshot, { tripStatusRef: deps.tripStatusRef, setRoutePoints: deps.setRoutePoints, setPolyline: deps.setPolyline, setVehicleDirection: deps.setVehicleDirection, setVehicleMoveAlongPoints: deps.setVehicleMoveAlongPoints, setRouteDistance: deps.setRouteDistance, setRouteDuration: deps.setRouteDuration, updateVehicleLocationThrottled: deps.updateVehicleLocationThrottled, erasePassedRouteSegments: deps.erasePassedRouteSegments, applyRoutePoints: deps.applyRoutePoints, polylineKeyRef: deps.polylineKeyRef, lastPolylineUpdateAtRef: deps.lastPolylineUpdateAtRef, lastRoutePointRef: deps.lastRoutePointRef, lastRouteUpdateAtRef: deps.lastRouteUpdateAtRef, lastVehiclePointRef: deps.lastVehiclePointRef, currentFullRouteRef: deps.currentFullRouteRef, lastErasedIndexRef: deps.lastErasedIndexRef, orderRouteInitializedRef: deps.orderRouteInitializedRef, }); }) .catch(() => { }); }; useDidShow(() => { setIsPageVisible(true); setIsSocketUnavailable(!wsManager.isConnectedStatus()); }); useDidHide(() => { setIsPageVisible(false); }); /** * 监听页面 socket 连接态,延迟确认掉线,避免与自动重连过程互相干扰。 */ useEffect(() => { /** * socket 恢复连接时关闭兜底状态。 */ const handleSocketOpen = () => { clearDisconnectConfirmTimer(); setIsSocketUnavailable(false); }; /** * socket 关闭时延迟确认掉线。 */ const handleSocketClose = () => { confirmSocketUnavailableWithDelay(); }; /** * socket 异常时延迟确认掉线。 */ const handleSocketError = () => { confirmSocketUnavailableWithDelay(); }; wsManager.on('open', handleSocketOpen); wsManager.on('close', handleSocketClose); wsManager.on('error', handleSocketError); return () => { clearDisconnectConfirmTimer(); wsManager.off('open', handleSocketOpen); wsManager.off('close', handleSocketClose); wsManager.off('error', handleSocketError); }; }, []); /** * 仅在页面可见且 socket 不可用时启动兜底轮询,socket 恢复后自动停止。 */ useEffect(() => { const targetOrderId = String(Taro.getStorageSync('orderId') || deps.orderId || ''); const shouldStartFallbackPolling = ( isPageVisible && isSocketUnavailable && !!targetOrderId && FALLBACK_ACTIVE_STATUSES.includes(deps.tripStatus) ); if (!shouldStartFallbackPolling) { return; } pollOrderStatus(); pollOrderRoute(); const statusTimer = setInterval(() => { pollOrderStatus(); }, FallbackPollingConfig.STATUS_POLL_INTERVAL); const routeTimer = setInterval(() => { pollOrderRoute(); }, FallbackPollingConfig.ROUTE_POLL_INTERVAL); return () => { clearInterval(statusTimer); clearInterval(routeTimer); }; }, [deps.orderId, deps.tripStatus, isPageVisible, isSocketUnavailable]); return { isSocketUnavailable }; } src/robotaxi/pages/begin-route/hooks/use-web-socket-events.ts
@@ -2,8 +2,13 @@ import Taro from '@tarojs/taro'; import wsManager from '../../../../utils/websocket'; import { queryVehicleByOrderId } from '../../../api/begin-route'; import { clearOrderTripStorage, safeParse } from '../../../../utils/Robotaxi-public-method'; import { normalizeDistanceKm, normalizeDurationSeconds } from '../../../../robotaxi/utils/format'; import { clearOrderTripStorage } from '../../../../utils/Robotaxi-public-method'; import { applyOrderSnapshot, applyRouteSnapshot, normalizeSocketOrderStatusSnapshot, normalizeSocketRouteSnapshot } from './runtime-sync'; type UseWebSocketEventsDeps = { tripStatusRef: any; @@ -62,25 +67,6 @@ deps.orderRouteInitializedRef.current = false; }; const syncTripStatus = (payload: any, deps: UseWebSocketEventsDeps) => { const mappedTripStatus = payload?.tripStatus; if (typeof mappedTripStatus !== 'number') { return null; } const currentTripStatus = Number(deps.tripStatusRef.current); // 行程进行关键态不允许被回退,避免 4/6 被延迟状态覆盖导致路线链路重置 if ([4, 6].includes(currentTripStatus) && mappedTripStatus < currentTripStatus) { return currentTripStatus; } if (currentTripStatus === mappedTripStatus) { return mappedTripStatus; } deps.setTripStatus(mappedTripStatus); deps.tripStatusRef.current = mappedTripStatus; Taro.setStorageSync('tripStatus', mappedTripStatus); return mappedTripStatus; }; const syncVehicleInfo = ( effectiveOrderId: string, deps: UseWebSocketEventsDeps, @@ -107,8 +93,8 @@ .catch(() => { }); }; const handleCancelPopupPush = (payload: any, changeStatusOld: number, deps: UseWebSocketEventsDeps) => { if (!payload?.showCancelPopup) return; const handleCancelPopupPush = (showCancelPopup: boolean, changeStatusOld: number, deps: UseWebSocketEventsDeps) => { if (!showCancelPopup) return; if (changeStatusOld === 4 || changeStatusOld === 5) { deps.setOrderId(''); deps.setVehicleInfo({}); @@ -148,159 +134,30 @@ return; } const mappedTripStatus = syncTripStatus(payload, deps); const effectiveOrderId = resolveOrderId(payload, currentOrderId); syncVehicleInfo(effectiveOrderId, deps, lastFetchedVehicleOrderIdRef); handleStatusSideEffects(mappedTripStatus, changeStatusOld, deps); handleCancelPopupPush(payload, changeStatusOld, deps); const orderSnapshot = normalizeSocketOrderStatusSnapshot(payload, currentOrderId); const { nextOrderId, nextTripStatus } = applyOrderSnapshot(orderSnapshot, deps, { protectCriticalTripStatus: true }); syncVehicleInfo(nextOrderId, deps, lastFetchedVehicleOrderIdRef); handleStatusSideEffects(nextTripStatus, changeStatusOld, deps); handleCancelPopupPush(orderSnapshot.showCancelPopup === true, changeStatusOld, deps); if (payload?.showPassengerNotice) { if (orderSnapshot.showPassengerNotice) { deps.checkAndShowPassengerNotice(); } }; // 统一解析方向值 const resolveDirectionValue = (payload: any) => { const rawDirection = payload?.content?.direction; const direction = Number(rawDirection); if (!Number.isFinite(direction)) { return null; } return ((direction % 360) + 360) % 360; }; const updateRouteSummary = (payload: any, deps: UseWebSocketEventsDeps) => { const normalizedDirection = resolveDirectionValue(payload); if (typeof normalizedDirection === 'number') { deps.setVehicleDirection(normalizedDirection); } const route = safeParse(payload?.content?.route, {}); const distanceKm = normalizeDistanceKm(Number(route?.distance)); if (distanceKm !== null) { deps.setRouteDistance(distanceKm); } const durationSeconds = normalizeDurationSeconds(Number(route?.duration)); if (durationSeconds !== null) { deps.setRouteDuration(durationSeconds); } }; const resolveVehiclePoint = (points: any[], servedPoints: any[] = []) => { const servedLastPoint = Array.isArray(servedPoints) && servedPoints.length > 0 ? servedPoints[servedPoints.length - 1] : null; if (servedLastPoint && typeof servedLastPoint.latitude === 'number' && typeof servedLastPoint.longitude === 'number') { return servedLastPoint; } const firstPoint = points?.[0]; if (firstPoint && typeof firstPoint.latitude === 'number' && typeof firstPoint.longitude === 'number') { return firstPoint; } return null; }; const resolveVehiclePointForErase = (points: any[], servedPoints: any[] = []) => { const firstPoint = points?.[0]; if (firstPoint && typeof firstPoint.latitude === 'number' && typeof firstPoint.longitude === 'number') { return firstPoint; } return resolveVehiclePoint(points, servedPoints); }; const syncVehiclePointWithRoute = (points: any[], deps: UseWebSocketEventsDeps, servedPoints: any[] = []) => { const targetPoint = resolveVehiclePoint(points, servedPoints); if (!targetPoint) { return; } const currentTripStatus = Number(deps.tripStatusRef.current); if (currentTripStatus === 7) { const lastPoint = deps.lastVehiclePointRef.current; 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) { return; } } deps.updateVehicleLocationThrottled(targetPoint.latitude, targetPoint.longitude, true); }; const handleEraseRouteStatus = (points: any[], servedPoints: any[], deps: UseWebSocketEventsDeps) => { const targetPoint = resolveVehiclePointForErase(points, servedPoints); if (targetPoint) { deps.updateVehicleLocationThrottled(targetPoint.latitude, targetPoint.longitude, true); } if (deps.orderRouteInitializedRef.current && deps.currentFullRouteRef.current.length > 0) { deps.erasePassedRouteSegments(points); } else { deps.orderRouteInitializedRef.current = true; deps.currentFullRouteRef.current = [...points]; deps.lastErasedIndexRef.current = 0; Taro.nextTick(() => { deps.applyRoutePoints(points); }); } }; const handleNormalRouteStatus = (points: any[], deps: UseWebSocketEventsDeps, servedPoints: any[] = []) => { Taro.nextTick(() => { const updated = deps.applyRoutePoints(points); if (!updated) return; if (![4, 5, 6, 7].includes(deps.tripStatusRef.current)) return; syncVehiclePointWithRoute(points, deps, servedPoints); }); }; const handleForceFullRouteRedraw = (points: any[], deps: UseWebSocketEventsDeps, servedPoints: any[] = []) => { deps.setRoutePoints([]); deps.setPolyline([]); deps.orderRouteInitializedRef.current = false; deps.currentFullRouteRef.current = []; deps.lastErasedIndexRef.current = 0; deps.polylineKeyRef.current = ''; deps.lastPolylineUpdateAtRef.current = 0; deps.lastRoutePointRef.current = null; deps.lastRouteUpdateAtRef.current = 0; Taro.nextTick(() => { deps.orderRouteInitializedRef.current = points.length > 0; deps.currentFullRouteRef.current = [...points]; deps.lastErasedIndexRef.current = 0; deps.applyRoutePoints(points); if (points.length > 0) { syncVehiclePointWithRoute(points, deps, servedPoints); } }); }; const handleOrderRoutePayload = (payload: any, deps: UseWebSocketEventsDeps) => { 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 ([8, 9].includes(currentTripStatus)) { return; } if ([4, 6].includes(currentTripStatus)) { deps.setVehicleMoveAlongPoints(servedPoints); } if (forceFullRedraw) { handleForceFullRouteRedraw(points, deps, servedPoints); return; } const isEraseStatus = ![7, 8, 9].includes(currentTripStatus); if (isEraseStatus) { handleEraseRouteStatus(points, servedPoints, deps); return; } handleNormalRouteStatus(points, deps, servedPoints); const routeSnapshot = normalizeSocketRouteSnapshot(payload); applyRouteSnapshot(routeSnapshot, deps); }; export default function useWebSocketEvents(deps: UseWebSocketEventsDeps) { // 持有最新依赖,避免渲染过程反复解绑/重绑监听 const depsRef = useRef(deps); /** * 同步最新 deps,避免 socket 监听因为渲染而反复解绑重绑。 */ useEffect(() => { depsRef.current = deps; }, [deps]); @@ -325,6 +182,9 @@ const lastFetchedVehicleOrderIdRef = useRef<string | null>(null); // 仅初始化一次事件处理器与解绑逻辑 /** * 初始化 socket 业务事件处理器,并在卸载时统一解绑。 */ useEffect(() => { authSuccessHandlerRef.current = () => { if (!messageBound.current && messageHandlerRef.current) { src/robotaxi/pages/begin-route/index.tsx
@@ -28,6 +28,8 @@ import useTripRuntime from './hooks/use-trip-runtime'; // 订单动作编排 import useOrderActions from './hooks/use-order-actions'; // socket 掉线兜底轮询编排 import useSocketFallbackPolling from './hooks/use-socket-fallback-polling'; // 视图委托渲染编排 import useViewDelegates from './hooks/use-view-delegates'; @@ -37,11 +39,17 @@ import suggest from '../../../robotaxi/assets/suggest.png' import originIcon from '../../../robotaxi/assets/origin-icon.png' import { _ROBOTAXI_WS_HOST } from '../../../constants/api'; import { queryOrderRoute, queryOrderTrajectory } from '../../api/begin-route'; export default function BeginRoute() { const WAITING_TIMER_START_KEY = 'waitingTimerStartAt'; // 等待计时开始时间 const WAITING_TIMER_ELAPSED_KEY = 'waitingTimerElapsed'; // 等待计时已过秒数 const REQUOTE_EXPIRE_SECONDS = 10 * 60; // 10分钟 const requestOrderRouteApi = (targetOrderId: string, status: number) => { // 掉线兜底专用路线查询,直接返回接口结果供统一落地层消费 const routeType = status <= 5 ? 1 : 2; const requestRoute = [8, 9].includes(status) ? queryOrderTrajectory : queryOrderRoute; return requestRoute({ orderId: targetOrderId, routeType }); }; const { showCustomerService, @@ -386,6 +394,35 @@ applyRoutePoints: routeApplyRoutePoints, updateVehicleLocationThrottled: vehicleState.updateVehicleLocationThrottled }); useSocketFallbackPolling({ tripStatus, tripStatusRef, orderId, setTripStatus, setOrderId, setVehicleInfo, setStartSite, setEndSite, setTrunkStatus, routeRequestOrderRouteApi: requestOrderRouteApi, setRoutePoints, setPolyline, setVehicleDirection, setVehicleMoveAlongPoints, setRouteDistance, setRouteDuration, erasePassedRouteSegments: routeErasePassedRouteSegments, applyRoutePoints: routeApplyRoutePoints, updateVehicleLocationThrottled: vehicleState.updateVehicleLocationThrottled, polylineKeyRef, lastPolylineUpdateAtRef, lastRoutePointRef, lastRouteUpdateAtRef, lastVehiclePointRef, currentFullRouteRef, lastErasedIndexRef, orderRouteInitializedRef }); const orderActions = useOrderActions({ tripStatus,