Merge branch 'feature_v2.0' into hc/fix_xpd_626
| | |
| | | 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 { |
| | |
| | | |
| | | 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 { |
| | | // 静默失败 |
| | | } |
| | | } |
| | |
| | | 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'; |
| | |
| | | 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); |
| | |
| | | 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; |
| | |
| | | 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; |
| | |
| | | if (tripStatus !== TripStatus.VehicleApproaching) { |
| | | return; |
| | | } |
| | | // 连环派单场景:车辆仍在执行上一单(hasBeforeUserEndSite 为 true)时, |
| | | // 当前用户的空调卡片尚未展示,不应初始化/下发温度,否则会把车内温度强制改回默认 23.5°C, |
| | | // 覆盖上一单乘客的设置。待车辆完成上一单后该副作用会重新执行并正常初始化。 |
| | | if (hasBeforeUserEndSite) { |
| | | // 连环派单场景:后端明确告知车辆仍在执行上一单(hasPreviousOrder 为 true)时, |
| | | // 不应初始化/下发温度,否则会把车内温度强制改回默认 23.5°C,覆盖上一单乘客的设置。 |
| | | // 该字段与 tripStatus 原子下发、首次即正确,无竞态窗口。 |
| | | if (hasPreviousOrder) { |
| | | return; |
| | | } |
| | | if (skipQueryOnThisCycleRef.current) { |
| | |
| | | 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; |
| | |
| | | } |
| | | }; |
| | | |
| | | // 车还在上一单时不展示空调卡片 |
| | | if (tripStatus !== TripStatus.VehicleApproaching || hasBeforeUserEndSite) { |
| | | // 连环单(车还在执行上一单)时不展示空调卡片 |
| | | if (tripStatus !== TripStatus.VehicleApproaching || hasPreviousOrder) { |
| | | return null; |
| | | } |
| | | |
| | |
| | | orderId?: string; |
| | | /** WebSocket 推送的上一单下车点坐标(有值表示车还在执行上一单) */ |
| | | hasBeforeUserEndSite?: boolean; |
| | | /** “上一单下车点”状态是否已由路线接口/Socket 确认(用于规避连环派单接单瞬间温度被重置的竞态) */ |
| | | beforeUserEndSiteResolved?: boolean; |
| | | /** 后端推送:当前被指派车辆是否仍在执行上一单(连环单) */ |
| | | hasPreviousOrder?: boolean; |
| | | orderTrunkStatus?: boolean; |
| | | /** 当前页面内是否已经解锁过车门 */ |
| | | doorUnlockedInCurrentPage?: boolean; |
| | |
| | | vehicleInfo, |
| | | orderId, |
| | | hasBeforeUserEndSite = false, |
| | | beforeUserEndSiteResolved = false, |
| | | hasPreviousOrder = false, |
| | | orderTrunkStatus, |
| | | doorUnlockedInCurrentPage = false, |
| | | bluetoothStatusReady = false, |
| | |
| | | <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} |
| | |
| | | import Taro from '@tarojs/taro'; |
| | | import { useState, useRef, useCallback, useMemo, useEffect } from 'react'; |
| | | import { computeAdaptiveBottomPadding } from '../../../../utils/Robotaxi-public-method'; |
| | | import { computeAdaptiveBottomPadding, debounce } from '../../../../utils/Robotaxi-public-method'; |
| | | import { TripStatus } from '../../../constants/trip-status'; |
| | | import { buildActiveTripSafeAreaMetrics } from './active-trip-camera-policy'; |
| | | |
| | |
| | | const ACTIVE_TRIP_VIEWPORT_SYNC_INTERVAL_MS = 600; // 行程安全区跟随的最小同步间隔,避免地图频繁漂移 |
| | | const ACTIVE_TRIP_VIEWPORT_SYNC_ANIMATION_MS = 300; // 跟车中心移动缓动时长,等待原生地图动画后再回写受控中心点 |
| | | const ORDER_DETAIL_SCALE_SYNC_DELAY_MS = 600; // 完成态 includePoints 后等待原生地图落稳,再同步实际 scale |
| | | const VIEWPORT_FIT_DEBOUNCE_MS = 300; // 视口 fit 统一防抖:状态切换瞬间路线点/视口类型会先残留旧值再被正确值覆盖, |
| | | // 防抖把窗口内的多次 includePoints/moveToLocation 合并为最后一次,避免连续命令式调用动画相互打断导致缩放卡死 |
| | | export const ACTIVE_TRIP_VIEWPORT_SAFE_X_MARGIN_RATIO = 0.2; // 横向两侧保留的边距比例,调大可让车辆更早触发横向回中,避免跑到贴边只剩一小段线路 |
| | | |
| | | type ViewportPoint = { |
| | |
| | | }, ORDER_DETAIL_SCALE_SYNC_DELAY_MS); |
| | | }, [deps.setLatitude, deps.setLongitude, deps.tripStatus, getMapCenterLocation, getMapScale]); |
| | | |
| | | const updateViewportByPayload = (viewportType: string, points: any[]) => { |
| | | const updateViewportByPayloadRaw = (viewportType: string, points: any[]) => { |
| | | const mapCtx = Taro.createMapContext('map'); |
| | | const bottomPadding = computeBottomPadding(points, viewportType, deps.tripStatus); |
| | | const baseTopPadding = viewportType === 'order_detail' ? 60 : 24; |
| | |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | // 防抖包裹:状态切换瞬间多次连续调用只执行最后一次,避免 includePoints 动画相互打断 |
| | | const updateViewportByPayload = debounce(updateViewportByPayloadRaw, VIEWPORT_FIT_DEBOUNCE_MS); |
| | | |
| | | const toOrigin = () => { |
| | | const mapCtx = Taro.createMapContext('map'); |
| | |
| | | if (deps.tripStatus === TripStatus.InService && viewportType === 'route') { |
| | | tripStatus6ViewportLockedRef.current = true; |
| | | } |
| | | // updateViewportByPayload 已用 debounce 包裹:状态切换瞬间多次调用只执行最后一次 |
| | | updateViewportByPayload(viewportType, points); |
| | | if ([TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(deps.tripStatus) && viewportType === 'order_detail') { |
| | | const hasOrderDetailRouteAfterEnter = orderDetailRouteUpdatedAfterEnterRef.current && deps.routePoints.length > 0; |
| | |
| | | setEndSite?: (site: any) => void; |
| | | /** 设置上一单乘客下车点。 */ |
| | | setBeforeUserEndSite?: (site: { longitude: number; latitude: number } | null) => void; |
| | | /** 设置后端推送的「是否仍在执行上一单」标识。 */ |
| | | setHasPreviousOrder?: (value: boolean) => void; |
| | | /** 计算顶部安全区。 */ |
| | | resolveSafeTopInset: () => number; |
| | | /** 计算底部视口内边距。 */ |
| | |
| | | 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 |
| | |
| | | import Taro from '@tarojs/taro'; |
| | | import { memo, useCallback, useMemo } from 'react'; |
| | | import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; |
| | | import { CoverImage, CoverView, Image, View } from '@tarojs/components'; |
| | | import ReleaseRoute from '../components/release-route'; |
| | | import ReleaseBeBeing from '../components/release-be-being'; |
| | |
| | | routeDuration: number | null; |
| | | /** 是否存在上一单终点 */ |
| | | hasBeforeUserEndSite: boolean; |
| | | /** “上一单下车点”状态是否已由路线接口/Socket 确认(用于规避连环派单接单瞬间温度被重置的竞态) */ |
| | | beforeUserEndSiteResolved: boolean; |
| | | /** 后端推送:当前被指派车辆是否仍在执行上一单(连环单) */ |
| | | hasPreviousOrder: boolean; |
| | | /** 自定义气泡图标 */ |
| | | customCalloutIconSrc: string; |
| | | /** 路线概览气泡背景图 */ |
| | |
| | | ), [deps.hasBeforeUserEndSite, deps.routeDistance, vehicleMetricDurationDisplay]); |
| | | |
| | | /** |
| | | * 安卓接驾态(VehicleApproaching)上车点气泡(customCallout)偶现不绘制,需拖动/缩放/点击后才出现。 |
| | | * 在 markers 更新后微调上车点气泡外层(透明容器,无可见样式)的 borderRadius, |
| | | * 触发原生地图对该 cover-view 气泡的回流重绘。仅作用在上车点气泡,不影响其它气泡。 |
| | | */ |
| | | const [calloutRepaintStyle, setCalloutRepaintStyle] = useState<{ borderRadius: string }>({ borderRadius: '0px' }); |
| | | const calloutRepaintTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null); |
| | | const markersRepaintSignature = useMemo( |
| | | () => deps.markers.map((marker: any) => `${marker.id}-${marker.name || ''}`).join('|'), |
| | | [deps.markers] |
| | | ); |
| | | useEffect(() => { |
| | | if (calloutRepaintTimerRef.current) { |
| | | clearTimeout(calloutRepaintTimerRef.current); |
| | | } |
| | | calloutRepaintTimerRef.current = setTimeout(() => { |
| | | setCalloutRepaintStyle((prev) => { |
| | | const next = prev.borderRadius === '0px' ? '0.5px' : '0px'; |
| | | return { borderRadius: next }; |
| | | }); |
| | | calloutRepaintTimerRef.current = null; |
| | | }, 800); |
| | | return () => { |
| | | if (calloutRepaintTimerRef.current) { |
| | | clearTimeout(calloutRepaintTimerRef.current); |
| | | calloutRepaintTimerRef.current = null; |
| | | } |
| | | }; |
| | | }, [markersRepaintSignature, deps.tripStatus]); |
| | | |
| | | /** |
| | | * 渲染等待态气泡。 |
| | | */ |
| | | const renderWaitingCallout = useCallback((item: any, calloutKey: string) => ( |
| | |
| | | * 渲染默认气泡。 |
| | | */ |
| | | const renderDefaultCallout = useCallback((item: any, calloutKey: string) => ( |
| | | <CoverView slot='callout' marker-id={item.id} key={calloutKey} className='custom-callout'> |
| | | <CoverView slot='callout' marker-id={item.id} key={calloutKey} className='custom-callout' style={item.id === 1 ? calloutRepaintStyle : undefined}> |
| | | {item.isRouteSummaryBubble ? ( |
| | | routeSummaryText ? ( |
| | | <CoverView className='route-summary-callout'> |
| | |
| | | deps.tripStatus, |
| | | routeSummaryText, |
| | | vehicleMetricDurationDisplay, |
| | | shouldShowVehicleMetricCallout |
| | | shouldShowVehicleMetricCallout, |
| | | calloutRepaintStyle |
| | | ]); |
| | | |
| | | /** |
| | |
| | | key={deps.tripStatus} |
| | | orderId={deps.orderId} |
| | | hasBeforeUserEndSite={deps.hasBeforeUserEndSite} |
| | | beforeUserEndSiteResolved={deps.beforeUserEndSiteResolved} |
| | | hasPreviousOrder={deps.hasPreviousOrder} |
| | | tripStatus={deps.tripStatus} |
| | | finishCountdownEnabled={deps.finishCountdownEnabled} |
| | | vehicleInfo={deps.vehicleInfo} |
| | |
| | | durationSeconds: number | null; |
| | | }) => void; |
| | | setBeforeUserEndSite: (site: { longitude: number; latitude: number } | null) => void; |
| | | setHasPreviousOrder: (value: boolean) => void; |
| | | isModifyDestinationReviewVisible?: boolean; |
| | | hasLocalDestinationOverride?: boolean; |
| | | isPickupRouteChangeVisible?: boolean; |
| | |
| | | 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(); |
| | |
| | | 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 |
| | |
| | | endSite: effectiveEndSite, |
| | | setStartSite, |
| | | setEndSite, |
| | | setBeforeUserEndSite: markBeforeUserEndSite, |
| | | setBeforeUserEndSite, |
| | | setHasPreviousOrder, |
| | | updateVehicleLocationThrottled: vehicleState.updateVehicleLocationThrottled, |
| | | vehicleMoveAlongPoints, |
| | | setVehicleDirection, |
| | |
| | | setRouteDuration, |
| | | setModifyDestinationSocketStatus, |
| | | setOrderRouteMetrics, |
| | | setBeforeUserEndSite: markBeforeUserEndSite, |
| | | setBeforeUserEndSite, |
| | | setHasPreviousOrder, |
| | | isModifyDestinationReviewVisible, |
| | | hasLocalDestinationOverride, |
| | | isPickupRouteChangeVisible, |
| | |
| | | */ |
| | | useEffect(() => { |
| | | setDoorUnlockedInCurrentPage(false); |
| | | setBeforeUserEndSiteResolved(false); // 订单切换后需重新确认“上一单下车点”状态,避免沿用上个订单的判定造成温度误重置 |
| | | }, [orderId]); |
| | | |
| | | const { queryPaymentResult: queryOrderPaymentResult } = useOrderPaymentResultQuery({ |
| | |
| | | routeDistance, |
| | | routeDuration, |
| | | hasBeforeUserEndSite: hasValidBeforeUserEndSite, |
| | | beforeUserEndSiteResolved, |
| | | hasPreviousOrder, |
| | | customCalloutIconSrc, |
| | | routeSummaryBgSrc, |
| | | showTrip1StyleEndpoint, |
| | |
| | | const ACTIVE_TRIP_VIEWPORT_SYNC_INTERVAL_MS = 600; |
| | | const ACTIVE_TRIP_VIEWPORT_SYNC_ANIMATION_MS = 300; |
| | | const ACTIVE_TRIP_VIEWPORT_SAFE_X_MARGIN_RATIO = 0.08; |
| | | const USER_GESTURE_HOLD_MS = 8000; |
| | | const USER_GESTURE_CAUSED_BY = new Set(["gesture", "drag", "scale"]); |
| | | |
| | | const buildSiteViewportPoint = (site: any) => { |
| | | if (!site?.latitude || !site?.longitude) { |
| | |
| | | : mapBottomPadding; |
| | | return computeAdaptiveBottomPadding(points, basePadding); |
| | | }, [mapBottomPadding]); |
| | | |
| | | /** |
| | | * 用户手势拖拽/缩放地图后,暂停一段时间的自动视口同步,避免地图被立刻抢回。 |
| | | */ |
| | | const handleRegionChange = useCallback((event: any) => { |
| | | const causedBy = String(event?.causedBy || event?.detail?.causedBy || "").toLowerCase(); |
| | | if (!USER_GESTURE_CAUSED_BY.has(causedBy)) { |
| | | return; |
| | | } |
| | | holdDynamicScale(USER_GESTURE_HOLD_MS); |
| | | }, [holdDynamicScale]); |
| | | |
| | | const updateMapViewportPadding = useCallback(() => { |
| | | Taro.nextTick(() => { |
| | |
| | | }); |
| | | }, [computeBottomPadding, holdDynamicScale, mapBottomPadding]); |
| | | |
| | | /** |
| | | * 接驾中/行程中复位地图时,优先把整条线路完整放回屏幕,而不是只把中心点拉回车辆位置。 |
| | | */ |
| | | const resetActiveTripViewportToRoute = useCallback(() => { |
| | | const { pickupSite, dropoffSite } = resolveViewportSites(); |
| | | const { points } = buildViewportPayload("route", pickupSite, dropoffSite); |
| | | if (points.length === 0) { |
| | | return; |
| | | } |
| | | lastActiveTripViewportSyncAtRef.current = Date.now(); |
| | | updateViewportByPayload("route", points); |
| | | }, [ |
| | | deps.endSite, |
| | | deps.routePoints, |
| | | deps.startSite, |
| | | deps.tripStatus, |
| | | updateViewportByPayload |
| | | ]); |
| | | |
| | | const syncActiveTripViewportToVehicle = useCallback(() => { |
| | | if (![TripStatus.VehicleApproaching, TripStatus.InService].includes(deps.tripStatus)) { |
| | | return; |
| | |
| | | if (!hasLatitudeDelta && !hasLongitudeDelta) { |
| | | return; |
| | | } |
| | | lastActiveTripViewportSyncAtRef.current = Date.now(); |
| | | const mapCtx = Taro.createMapContext("map"); |
| | | mapCtx.moveToLocation({ |
| | | latitude: desiredCenterLatitude, |
| | | longitude: desiredCenterLongitude |
| | | }); |
| | | if (activeTripViewportSyncTimerRef.current) { |
| | | clearTimeout(activeTripViewportSyncTimerRef.current); |
| | | } |
| | | activeTripViewportSyncTimerRef.current = setTimeout(() => { |
| | | if (typeof deps.setLatitude === "function") { |
| | | deps.setLatitude(desiredCenterLatitude); |
| | | } |
| | | if (typeof deps.setLongitude === "function") { |
| | | deps.setLongitude(desiredCenterLongitude); |
| | | } |
| | | activeTripViewportSyncTimerRef.current = null; |
| | | }, ACTIVE_TRIP_VIEWPORT_SYNC_ANIMATION_MS); |
| | | resetActiveTripViewportToRoute(); |
| | | }) |
| | | .finally(() => { |
| | | activeTripViewportSyncingRef.current = false; |
| | |
| | | ensureMapRect, |
| | | getDynamicScaleHoldUntil, |
| | | getMapCenterLocation, |
| | | getMapRegion |
| | | getMapRegion, |
| | | resetActiveTripViewportToRoute |
| | | ]); |
| | | |
| | | useEffect(() => { |
| | |
| | | mapBottomPadding, |
| | | updateMapViewportPadding, |
| | | resolveSafeTopInset, |
| | | computeBottomPadding |
| | | computeBottomPadding, |
| | | handleRegionChange |
| | | }; |
| | | } |
| | |
| | | import Taro, { useDidShow, useRouter } from '@tarojs/taro'; |
| | | import { useEffect, useMemo, useRef, useState } from 'react'; |
| | | import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; |
| | | import './index.less'; |
| | | import useMapMarkers from './hooks/use-map-markers'; |
| | | import useVehicleTracking from './hooks/use-vehicle-tracking'; |
| | |
| | | updateVehicleLocationThrottled, |
| | | }); |
| | | |
| | | const { mapScale, updateMapViewportPadding } = useMapViewport({ |
| | | const { |
| | | mapScale, |
| | | updateMapViewportPadding, |
| | | handleRegionChange: handleViewportRegionChange |
| | | } = useMapViewport({ |
| | | tripStatus, |
| | | statusRule, |
| | | routePoints, |
| | |
| | | longitude, |
| | | latitude, |
| | | }); // Android 地图浮层 |
| | | const handleMapRegionChange = useCallback((event: any) => { |
| | | handleViewportRegionChange(event); |
| | | handleRegionChange(event); |
| | | }, [handleRegionChange, handleViewportRegionChange]); |
| | | const markersWithRouteSummary = useMemo(() => { |
| | | if (!routeSummaryMarker) { |
| | | return markers; |
| | |
| | | polyline={polyline} |
| | | routeDuration={displayRouteDuration} |
| | | renderMapOverlays={renderMapOverlays} |
| | | handleMapRegionChange={handleRegionChange} |
| | | handleMapRegionChange={handleMapRegionChange} |
| | | handleMapPolylineTap={handlePolylineTap} |
| | | handleMapTap={handleMapTap} |
| | | goHome={goHome} |