feat(robotaxi): 添加已行驶路线缓存, 优化地图动态缩放并修复逻辑问题
- 新增已行驶路线缓存模块,支持持久化与恢复车辆已行驶的路线点数据
- 重构 useActiveTripScale 钩子,采用车速缩放档位+平滑滞回逻辑实现更自然的地图缩放
- 修复个人中心优惠券API数据获取错误以及socket路线的速度单位转换问题
- 优化定位流程,新增forceNearestSite选项并调整活跃订单上车点处理逻辑
- 更新折线渲染依赖项,在页面返回和路线重置时清理已行驶路线缓存
- 移除useActiveTripScale中未使用的routePoints依赖项
| | |
| | | import Taro from '@tarojs/taro'; |
| | | import wsManager from '../../../../utils/websocket'; |
| | | import { mapOrderStatusToTripStatus } from '../../../../utils/Robotaxi-public-method'; |
| | | import { TripStatus, toTripStatus } from '../../../../robotaxi/pages/begin-route/constants/trip-status'; |
| | | import { SocketOrderStatus, TripStatus, toTripStatus } from '../../../../robotaxi/pages/begin-route/constants/trip-status'; |
| | | import { |
| | | clearHomeStartSiteDisplayState, |
| | | HOME_START_SITE_SOURCE_ACTIVE_ORDER, |
| | |
| | | hasActiveOrder: boolean; |
| | | /** 当前首页确认上车点。 */ |
| | | preferredStartSite: any; |
| | | /** 当前是否应按用户位置吸附最近站点。 */ |
| | | shouldUseNearestSiteByOrder: boolean; |
| | | }; |
| | | |
| | | type GetLocationInfoOptions = { |
| | | /** 是否更新地图中心。 */ |
| | | updateCenter?: boolean; |
| | | /** 是否强制刷新城市信息。 */ |
| | | forceCityRefresh?: boolean; |
| | | /** 是否强制按用户位置吸附最近站点。 */ |
| | | forceNearestSite?: boolean; |
| | | }; |
| | | |
| | | type Deps = { |
| | |
| | | /** 设置经度 */ |
| | | setLongitude: (value: number) => void; |
| | | /** 读取定位并更新站点 */ |
| | | getLocationInfo: (updateCenter?: boolean, forceCityRefresh?: boolean) => void; |
| | | getLocationInfo: (options?: GetLocationInfoOptions) => void; |
| | | /** 处理切城标记 */ |
| | | consumeCitySwitchFlag: () => { switched: boolean; shouldMoveToCityCenter: boolean }; |
| | | /** 跳过下一次居中 */ |
| | |
| | | }; |
| | | |
| | | /** |
| | | * 判断当前活跃订单是否应按用户位置吸附最近站点。 |
| | | */ |
| | | const shouldUseNearestSiteByActiveOrderStatus = (status: unknown) => ( |
| | | Number(status) >= SocketOrderStatus.PendingDeparture |
| | | ); |
| | | |
| | | /** |
| | | * 将活跃订单起点映射为首页缓存站点。 |
| | | */ |
| | | const buildStartSiteByActiveOrder = (activeOrder: any) => ({ |
| | |
| | | deps.setNeedLogin(!otp); |
| | | if (!otp) { |
| | | deps.setPreferredStartSite(null); |
| | | return { hasLogin: false, hasActiveOrder: false, preferredStartSite: null } as AuthOrderSyncResult; |
| | | return { |
| | | hasLogin: false, |
| | | hasActiveOrder: false, |
| | | preferredStartSite: null, |
| | | shouldUseNearestSiteByOrder: false |
| | | } as AuthOrderSyncResult; |
| | | } |
| | | wsManager.connect(deps.wsHost); |
| | | const agreement = await deps.getAgreement(); |
| | |
| | | deps.setTripStatus(0); |
| | | deps.setOrderData({}); |
| | | deps.setPreferredStartSite(preferredStartSite); |
| | | return { hasLogin: true, hasActiveOrder: false, preferredStartSite } as AuthOrderSyncResult; |
| | | return { |
| | | hasLogin: true, |
| | | hasActiveOrder: false, |
| | | preferredStartSite, |
| | | shouldUseNearestSiteByOrder: false |
| | | } as AuthOrderSyncResult; |
| | | } |
| | | deps.setOrderData(activeOrder || {}); |
| | | const mappedStatus = toHomeTripStatus(mapOrderStatusToTripStatus(activeOrder.status || 0)); |
| | | const startSite = buildStartSiteByActiveOrder(activeOrder); |
| | | const endSite = buildEndSiteByActiveOrder(activeOrder); |
| | | const preferredState = readHomeStartSiteDisplayState(); |
| | | const shouldUseNearestSiteByOrder = shouldUseNearestSiteByActiveOrderStatus(activeOrder.status); |
| | | const manualSelectedStartSite = isManualSelectedHomeStartSiteDisplayState(preferredState) |
| | | ? resolveHomeStartSiteDisplaySite() |
| | | : null; // 手动选择的上车点优先用于首页回显,避免返回首页时被订单起点覆盖 |
| | | const preferredStartSite = manualSelectedStartSite || startSite; |
| | | const preferredStartSite = shouldUseNearestSiteByOrder ? null : (manualSelectedStartSite || startSite); |
| | | deps.setTripStatus(mappedStatus); |
| | | Taro.setStorageSync('tripStatus', mappedStatus); |
| | | Taro.setStorageSync('orderId', activeOrder.id || ''); |
| | | Taro.setStorageSync('startSite', preferredStartSite); |
| | | Taro.setStorageSync('endSite', endSite); |
| | | if (!manualSelectedStartSite) { |
| | | if (shouldUseNearestSiteByOrder) { |
| | | Taro.removeStorageSync('startSite'); |
| | | } else { |
| | | Taro.setStorageSync('startSite', preferredStartSite); |
| | | } |
| | | if (shouldUseNearestSiteByOrder && preferredState?.source === HOME_START_SITE_SOURCE_ACTIVE_ORDER) { |
| | | clearHomeStartSiteDisplayState(); |
| | | } |
| | | if (!shouldUseNearestSiteByOrder && !manualSelectedStartSite) { |
| | | writeHomeStartSiteDisplayState(startSite, HOME_START_SITE_SOURCE_ACTIVE_ORDER); |
| | | } |
| | | deps.setPreferredStartSite(preferredStartSite); |
| | | return { hasLogin: true, hasActiveOrder: true, preferredStartSite } as AuthOrderSyncResult; |
| | | return { |
| | | hasLogin: true, |
| | | hasActiveOrder: true, |
| | | preferredStartSite, |
| | | shouldUseNearestSiteByOrder |
| | | } as AuthOrderSyncResult; |
| | | }; |
| | | |
| | | /** |
| | | * 处理定位与首屏地图状态。 |
| | | */ |
| | | const processLocationSetting = async (auth: any, startSite: any) => { |
| | | const processLocationSetting = async (auth: any, startSite: any, shouldUseNearestSiteByOrder = false) => { |
| | | const hasStartSite = startSite && startSite.latitude && startSite.longitude; |
| | | deps.setIsLocationAuthDenied(auth === false); |
| | | const switchState = deps.consumeCitySwitchFlag(); |
| | |
| | | deps.setLongitude(deps.defaultLongitude); |
| | | return; |
| | | } |
| | | if (shouldUseNearestSiteByOrder) { |
| | | deps.getLocationInfo({ |
| | | updateCenter: true, |
| | | forceCityRefresh: true, |
| | | forceNearestSite: true |
| | | }); |
| | | return; |
| | | } |
| | | if (auth === undefined) { |
| | | deps.getLocationInfo(); |
| | | return; |
| | |
| | | deps.skipNextLocationCenterRef.current = false; |
| | | const hasEntered = Taro.getStorageSync(deps.firstEnterKey) === 'true'; |
| | | if (!hasEntered) { |
| | | deps.getLocationInfo(true); |
| | | deps.getLocationInfo({ updateCenter: true }); |
| | | Taro.setStorageSync(deps.firstEnterKey, 'true'); |
| | | return; |
| | | } |
| | | deps.getLocationInfo(shouldUpdateCenter); |
| | | if (!hasStartSite && auth === true) { |
| | | deps.getLocationInfo({ updateCenter: shouldUpdateCenter }); |
| | | if (!hasStartSite && auth === true && !shouldUseNearestSiteByOrder) { |
| | | await deps.toOrigin(); |
| | | } |
| | | }; |
| | |
| | | Promise.resolve(authSyncTask) |
| | | .then((syncResult) => { |
| | | const startSite = syncResult.preferredStartSite || Taro.getStorageSync('startSite'); |
| | | processLocationSetting(auth, startSite); |
| | | processLocationSetting(auth, startSite, syncResult.shouldUseNearestSiteByOrder); |
| | | }) |
| | | .catch(() => { |
| | | processLocationSetting(auth, null); |
| | |
| | | onLocationSettled: (lat: number, lng: number, isInside: boolean) => void; |
| | | }; |
| | | |
| | | type GetLocationInfoOptions = { |
| | | /** 是否更新地图中心。 */ |
| | | updateCenter?: boolean; |
| | | /** 是否强制刷新城市信息。 */ |
| | | forceCityRefresh?: boolean; |
| | | /** 是否强制按用户位置吸附最近站点。 */ |
| | | forceNearestSite?: boolean; |
| | | }; |
| | | |
| | | /** |
| | | * 定位流程编排:不持有状态,仅组合 map/site/city 状态 hook 完成定位主链路。 |
| | | */ |
| | |
| | | /** |
| | | * 定位入口:定位 -> 城市就绪 -> 地图数据刷新 -> 吸附判断 -> 地址兜底。 |
| | | */ |
| | | const getLocationInfo = (updateCenter = true, forceCityRefresh = false) => { |
| | | const getLocationInfo = (options: GetLocationInfoOptions = {}) => { |
| | | const { |
| | | updateCenter = true, |
| | | forceCityRefresh = false, |
| | | forceNearestSite = false |
| | | } = options; |
| | | Taro.getLocation({ type: 'gcj02' }) |
| | | .then(async (res) => { |
| | | mapState.setIsLocationAuthDenied(false); |
| | |
| | | ); |
| | | if (cityInfo?.code) { |
| | | const { isInside, newMarkers, sites } = await mapState.updateMapData(res.latitude, res.longitude, cityInfo.code); |
| | | const lockedManualStartSite = resolveLockedManualStartSite(cityInfo.code); |
| | | const lockedManualStartSite = forceNearestSite ? null : resolveLockedManualStartSite(cityInfo.code); |
| | | const nearestCandidates = sites.length > 0 ? sites : newMarkers; |
| | | const { adsorbed, marker } = lockedManualStartSite |
| | | ? { adsorbed: false, marker: null } |
| | |
| | | /** 设置地图缩放 */ |
| | | setMapScale: (v: number) => void; |
| | | /** 主动定位 */ |
| | | getLocationInfo: (updateCenter?: boolean, forceCityRefresh?: boolean) => void; |
| | | getLocationInfo: (options?: { |
| | | /** 是否更新地图中心。 */ |
| | | updateCenter?: boolean; |
| | | /** 是否强制刷新城市信息。 */ |
| | | forceCityRefresh?: boolean; |
| | | /** 是否强制按用户位置吸附最近站点。 */ |
| | | forceNearestSite?: boolean; |
| | | }) => void; |
| | | /** 查询指针吸附 */ |
| | | queryPointerByCenter: (lat: number, lng: number) => Promise<void>; |
| | | }; |
| | |
| | | if (res.authSetting['scope.userLocation']) { |
| | | deps.setIsLocationAuthDenied(false); |
| | | deps.setMapScale(14); |
| | | deps.getLocationInfo(true, true); |
| | | deps.getLocationInfo({ updateCenter: true, forceCityRefresh: true }); |
| | | } |
| | | } |
| | | }); |
| New file |
| | |
| | | import Taro from '@tarojs/taro'; |
| | | |
| | | /** 已走过路线缓存键。 */ |
| | | export const PASSED_ROUTE_CACHE_KEY = 'beginRoutePassedRouteCache'; |
| | | |
| | | /** 已走过路线缓存结构。 */ |
| | | export type PassedRouteCacheState = { |
| | | /** 订单 ID。 */ |
| | | orderId: string; |
| | | /** 已走过路线点。 */ |
| | | points: Array<{ latitude: number; longitude: number }>; |
| | | /** 最近更新时间。 */ |
| | | updatedAt: number; |
| | | }; |
| | | |
| | | /** |
| | | * 归一化已走过路线点,仅保留有效经纬度。 |
| | | */ |
| | | const normalizePassedRoutePoints = (points: any[] = []) => { |
| | | return points |
| | | .map((point) => ({ |
| | | latitude: Number(point?.latitude), |
| | | longitude: Number(point?.longitude) |
| | | })) |
| | | .filter((point) => Number.isFinite(point.latitude) && Number.isFinite(point.longitude)); |
| | | }; |
| | | |
| | | /** |
| | | * 清理已走过路线缓存。 |
| | | */ |
| | | export const clearPassedRouteCache = () => { |
| | | Taro.removeStorageSync(PASSED_ROUTE_CACHE_KEY); |
| | | }; |
| | | |
| | | /** |
| | | * 写入已走过路线缓存。 |
| | | */ |
| | | export const writePassedRouteCache = (orderId: string, points: any[]) => { |
| | | const normalizedOrderId = String(orderId || ''); |
| | | const normalizedPoints = normalizePassedRoutePoints(points); |
| | | if (!normalizedOrderId || normalizedPoints.length <= 1) { |
| | | return; |
| | | } |
| | | const nextState: PassedRouteCacheState = { |
| | | orderId: normalizedOrderId, |
| | | points: normalizedPoints, |
| | | updatedAt: Date.now() |
| | | }; |
| | | Taro.setStorageSync(PASSED_ROUTE_CACHE_KEY, nextState); |
| | | }; |
| | | |
| | | /** |
| | | * 读取当前订单对应的已走过路线缓存。 |
| | | */ |
| | | export const readPassedRouteCache = (orderId?: string) => { |
| | | const cachedState = Taro.getStorageSync(PASSED_ROUTE_CACHE_KEY) as PassedRouteCacheState | null; |
| | | if (!cachedState || !Array.isArray(cachedState.points)) { |
| | | return []; |
| | | } |
| | | const normalizedOrderId = String(orderId || ''); |
| | | if (normalizedOrderId && String(cachedState.orderId || '') !== normalizedOrderId) { |
| | | return []; |
| | | } |
| | | return normalizePassedRoutePoints(cachedState.points); |
| | | }; |
| | |
| | | export function normalizeSocketRouteSnapshot(payload: any): RouteSnapshot { |
| | | const route = safeParse(payload?.content?.route, {}); |
| | | const timestamp = Number(payload?.data?.timestamp); |
| | | const speedMps = Number(route?.speed); |
| | | const speedKmh = Number(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(speedMps) ? speedMps * 3.6 : null, |
| | | speedKmh: Number.isFinite(speedKmh) ? speedKmh : null, |
| | | timestamp: Number.isFinite(timestamp) ? timestamp : null, |
| | | vehicleMoveAlongPoints: normalizeRoutePoints(payload?.servedPoints), |
| | | forceFullRedraw: payload?.forceFullRedraw === true, |
| | |
| | | |
| | | type Deps = { |
| | | tripStatus: TripStatus; |
| | | routePoints: RoutePoint[]; |
| | | speedKmh: number | null; |
| | | mapScale: number; |
| | | setMapScale: (value: number) => void; |
| | | syncActiveTripViewportToVehicle: () => void; |
| | | computeBottomPadding: (points: any[], viewportType: string) => number; |
| | | resolveSafeTopInset: () => number; |
| | | holdDynamicScale: (durationMs?: number) => void; |
| | |
| | | const USER_GESTURE_HOLD_MS = 3000; // 用户手势后暂停速度缩放时长 |
| | | const ORDER_UPDATE_LINE_HOLD_MS = 2500; // ORDER_UPDATE_LINE 全路线入屏后暂停速度缩放时长 |
| | | const SCALE_RESUME_BUFFER_MS = 80; // 冷却窗口结束后的恢复缓冲时间 |
| | | const LOW_SPEED_KMH = 0; // 低速缩放区间起点 |
| | | const HIGH_SPEED_KMH = 60; // 高速缩放区间终点 |
| | | const MIN_DYNAMIC_SCALE = 14; // 高速时允许的最远视角比例尺 |
| | | const MAX_DYNAMIC_SCALE = 18; // 低速时允许的最近视角比例尺 |
| | | const SCALE_STEP = 0.5; // 比例尺离散步进,避免频繁抖动 |
| | | const MIN_SCALE_CHANGE = 0.25; // 触发 setMapScale 的最小比例尺变化 |
| | | const SPEED_SMOOTHING_SAMPLE_SIZE = 3; // 速度平滑采样数量 |
| | | const SCALE_BAND_HYSTERESIS_KMH = 4; // 档位切换滞回,避免边界抖动 |
| | | const MAX_SCALE_STEP_PER_TICK = 1; // 单次缩放最大变化,避免突然大幅跳变 |
| | | 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 顶部安全区附加留白 |
| | | |
| | | type ScaleBand = { |
| | | minSpeedKmh: number; |
| | | maxSpeedKmh: number; |
| | | scale: number; |
| | | }; |
| | | |
| | | const DYNAMIC_SCALE_BANDS: ScaleBand[] = [ |
| | | { minSpeedKmh: 0, maxSpeedKmh: 10, scale: 18 }, |
| | | { minSpeedKmh: 10, maxSpeedKmh: 20, scale: 17.5 }, |
| | | { minSpeedKmh: 20, maxSpeedKmh: 30, scale: 17 }, |
| | | { minSpeedKmh: 30, maxSpeedKmh: 40, scale: 16 }, |
| | | { minSpeedKmh: 40, maxSpeedKmh: 50, scale: 15.5 }, |
| | | { minSpeedKmh: 50, maxSpeedKmh: 60, scale: 15 }, |
| | | { minSpeedKmh: 60, maxSpeedKmh: Number.POSITIVE_INFINITY, scale: 14 } |
| | | ]; // 基于车速的动态缩放档位,覆盖低速精细、中速平衡、高速宏观三类视角 |
| | | |
| | | /** |
| | | * 将数值限制在给定区间内。 |
| | |
| | | }; |
| | | |
| | | /** |
| | | * 根据速度命中所属缩放档位。 |
| | | */ |
| | | const resolveScaleBandIndex = (speedKmh: number) => { |
| | | const safeSpeed = Math.max(0, Number(speedKmh) || 0); |
| | | const bandIndex = DYNAMIC_SCALE_BANDS.findIndex((band) => safeSpeed >= band.minSpeedKmh && safeSpeed < band.maxSpeedKmh); |
| | | return bandIndex >= 0 ? bandIndex : DYNAMIC_SCALE_BANDS.length - 1; |
| | | }; |
| | | |
| | | /** |
| | | * 基于上一次档位应用滞回,避免速度在边界附近频繁切换。 |
| | | */ |
| | | const resolveStableScaleBandIndex = (speedKmh: number, previousBandIndex: number | null) => { |
| | | const targetBandIndex = resolveScaleBandIndex(speedKmh); |
| | | if (previousBandIndex === null || previousBandIndex === targetBandIndex) { |
| | | return targetBandIndex; |
| | | } |
| | | let nextBandIndex = previousBandIndex; |
| | | if (targetBandIndex > previousBandIndex) { |
| | | while (nextBandIndex < targetBandIndex) { |
| | | const fasterBand = DYNAMIC_SCALE_BANDS[nextBandIndex + 1]; |
| | | if (speedKmh >= fasterBand.minSpeedKmh + SCALE_BAND_HYSTERESIS_KMH) { |
| | | nextBandIndex += 1; |
| | | continue; |
| | | } |
| | | break; |
| | | } |
| | | return nextBandIndex; |
| | | } |
| | | while (nextBandIndex > targetBandIndex) { |
| | | const currentBand = DYNAMIC_SCALE_BANDS[nextBandIndex]; |
| | | if (speedKmh < currentBand.minSpeedKmh - SCALE_BAND_HYSTERESIS_KMH) { |
| | | nextBandIndex -= 1; |
| | | continue; |
| | | } |
| | | break; |
| | | } |
| | | return nextBandIndex; |
| | | }; |
| | | |
| | | /** |
| | | * 对最近几帧速度做均值平滑,降低单帧抖动对缩放的影响。 |
| | | */ |
| | | const resolveSmoothedSpeed = (samples: number[], nextSpeedKmh: number) => { |
| | | const nextSamples = [...samples, Math.max(0, nextSpeedKmh)].slice(-SPEED_SMOOTHING_SAMPLE_SIZE); |
| | | const total = nextSamples.reduce((sum, item) => sum + item, 0); |
| | | return { |
| | | nextSamples, |
| | | smoothedSpeedKmh: total / nextSamples.length |
| | | }; |
| | | }; |
| | | |
| | | /** |
| | | * 将目标比例尺限制为渐进变化,避免视角一步跳太远。 |
| | | */ |
| | | const resolveNextScale = (currentScale: number, targetScale: number) => { |
| | | const scaleDelta = targetScale - currentScale; |
| | | if (Math.abs(scaleDelta) <= MAX_SCALE_STEP_PER_TICK) { |
| | | return targetScale; |
| | | } |
| | | return currentScale + Math.sign(scaleDelta) * MAX_SCALE_STEP_PER_TICK; |
| | | }; |
| | | |
| | | /** |
| | | * 将速度换算成目标比例尺,速度越高视角越远。 |
| | | */ |
| | | const resolveScaleBySpeed = (speedKmh: number) => { |
| | | const safeSpeed = clamp(speedKmh, LOW_SPEED_KMH, HIGH_SPEED_KMH); |
| | | const speedRatio = (safeSpeed - LOW_SPEED_KMH) / (HIGH_SPEED_KMH - LOW_SPEED_KMH || 1); |
| | | const nextScale = MAX_DYNAMIC_SCALE - (MAX_DYNAMIC_SCALE - MIN_DYNAMIC_SCALE) * speedRatio; |
| | | return Math.round(nextScale / SCALE_STEP) * SCALE_STEP; |
| | | const resolveScaleBySpeed = (speedKmh: number, previousBandIndex: number | null) => { |
| | | const nextBandIndex = resolveStableScaleBandIndex(speedKmh, previousBandIndex); |
| | | return { |
| | | targetScale: DYNAMIC_SCALE_BANDS[nextBandIndex].scale, |
| | | bandIndex: nextBandIndex |
| | | }; |
| | | }; |
| | | |
| | | /** |
| | |
| | | export default function useActiveTripScale(deps: Deps) { |
| | | const [resumeVersion, setResumeVersion] = useState(0); // 冷却结束后触发重新评估 |
| | | const resumeTimerRef = useRef<any>(null); |
| | | const speedSamplesRef = useRef<number[]>([]); |
| | | const scaleBandIndexRef = useRef<number | null>(null); |
| | | const isActiveStatus = useMemo(() => { |
| | | return ACTIVE_TRIP_STATUSES.includes(deps.tripStatus); |
| | | }, [deps.tripStatus]); |
| | |
| | | } |
| | | deps.holdDynamicScale(USER_GESTURE_HOLD_MS); |
| | | scheduleResume(USER_GESTURE_HOLD_MS + SCALE_RESUME_BUFFER_MS); |
| | | }, [deps, scheduleResume]); |
| | | }, [deps.holdDynamicScale, scheduleResume]); |
| | | |
| | | /** |
| | | * ORDER_UPDATE_LINE 渲染完成后,自动执行一次全路线入屏。 |
| | |
| | | }, 0); |
| | | }); |
| | | scheduleResume(ORDER_UPDATE_LINE_HOLD_MS + SCALE_RESUME_BUFFER_MS); |
| | | }, [deps, isActiveStatus, scheduleResume]); |
| | | }, [deps.computeBottomPadding, deps.holdDynamicScale, deps.resolveSafeTopInset, isActiveStatus, scheduleResume]); |
| | | |
| | | useEffect(() => { |
| | | if (!isActiveStatus) { |
| | | clearResumeTimer(); |
| | | speedSamplesRef.current = []; |
| | | scaleBandIndexRef.current = null; |
| | | return; |
| | | } |
| | | const speedKmh = Number(deps.speedKmh); |
| | |
| | | scheduleResume(holdUntil - now + SCALE_RESUME_BUFFER_MS); |
| | | return; |
| | | } |
| | | const targetScale = resolveScaleBySpeed(speedKmh); |
| | | if (Math.abs(targetScale - deps.mapScale) < MIN_SCALE_CHANGE) { |
| | | const { nextSamples, smoothedSpeedKmh } = resolveSmoothedSpeed(speedSamplesRef.current, speedKmh); |
| | | speedSamplesRef.current = nextSamples; |
| | | const { targetScale, bandIndex } = resolveScaleBySpeed(smoothedSpeedKmh, scaleBandIndexRef.current); |
| | | scaleBandIndexRef.current = bandIndex; |
| | | const nextScale = resolveNextScale(deps.mapScale, targetScale); |
| | | if (Math.abs(nextScale - deps.mapScale) < MIN_SCALE_CHANGE) { |
| | | deps.syncActiveTripViewportToVehicle(); |
| | | return; |
| | | } |
| | | deps.setMapScale(targetScale); |
| | | }, [deps, isActiveStatus, resumeVersion, scheduleResume]); |
| | | deps.setMapScale(clamp(nextScale, 14, 18)); |
| | | deps.syncActiveTripViewportToVehicle(); |
| | | }, [deps.getDynamicScaleHoldUntil, deps.mapScale, deps.setMapScale, deps.speedKmh, deps.syncActiveTripViewportToVehicle, isActiveStatus, resumeVersion, scheduleResume]); |
| | | |
| | | useEffect(() => { |
| | | return () => { |
| | |
| | | |
| | | const DROPOFF_MAX_SCALE = 20; |
| | | const DEFAULT_DYNAMIC_SCALE_HOLD_MS = 2500; // 一次性视口调整后,暂停速度缩放的默认冷却时长 |
| | | const ACTIVE_TRIP_VIEWPORT_SYNC_MIN_DELTA = 0.00001; // 车辆中心跟随的最小经纬度变化,约 1 米,避免重复写入中心点状态 |
| | | |
| | | export default function useMapViewport(rawDeps?: Partial<Deps>) { |
| | | const deps = { ...defaultDeps, ...(rawDeps || {}) } as Deps; |
| | |
| | | .catch(() => { }); |
| | | }; |
| | | |
| | | /** |
| | | * 行程动态缩放生效时,将地图中心跟随到车辆位置,确保车辆留在当前视野内。 |
| | | */ |
| | | const syncActiveTripViewportToVehicle = useCallback(() => { |
| | | const vehicleLatitude = Number(deps.vehicleLocation?.latitude); |
| | | const vehicleLongitude = Number(deps.vehicleLocation?.longitude); |
| | | if (!Number.isFinite(vehicleLatitude) || !Number.isFinite(vehicleLongitude)) { |
| | | return; |
| | | } |
| | | const currentLatitude = Number(deps.latitude); |
| | | const currentLongitude = Number(deps.longitude); |
| | | const hasLatitudeDelta = !Number.isFinite(currentLatitude) || Math.abs(currentLatitude - vehicleLatitude) >= ACTIVE_TRIP_VIEWPORT_SYNC_MIN_DELTA; |
| | | const hasLongitudeDelta = !Number.isFinite(currentLongitude) || Math.abs(currentLongitude - vehicleLongitude) >= ACTIVE_TRIP_VIEWPORT_SYNC_MIN_DELTA; |
| | | if (!hasLatitudeDelta && !hasLongitudeDelta) { |
| | | return; |
| | | } |
| | | deps.setLatitude(vehicleLatitude); |
| | | deps.setLongitude(vehicleLongitude); |
| | | }, [deps.latitude, deps.longitude, deps.setLatitude, deps.setLongitude, deps.vehicleLocation?.latitude, deps.vehicleLocation?.longitude]); |
| | | |
| | | useEffect(() => { |
| | | if ([TripStatus.InService, TripStatus.ArrivedDestination].includes(deps.tripStatus)) { |
| | | setShowLocationFlag(false); |
| | |
| | | computeBottomPadding, |
| | | holdDynamicScale, |
| | | getDynamicScaleHoldUntil, |
| | | syncActiveTripViewportToVehicle, |
| | | updateMapViewportPadding, |
| | | toOrigin, |
| | | getLocationInfo |
| | |
| | | MODIFY_DESTINATION_ROUTE_POINTS_KEY, |
| | | MODIFY_DESTINATION_SESSION_KEY |
| | | } from '../constants/modify-destination'; |
| | | import { clearPassedRouteCache } from './passed-route-cache'; |
| | | |
| | | type Deps = { |
| | | tripStatus: TripStatus; |
| | |
| | | */ |
| | | const toBack = () => { |
| | | clearModifyDestinationStorage(); |
| | | clearPassedRouteCache(); |
| | | if (deps.tripStatus === TripStatus.PaymentSuccess || deps.tripStatus === TripStatus.PublishRoute) { |
| | | clearOrderTripStorage(); |
| | | Taro.removeStorageSync('endSite'); |
| | |
| | | import { queryVehicleByOrderId } from '../../../api/begin-route'; |
| | | import { toTripStatus, TripStatus } from '../constants/trip-status'; |
| | | import { MODIFY_DESTINATION_SESSION_KEY } from '../constants/modify-destination'; |
| | | import { readPassedRouteCache } from './passed-route-cache'; |
| | | |
| | | type Deps = { |
| | | wsHost: string; |
| | |
| | | setTripStatus: (value: TripStatus) => void; |
| | | setStartSite: (value: any) => void; |
| | | setEndSite: (value: any) => void; |
| | | setVehicleMoveAlongPoints: (value: any[]) => void; |
| | | setTrunkStatus: (value: boolean) => void; |
| | | setLatitude: (value: number | null) => void; |
| | | setLongitude: (value: number | null) => void; |
| | |
| | | const cachedTripStatusStorage = Taro.getStorageSync('tripStatus'); |
| | | const cachedTripStatus = toTripStatus(cachedTripStatusStorage, deps.getCurrentTripStatus()); |
| | | deps.setTripStatus(cachedTripStatus); |
| | | if ([TripStatus.VehicleApproaching, TripStatus.InService].includes(cachedTripStatus)) { |
| | | deps.setVehicleMoveAlongPoints(readPassedRouteCache(String(cachedOrderId || ''))); |
| | | } else { |
| | | deps.setVehicleMoveAlongPoints([]); |
| | | } |
| | | syncCachedOrderVehicle(cachedOrderId); |
| | | syncSitesFromActiveOrder(cachedTripStatusStorage, s, e); |
| | | if (cachedOrderId && [TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup, TripStatus.InService, TripStatus.ArrivedDestination, TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(cachedTripStatus)) { |
| | |
| | | lastPolylineUpdateAtRef.current = 0; |
| | | }, [ |
| | | allRoutePoints, |
| | | deps.allowSelectableRoutes, |
| | | deps.isAndroid, |
| | | deps.shouldShowRoute, |
| | | deps.statusRule.showWalking, |
| | | deps.tripStatus, |
| | | deps.vehicleMoveAlongPoints, |
| | | routePoints, |
| | | selectedRouteIndex, |
| | | walkingPoints |
| | |
| | | import { toTripStatus, TripStatus } from '../constants/trip-status'; |
| | | import { normalizeSocketRouteSnapshot } from './runtime-sync'; |
| | | import { MODIFY_DESTINATION_SOCKET_STATUS_KEY } from '../constants/modify-destination'; |
| | | import { clearPassedRouteCache, writePassedRouteCache } from './passed-route-cache'; |
| | | |
| | | type UseWebSocketEventsDeps = { |
| | | tripStatusRef: React.MutableRefObject<TripStatus>; |
| | |
| | | deps.setRoutePoints([]); |
| | | deps.setPolyline([]); |
| | | deps.setVehicleMoveAlongPoints([]); |
| | | clearPassedRouteCache(); |
| | | deps.polylineKeyRef.current = ''; |
| | | deps.lastPolylineUpdateAtRef.current = 0; |
| | | deps.lastRoutePointRef.current = null; |
| | |
| | | return; |
| | | } |
| | | const routeSnapshot = normalizeSocketRouteSnapshot(payload); |
| | | const currentOrderId = String(deps.orderId || Taro.getStorageSync('orderId') || ''); |
| | | if (routeSnapshot.vehicleMoveAlongPoints.length > 1) { |
| | | writePassedRouteCache(currentOrderId, routeSnapshot.vehicleMoveAlongPoints); |
| | | } |
| | | deps.applySmoothRouteSnapshot(routeSnapshot); |
| | | if (routeSnapshot.forceFullRedraw && typeof deps.onOrderUpdateLineRendered === 'function') { |
| | | Taro.nextTick(() => { |
| | |
| | | toOrigin, |
| | | getLocationInfo, |
| | | computeBottomPadding, |
| | | resolveSafeTopInset |
| | | resolveSafeTopInset, |
| | | syncActiveTripViewportToVehicle |
| | | } = viewportState; // 视口状态解构 |
| | | const activeTripScale = useActiveTripScale({ |
| | | tripStatus, |
| | | routePoints, |
| | | speedKmh: orderRouteMetrics?.speedKmh, |
| | | mapScale, |
| | | setMapScale: viewportState.setMapScale, |
| | | syncActiveTripViewportToVehicle, |
| | | computeBottomPadding, |
| | | resolveSafeTopInset, |
| | | holdDynamicScale, |
| | |
| | | setTripStatus, |
| | | setStartSite, |
| | | setEndSite, |
| | | setVehicleMoveAlongPoints, |
| | | setTrunkStatus, |
| | | setLatitude, |
| | | setLongitude, |
| | |
| | | const queryCouponCount = async () => { |
| | | try { |
| | | const res = await getUserouponNum(); |
| | | const nextCouponCount = res.data || 0; |
| | | const nextCouponCount = res || 0; |
| | | setCouponCount(nextCouponCount); |
| | | } catch (error) { |
| | | setCouponCount(0); |