src/robotaxi/pages/begin-route/components/calling-car-modules/ac-storage.ts
@@ -1,23 +1,26 @@ import Taro from '@tarojs/taro'; const AC_STORAGE_KEY_PREFIX = 'rt_control_'; const AC_STORAGE_KEY = 'rt_control'; export interface AcStorageData { acOpen: boolean; temperature: number; } function getStorageKey(orderId: string): string { return `${AC_STORAGE_KEY_PREFIX}${orderId}`; interface AcStorageEntry { orderId: string; acOpen: boolean; temperature: number; } export function getAcStorage(orderId: string): AcStorageData | null { try { const raw = Taro.getStorageSync(getStorageKey(orderId)); const raw = Taro.getStorageSync(AC_STORAGE_KEY); if (!raw) return null; const data = typeof raw === 'string' ? JSON.parse(raw) : raw; const data: AcStorageEntry = typeof raw === 'string' ? JSON.parse(raw) : raw; if (String(data.orderId || '') !== orderId) return null; if (typeof data.acOpen === 'boolean' && typeof data.temperature === 'number') { return data; return { acOpen: data.acOpen, temperature: data.temperature }; } return null; } catch { @@ -27,16 +30,9 @@ export function saveAcStorage(orderId: string, data: AcStorageData): void { try { Taro.setStorageSync(getStorageKey(orderId), JSON.stringify(data)); Taro.setStorageSync(AC_STORAGE_KEY, JSON.stringify({ orderId, acOpen: data.acOpen, temperature: data.temperature })); } catch { // 静默失败,不阻断交互 } } export function removeAcStorage(orderId: string): void { try { Taro.removeStorageSync(getStorageKey(orderId)); } catch { // 静默失败 } } src/robotaxi/pages/begin-route/components/calling-car-modules/temperature-control.tsx
@@ -13,7 +13,7 @@ import { getOrderId } from '../../../../utils/order'; import { TripStatus } from '../../../../constants/trip-status'; import { MODIFY_DESTINATION_SKIP_AC_QUERY_ON_RETURN_KEY } from '../../../../constants/modify-destination'; import { getAcStorage, saveAcStorage, removeAcStorage } from './ac-storage'; import { getAcStorage, saveAcStorage } from './ac-storage'; import useMinimumSpinLoading from './use-minimum-spin-loading'; const AC_SWITCH_OPEN_ICON = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/T-switch-open.png'; @@ -28,18 +28,15 @@ tripStatus: TripStatus; orderId?: string; pageDidShowVersion?: number; /** 车是否还在执行上一单 */ hasBeforeUserEndSite?: boolean; /** “上一单下车点”状态是否已由路线接口/Socket 确认 */ beforeUserEndSiteResolved?: boolean; /** 后端推送:当前被指派车辆是否仍在执行上一单(连环单) */ hasPreviousOrder?: boolean; }; export default function TemperatureControl({ tripStatus, orderId, pageDidShowVersion = 0, hasBeforeUserEndSite = false, beforeUserEndSiteResolved = false }: Props) { export default function TemperatureControl({ tripStatus, orderId, pageDidShowVersion = 0, hasPreviousOrder = false }: Props) { const [tempStatus, setTempStatus] = useState<number | null>(null); const [acSwitchOn, setAcSwitchOn] = useState(false); const [acStatusLoaded, setAcStatusLoaded] = useState(false); const tempTimerRef = useRef<any>(null); const firstInitFallbackTimerRef = useRef<any>(null); const lastConfirmedTempRef = useRef<number | null>(null); const skipQueryOnThisCycleRef = useRef(false); const userActionSeqRef = useRef(0); @@ -49,10 +46,6 @@ const TEMP_MAX = 32.5; const TEMP_UPDATE_DEBOUNCE_MS = 500; const DEFAULT_FIRST_TEMP = 23.5; // 确定性版兜底超时:正常情况下进入接驾态会很快通过路线接口/Socket 确认“上一单下车点”状态(beforeUserEndSiteResolved 置真), // 届时立即下发默认温度,无固定延迟。此超时仅为路线数据异常迟迟未达(如接口失败)时的安全兜底, // 避免空调永久不初始化;正常 happy path 不会走到这里。 const FIRST_INIT_RESOLVE_FALLBACK_MS = 3000; const isTogglingAc = acLoadingKey === 'ac'; const canToggleSwitch = tripStatus === TripStatus.VehicleApproaching && !isTogglingAc; @@ -69,15 +62,6 @@ const increaseButtonIconSuffix = increaseButtonDisabled ? '-no' : ''; const tempTextDisabledClassName = acSwitchOn ? '' : 'color-000-25'; useEffect(() => { if (tripStatus === TripStatus.VehicleApproaching) { return; } const targetOrderId = getOrderId(orderId); if (targetOrderId) { removeAcStorage(targetOrderId); } }, [tripStatus, orderId]); useEffect(() => { const skipOnce = Number(Taro.getStorageSync(MODIFY_DESTINATION_SKIP_AC_QUERY_ON_RETURN_KEY)) === 1; @@ -206,10 +190,10 @@ if (tripStatus !== TripStatus.VehicleApproaching) { return; } // 连环派单场景:车辆仍在执行上一单(hasBeforeUserEndSite 为 true)时, // 当前用户的空调卡片尚未展示,不应初始化/下发温度,否则会把车内温度强制改回默认 23.5°C, // 覆盖上一单乘客的设置。待车辆完成上一单后该副作用会重新执行并正常初始化。 if (hasBeforeUserEndSite) { // 连环派单场景:后端明确告知车辆仍在执行上一单(hasPreviousOrder 为 true)时, // 不应初始化/下发温度,否则会把车内温度强制改回默认 23.5°C,覆盖上一单乘客的设置。 // 该字段与 tripStatus 原子下发、首次即正确,无竞态窗口。 if (hasPreviousOrder) { return; } if (skipQueryOnThisCycleRef.current) { @@ -223,36 +207,12 @@ const storageData = getAcStorage(targetOrderId); if (storageData) { // 先不展示 storageData,等待校准(限定时间内接口返回 → 展示接口数据,超时 → 展示 storageData 并同步远程) handleCalibration(targetOrderId, storageData.acOpen, storageData.temperature); return; } // 首次初始化(无本地缓存):必须等到“上一单下车点”状态被确认后再决定是否下发默认温度。 // 连环派单刚接单时,订单状态消息(推进到接驾中)通常早于订单线路消息(携带上一单下车点)到达, // 存在 hasBeforeUserEndSite 暂为 false 的竞态窗口,若此刻直接下发默认 23.5°C 会覆盖上一单乘客的温度设置。 // 因此:已确认(resolved)→ 立即按 hasBeforeUserEndSite 据实初始化(普通单无固定延迟); // 未确认 → 仅挂兜底定时器,确认到达时本副作用会重跑并据实判断(命中上方 hasBeforeUserEndSite 守卫或下面的 resolved 分支)。 if (beforeUserEndSiteResolved) { handleFirstTimeInit(targetOrderId); return; } firstInitFallbackTimerRef.current = setTimeout(() => { firstInitFallbackTimerRef.current = null; // 兜底触发前若已写入空调缓存(如用户手动操作/校准落库),不再强制下发默认温度。 if (getAcStorage(targetOrderId)) { return; } handleFirstTimeInit(targetOrderId); }, FIRST_INIT_RESOLVE_FALLBACK_MS); return () => { if (firstInitFallbackTimerRef.current) { clearTimeout(firstInitFallbackTimerRef.current); firstInitFallbackTimerRef.current = null; } }; }, [tripStatus, orderId, pageDidShowVersion, hasBeforeUserEndSite, beforeUserEndSiteResolved]); handleFirstTimeInit(targetOrderId); }, [tripStatus, orderId, pageDidShowVersion, hasPreviousOrder]); useEffect(() => { lastConfirmedTempRef.current = tempStatus; @@ -378,8 +338,8 @@ } }; // 车还在上一单时不展示空调卡片 if (tripStatus !== TripStatus.VehicleApproaching || hasBeforeUserEndSite) { // 连环单(车还在执行上一单)时不展示空调卡片 if (tripStatus !== TripStatus.VehicleApproaching || hasPreviousOrder) { return null; } src/robotaxi/pages/begin-route/components/calling-car.tsx
@@ -72,8 +72,8 @@ orderId?: string; /** WebSocket 推送的上一单下车点坐标(有值表示车还在执行上一单) */ hasBeforeUserEndSite?: boolean; /** “上一单下车点”状态是否已由路线接口/Socket 确认(用于规避连环派单接单瞬间温度被重置的竞态) */ beforeUserEndSiteResolved?: boolean; /** 后端推送:当前被指派车辆是否仍在执行上一单(连环单) */ hasPreviousOrder?: boolean; orderTrunkStatus?: boolean; /** 当前页面内是否已经解锁过车门 */ doorUnlockedInCurrentPage?: boolean; @@ -123,7 +123,7 @@ vehicleInfo, orderId, hasBeforeUserEndSite = false, beforeUserEndSiteResolved = false, hasPreviousOrder = false, orderTrunkStatus, doorUnlockedInCurrentPage = false, bluetoothStatusReady = false, @@ -207,7 +207,7 @@ <View className="car-card" style={{ marginBottom: tripStatus === TripStatus.TripFinished ? '70px' : '0px' }}> <VehicleInfoCard vehicleInfo={vehicleInfo} /> <TemperatureControl tripStatus={tripStatus} orderId={orderId} pageDidShowVersion={pageDidShowVersion} hasBeforeUserEndSite={hasBeforeUserEndSite} beforeUserEndSiteResolved={beforeUserEndSiteResolved} /> <TemperatureControl tripStatus={tripStatus} orderId={orderId} pageDidShowVersion={pageDidShowVersion} hasPreviousOrder={hasPreviousOrder} /> <DoorTrunkControl tripStatus={tripStatus} src/robotaxi/pages/begin-route/hooks/use-route-manager.shared.ts
@@ -91,6 +91,8 @@ setEndSite?: (site: any) => void; /** 设置上一单乘客下车点。 */ setBeforeUserEndSite?: (site: { longitude: number; latitude: number } | null) => void; /** 设置后端推送的「是否仍在执行上一单」标识。 */ setHasPreviousOrder?: (value: boolean) => void; /** 计算顶部安全区。 */ resolveSafeTopInset: () => number; /** 计算底部视口内边距。 */ src/robotaxi/pages/begin-route/hooks/use-route-request.ts
@@ -543,6 +543,9 @@ response?.endLatitude ?? response?.endlatitude ); deps.setBeforeUserEndSite?.(beforeUserSite); const hasPreviousOrderValue = Boolean(response?.hasPreviousOrder); deps.setHasPreviousOrder?.(hasPreviousOrderValue); Taro.setStorageSync('hasPreviousOrder', { orderId: targetOrderId, value: hasPreviousOrderValue }); const rawPoints = Array.isArray(response?.polyline) ? response.polyline : []; const points = rawPoints src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx
@@ -47,8 +47,8 @@ routeDuration: number | null; /** 是否存在上一单终点 */ hasBeforeUserEndSite: boolean; /** “上一单下车点”状态是否已由路线接口/Socket 确认(用于规避连环派单接单瞬间温度被重置的竞态) */ beforeUserEndSiteResolved: boolean; /** 后端推送:当前被指派车辆是否仍在执行上一单(连环单) */ hasPreviousOrder: boolean; /** 自定义气泡图标 */ customCalloutIconSrc: string; /** 路线概览气泡背景图 */ @@ -523,7 +523,7 @@ key={deps.tripStatus} orderId={deps.orderId} hasBeforeUserEndSite={deps.hasBeforeUserEndSite} beforeUserEndSiteResolved={deps.beforeUserEndSiteResolved} hasPreviousOrder={deps.hasPreviousOrder} tripStatus={deps.tripStatus} finishCountdownEnabled={deps.finishCountdownEnabled} vehicleInfo={deps.vehicleInfo} src/robotaxi/pages/begin-route/hooks/use-web-socket-events.ts
@@ -70,6 +70,7 @@ durationSeconds: number | null; }) => void; setBeforeUserEndSite: (site: { longitude: number; latitude: number } | null) => void; setHasPreviousOrder: (value: boolean) => void; isModifyDestinationReviewVisible?: boolean; hasLocalDestinationOverride?: boolean; isPickupRouteChangeVisible?: boolean; src/robotaxi/pages/begin-route/hooks/web-socket-handlers/create-order-status-socket-handler.ts
@@ -396,6 +396,12 @@ if (!shouldKeepPickupExceptionPopup) { currentDeps.handlePickupException?.(normalizedPickupException); } const rawHasPreviousOrder = payload?.hasPreviousOrder ?? payload?.content?.hasPreviousOrder; if (rawHasPreviousOrder !== undefined) { const hasPreviousOrder = Boolean(rawHasPreviousOrder); currentDeps.setHasPreviousOrder(hasPreviousOrder); Taro.setStorageSync('hasPreviousOrder', { orderId: effectiveOrderId, value: hasPreviousOrder }); } handleCancelPopupPush(payload, changeStatusOld, currentDeps, pendingCancelContext); if (payload?.showPassengerNotice) { currentDeps.checkAndShowPassengerNotice(); src/robotaxi/pages/begin-route/index.tsx
@@ -231,12 +231,14 @@ const [tripStatus, setTripStatus] = useState<TripStatus>(toTripStatus(Taro.getStorageSync('tripStatus'))); // 当前行程状态 const [elapsedSeconds, setElapsedSeconds] = useState(0); // 等待计时秒数 const [beforeUserEndSite, setBeforeUserEndSite] = useState<BeforeUserEndSite | null>(null); // 上一单乘客下车点 const [beforeUserEndSiteResolved, setBeforeUserEndSiteResolved] = useState(false); // 当前订单的“上一单下车点”状态是否已由路线接口/Socket 确认过(用于规避连环派单接单瞬间温度被重置的竞态) // 统一写入“上一单下车点”:在更新坐标的同时标记其已被确认,确保温度模块能在确认后再决定是否下发默认温度。 const markBeforeUserEndSite = useCallback((site: BeforeUserEndSite | null) => { setBeforeUserEndSite(site); setBeforeUserEndSiteResolved(true); }, []); const [hasPreviousOrder, setHasPreviousOrder] = useState(() => { const cached = Taro.getStorageSync('hasPreviousOrder'); const cachedOrderId = String(Taro.getStorageSync('orderId') || ''); if (cached && cachedOrderId && String(cached.orderId || '') === cachedOrderId) { return Boolean(cached.value); } return false; }); // 后端推送:当前被指派车辆是否仍在执行上一单(连环单) const requoteTriggeredRef = useRef(false); // 重新报价触发标记,防止重复触发 const [callSubmitting, setCallSubmitting] = useState(false); // 呼叫车辆提交中状态 const [orderId, setOrderId] = useState<string>(''); // 订单ID @@ -603,7 +605,8 @@ endSite: effectiveEndSite, setStartSite, setEndSite, setBeforeUserEndSite: markBeforeUserEndSite, setBeforeUserEndSite, setHasPreviousOrder, updateVehicleLocationThrottled: vehicleState.updateVehicleLocationThrottled, vehicleMoveAlongPoints, setVehicleDirection, @@ -1364,7 +1367,8 @@ setRouteDuration, setModifyDestinationSocketStatus, setOrderRouteMetrics, setBeforeUserEndSite: markBeforeUserEndSite, setBeforeUserEndSite, setHasPreviousOrder, isModifyDestinationReviewVisible, hasLocalDestinationOverride, isPickupRouteChangeVisible, @@ -1416,7 +1420,6 @@ */ useEffect(() => { setDoorUnlockedInCurrentPage(false); setBeforeUserEndSiteResolved(false); // 订单切换后需重新确认“上一单下车点”状态,避免沿用上个订单的判定造成温度误重置 }, [orderId]); const { queryPaymentResult: queryOrderPaymentResult } = useOrderPaymentResultQuery({ @@ -1983,7 +1986,7 @@ routeDistance, routeDuration, hasBeforeUserEndSite: hasValidBeforeUserEndSite, beforeUserEndSiteResolved, hasPreviousOrder, customCalloutIconSrc, routeSummaryBgSrc, showTrip1StyleEndpoint,