feat(robotaxi): 优化行程功能与修复多类交互问题
本次提交优化机器人出租车行程全链路功能,修复各类已知问题:
1. 样式与交互修复:调整车辆marker高度、修正折扣文案负号显示、优化收藏按钮hover交互、统一首页面板高度动画逻辑
2. 搜索与收藏优化:兼容编码格式的路由关键词参数、将收藏弹窗与校验的分页大小改为全量拉取
3. 路线模块重构:迁移路线交互状态至父组件、新增速度驱动的动态地图比例尺适配、优化路线选中与偏好匹配逻辑、优化车辆marker更新策略
4. 修改目的地流程完善:新增报价超时重估逻辑、修复会话缓存回滚失效问题
5. 端侧适配优化:新增Android端路线摘要气泡覆盖物、修复Android端地图气泡遮挡与手势冲突问题
6. 订单功能扩展:将再来一单支持范围扩展至待评价订单、优化首页订单同步与用户实时定位缓存逻辑
| | |
| | | return ( |
| | | <View |
| | | className="content-box" |
| | | style={{ height: props.bannerList.length > 1 ? `${props.boxHeight}px` : 'auto', transition: props.isDragging ? 'none' : 'height 0.3s ease-out' }} |
| | | style={{ height:`${props.boxHeight}px`, transition: props.isDragging ? 'none' : 'height 0.3s ease-out' }} |
| | | catchMove |
| | | onTouchStart={(event) => { |
| | | event.stopPropagation(); |
| | |
| | | |
| | | type HomeTripStatus = TripStatus | 0; |
| | | |
| | | /** 首页登录与订单同步结果。 */ |
| | | type AuthOrderSyncResult = { |
| | | /** 当前是否已登录。 */ |
| | | hasLogin: boolean; |
| | | /** 当前是否存在活跃订单。 */ |
| | | hasActiveOrder: boolean; |
| | | }; |
| | | |
| | | type Deps = { |
| | | /** ws 服务地址 */ |
| | | wsHost: string; |
| | |
| | | }; |
| | | |
| | | /** |
| | | * 将活跃订单起点映射为首页缓存站点。 |
| | | */ |
| | | const buildStartSiteByActiveOrder = (activeOrder: any) => ({ |
| | | id: activeOrder.startPointId || '', |
| | | name: activeOrder.startPointName || '', |
| | | latitude: activeOrder.startLat || 0, |
| | | longitude: activeOrder.startLon || 0, |
| | | electronicAreaId: activeOrder.xpElectronicFenceAreaId || '', |
| | | time: activeOrder.startParkingTimeLimit || 0, |
| | | }); |
| | | |
| | | /** |
| | | * 将活跃订单终点映射为首页缓存站点。 |
| | | */ |
| | | const buildEndSiteByActiveOrder = (activeOrder: any) => ({ |
| | | id: activeOrder.endPointId || '', |
| | | name: activeOrder.endPointName || '', |
| | | latitude: activeOrder.endLat || 0, |
| | | longitude: activeOrder.endLon || 0, |
| | | electronicAreaId: activeOrder.xpElectronicFenceAreaId || '', |
| | | time: activeOrder.endParkingTimeLimit || 0, |
| | | }); |
| | | |
| | | /** |
| | | * 同步登录、协议与活跃订单。 |
| | | */ |
| | | const syncAuthAndOrder = async () => { |
| | | const otp = await deps.checkOtp(false); |
| | | deps.setNeedLogin(!otp); |
| | | if (!otp) return false; |
| | | if (!otp) { |
| | | return { hasLogin: false, hasActiveOrder: false } as AuthOrderSyncResult; |
| | | } |
| | | wsManager.connect(deps.wsHost); |
| | | const agreement = await deps.getAgreement(); |
| | | if (agreement === false) { |
| | |
| | | if (!activeOrder) { |
| | | Taro.removeStorageSync('orderId'); |
| | | Taro.removeStorageSync('tripStatus'); |
| | | Taro.removeStorageSync('startSite'); |
| | | Taro.removeStorageSync('endSite'); |
| | | deps.setTripStatus(0); |
| | | deps.setOrderData({}); |
| | | return true; |
| | | return { hasLogin: true, hasActiveOrder: false } as AuthOrderSyncResult; |
| | | } |
| | | deps.setOrderData(activeOrder || {}); |
| | | const mappedStatus = toHomeTripStatus(mapOrderStatusToTripStatus(activeOrder.status || 0)); |
| | | const startSite = buildStartSiteByActiveOrder(activeOrder); |
| | | const endSite = buildEndSiteByActiveOrder(activeOrder); |
| | | deps.setTripStatus(mappedStatus); |
| | | Taro.setStorageSync('tripStatus', mappedStatus); |
| | | Taro.setStorageSync('orderId', activeOrder.id || ''); |
| | | if (!Taro.getStorageSync('startSite')) { |
| | | Taro.setStorageSync('startSite', { |
| | | id: activeOrder.startPointId || '', |
| | | name: activeOrder.startPointName || '', |
| | | latitude: activeOrder.startLat || 0, |
| | | longitude: activeOrder.startLon || 0, |
| | | electronicAreaId: activeOrder.xpElectronicFenceAreaId || '', |
| | | time: activeOrder.startParkingTimeLimit || 0, |
| | | }); |
| | | } |
| | | if (!Taro.getStorageSync('endSite')) { |
| | | Taro.setStorageSync('endSite', { |
| | | id: activeOrder.endPointId || '', |
| | | name: activeOrder.endPointName || '', |
| | | latitude: activeOrder.endLat || 0, |
| | | longitude: activeOrder.endLon || 0, |
| | | electronicAreaId: activeOrder.xpElectronicFenceAreaId || '', |
| | | time: activeOrder.endParkingTimeLimit || 0, |
| | | }); |
| | | } |
| | | return true; |
| | | Taro.setStorageSync('startSite', startSite); |
| | | Taro.setStorageSync('endSite', endSite); |
| | | return { hasLogin: true, hasActiveOrder: true } as AuthOrderSyncResult; |
| | | }; |
| | | |
| | | /** |
| | |
| | | Taro.getSetting({ |
| | | success: (res) => { |
| | | const auth = res.authSetting['scope.userLocation']; |
| | | const startSite = Taro.getStorageSync('startSite'); |
| | | processLocationSetting(auth, startSite); |
| | | Promise.resolve(authSyncTask) |
| | | .then((syncResult) => { |
| | | const startSite = syncResult.hasActiveOrder ? Taro.getStorageSync('startSite') : null; |
| | | processLocationSetting(auth, startSite); |
| | | }) |
| | | .catch(() => { |
| | | processLocationSetting(auth, null); |
| | | }); |
| | | } |
| | | }); |
| | | return authSyncTask; |
| | | return authSyncTask.then((syncResult) => syncResult.hasLogin); |
| | | }; |
| | | |
| | | return { |
| | |
| | | longitude: vehicle.longitude, |
| | | rotate: vehicle.direction, |
| | | width: 20, |
| | | height: 20, |
| | | height: 40, |
| | | zIndex: 6, |
| | | anchor: { x: 0.5, y: 0.5 }, |
| | | iconPath: VEHICLE_MARKER_ICON |
| | |
| | | z-index: 11; |
| | | width: 100%; |
| | | box-sizing: border-box; |
| | | padding: 24px 38px 62px 32px; |
| | | background: #FEFEFF; |
| | | |
| | | .discount-amount { |
| | | display: flex; |
| | |
| | | padding: 8px 32px 8px 24px; |
| | | background: #FFF3E6; |
| | | |
| | | .flex-center { |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | |
| | | .discount-amount-text { |
| | | flex-shrink: 0; |
| | | border-radius: 6px; |
| | | background: rgba(245, 137, 0, 0.06); |
| | | box-sizing: border-box; |
| | |
| | | } |
| | | |
| | | .change-coupon-icon { |
| | | flex-shrink: 0; |
| | | width: 24px; |
| | | height: 24px; |
| | | } |
| | |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | padding: 24px 38px 62px 32px; |
| | | background: #FEFEFF; |
| | | |
| | | .money-box { |
| | | |
| | |
| | | )} |
| | | {isOperateFree ? null : fareEstimate.discountAmount > 0 && |
| | | <View className='modify-destination-review-fare-discount'> |
| | | 已优惠<Text className='modify-destination-review-fare-discount-amount'>{-fareEstimate.discountAmount}元</Text> |
| | | 已优惠<Text className='modify-destination-review-fare-discount-amount'>{fareEstimate.discountAmount}元</Text> |
| | | </View> |
| | | } |
| | | </View> |
| | |
| | | { |
| | | isOperateFree ? null : fareEstimate.discountAmount > 0 && |
| | | <View className="discount-amount" onClick={onOpenCouponSelect}> |
| | | <View> |
| | | <View className="flex-center"> |
| | | <View className="discount-amount-text">最佳优惠</View> |
| | | <View className="discount-amount-desc" >已为您选择最佳优惠</View> |
| | | </View> |
| | | <View> |
| | | <View className="flex-center"> |
| | | <View className="discount-amount-num">{fareEstimate.discountAmount}元</View> |
| | | <Image className="change-coupon-icon" src={changeCouponIcon} /> |
| | | </View> |
| | |
| | | type Props = { |
| | | routeOptions?: any[]; |
| | | selectedRouteIndex?: number; |
| | | handleSelectRoute?: (index: number) => void; |
| | | handleSelectRoute?: (index: number, options?: { syncSites?: boolean; syncViewport?: boolean }) => void; |
| | | }; |
| | | |
| | | /** |
| | | * 根据路线规则匹配偏好项。 |
| | | */ |
| | | const findPreferenceByRouteRule = ( |
| | | options: RoutePreferenceOption[], |
| | | routeRule: string | number | null | undefined |
| | | ) => options.find((item) => String(item?.value ?? '') === String(routeRule ?? '')) || null; |
| | | |
| | | export default function RouteSelectionPanel({ |
| | | routeOptions = [], |
| | |
| | | }, [openCityId]); |
| | | |
| | | /** |
| | | * 初始化回显:优先 selected=true,否则取第一个偏好,并按策略值匹配默认选中路线。 |
| | | * 初始化回显: |
| | | * 1. 有 selected=true 时,优先按该偏好匹配路线; |
| | | * 2. 没有 selected=true 时,优先回显第一个路线,并反向匹配对应偏好。 |
| | | */ |
| | | useEffect(() => { |
| | | if (hasAppliedDefaultSelectionRef.current) return; |
| | | if (!preferenceOptions.length) return; |
| | | const defaultOption = preferenceOptions.find(item => item.selected === true) || preferenceOptions[0]; |
| | | if (!defaultOption) return; |
| | | setPreference(defaultOption); |
| | | if (!routeOptions.length || !handleSelectRoute) return; |
| | | const targetIndex = routeOptions.findIndex(route => String(route?.primaryRoutePlanRule ?? '') === String(defaultOption.value)); |
| | | if (targetIndex !== -1 && targetIndex !== selectedRouteIndex) { |
| | | handleSelectRoute(targetIndex); |
| | | |
| | | const selectedPreferenceOption = preferenceOptions.find((item) => item.selected === true) || null; |
| | | if (selectedPreferenceOption) { |
| | | setPreference(selectedPreferenceOption); |
| | | if (!routeOptions.length || !handleSelectRoute) return; |
| | | const targetIndex = routeOptions.findIndex((route) => ( |
| | | String(route?.primaryRoutePlanRule ?? '') === String(selectedPreferenceOption.value) |
| | | )); |
| | | if (targetIndex !== -1 && targetIndex !== selectedRouteIndex) { |
| | | handleSelectRoute(targetIndex, { syncSites: false, syncViewport: false }); |
| | | } |
| | | hasAppliedDefaultSelectionRef.current = true; |
| | | return; |
| | | } |
| | | |
| | | const firstRoute = routeOptions[0]; |
| | | if (!firstRoute) return; |
| | | const matchedPreference = findPreferenceByRouteRule(preferenceOptions, firstRoute?.primaryRoutePlanRule); |
| | | setPreference(matchedPreference || DEFAULT_PREFERENCE); |
| | | if (handleSelectRoute && selectedRouteIndex !== 0) { |
| | | handleSelectRoute(0, { syncSites: false, syncViewport: false }); |
| | | } |
| | | hasAppliedDefaultSelectionRef.current = true; |
| | | }, [preferenceOptions, routeOptions, handleSelectRoute, selectedRouteIndex]); |
| | |
| | | }); |
| | | |
| | | if (targetIndex !== -1) { |
| | | handleSelectRoute(targetIndex); |
| | | handleSelectRoute(targetIndex, { syncSites: false }); |
| | | } |
| | | } |
| | | |
| | |
| | | <View |
| | | className={`route-select-content-item ${isSelected ? 'active-item' : ''}`} |
| | | key={index} |
| | | onClick={() => handleSelectRoute && handleSelectRoute(index)} |
| | | onClick={() => handleSelectRoute && handleSelectRoute(index, { syncSites: false })} |
| | | > |
| | | <View className='route-select-content-item-title'>{item?.primaryStrategyLabel}</View> |
| | | <View className='route-select-content-item-time'>{time.value}{time.unit}</View> |
| New file |
| | |
| | | import Taro from '@tarojs/taro'; |
| | | import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; |
| | | import { TripStatus } from '../constants/trip-status'; |
| | | |
| | | type RoutePoint = { |
| | | latitude: number; |
| | | longitude: number; |
| | | }; |
| | | |
| | | type Deps = { |
| | | tripStatus: TripStatus; |
| | | routePoints: RoutePoint[]; |
| | | speedKmh: number | null; |
| | | mapScale: number; |
| | | setMapScale: (value: number) => void; |
| | | computeBottomPadding: (points: any[], viewportType: string) => number; |
| | | resolveSafeTopInset: () => number; |
| | | holdDynamicScale: (durationMs?: number) => void; |
| | | getDynamicScaleHoldUntil: () => number; |
| | | }; |
| | | |
| | | const ACTIVE_TRIP_STATUSES = [TripStatus.VehicleApproaching, TripStatus.InService]; // 启用速度缩放的行程状态 |
| | | const USER_GESTURE_CAUSED_BY = new Set(['gesture', 'drag', 'scale']); // 需要暂停速度缩放的用户手势来源 |
| | | const USER_GESTURE_HOLD_MS = 3000; // 用户手势后暂停速度缩放时长 |
| | | const ORDER_UPDATE_LINE_HOLD_MS = 2500; // ORDER_UPDATE_LINE 全路线入屏后暂停速度缩放时长 |
| | | const 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 ORDER_UPDATE_LINE_SIDE_PADDING = 100; // ORDER_UPDATE_LINE 路线入屏左右留白 |
| | | const ORDER_UPDATE_LINE_TOP_PADDING = 24; // ORDER_UPDATE_LINE 路线入屏最小顶部留白 |
| | | const ORDER_UPDATE_LINE_TOP_EXTRA_PADDING = 32; // ORDER_UPDATE_LINE 顶部安全区附加留白 |
| | | |
| | | /** |
| | | * 将数值限制在给定区间内。 |
| | | */ |
| | | const clamp = (value: number, min: number, max: number) => { |
| | | if (value < min) return min; |
| | | if (value > max) return max; |
| | | return value; |
| | | }; |
| | | |
| | | /** |
| | | * 将速度换算成目标比例尺,速度越高视角越远。 |
| | | */ |
| | | 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; |
| | | }; |
| | | |
| | | /** |
| | | * 接驾中/行程中动态缩放:由速度驱动比例尺,并保留 ORDER_UPDATE_LINE 的一次性全路线入屏。 |
| | | */ |
| | | export default function useActiveTripScale(deps: Deps) { |
| | | const [resumeVersion, setResumeVersion] = useState(0); // 冷却结束后触发重新评估 |
| | | const resumeTimerRef = useRef<any>(null); |
| | | const isActiveStatus = useMemo(() => { |
| | | return ACTIVE_TRIP_STATUSES.includes(deps.tripStatus); |
| | | }, [deps.tripStatus]); |
| | | |
| | | /** |
| | | * 清理冷却恢复定时器。 |
| | | */ |
| | | const clearResumeTimer = () => { |
| | | if (!resumeTimerRef.current) { |
| | | return; |
| | | } |
| | | clearTimeout(resumeTimerRef.current); |
| | | resumeTimerRef.current = null; |
| | | }; |
| | | |
| | | /** |
| | | * 在冷却结束后触发一次动态缩放重算。 |
| | | */ |
| | | const scheduleResume = useCallback((delayMs: number) => { |
| | | clearResumeTimer(); |
| | | const safeDelayMs = Math.max(0, Math.ceil(delayMs)); |
| | | resumeTimerRef.current = setTimeout(() => { |
| | | setResumeVersion((value) => value + 1); |
| | | resumeTimerRef.current = null; |
| | | }, safeDelayMs); |
| | | }, []); |
| | | |
| | | /** |
| | | * 处理地图手势,手动缩放/拖拽后短暂暂停速度接管。 |
| | | */ |
| | | const handleRegionChange = useCallback((event: any) => { |
| | | const causedBy = String(event?.causedBy || event?.detail?.causedBy || '').toLowerCase(); |
| | | if (!USER_GESTURE_CAUSED_BY.has(causedBy)) { |
| | | return; |
| | | } |
| | | deps.holdDynamicScale(USER_GESTURE_HOLD_MS); |
| | | scheduleResume(USER_GESTURE_HOLD_MS + SCALE_RESUME_BUFFER_MS); |
| | | }, [deps, scheduleResume]); |
| | | |
| | | /** |
| | | * ORDER_UPDATE_LINE 渲染完成后,自动执行一次全路线入屏。 |
| | | */ |
| | | const handleOrderUpdateLineRendered = useCallback((points: RoutePoint[]) => { |
| | | if (!isActiveStatus) { |
| | | return; |
| | | } |
| | | const safePoints = Array.isArray(points) ? points : []; |
| | | if (safePoints.length < 2) { |
| | | return; |
| | | } |
| | | const mapCtx = Taro.createMapContext('map'); |
| | | const bottomPadding = deps.computeBottomPadding(safePoints, 'route'); |
| | | const topPadding = Math.max(ORDER_UPDATE_LINE_TOP_PADDING, deps.resolveSafeTopInset() + ORDER_UPDATE_LINE_TOP_EXTRA_PADDING); |
| | | deps.holdDynamicScale(ORDER_UPDATE_LINE_HOLD_MS); |
| | | Taro.nextTick(() => { |
| | | setTimeout(() => { |
| | | mapCtx.includePoints({ |
| | | points: safePoints, |
| | | padding: [topPadding, ORDER_UPDATE_LINE_SIDE_PADDING, bottomPadding, ORDER_UPDATE_LINE_SIDE_PADDING] |
| | | }); |
| | | }, 0); |
| | | }); |
| | | scheduleResume(ORDER_UPDATE_LINE_HOLD_MS + SCALE_RESUME_BUFFER_MS); |
| | | }, [deps, isActiveStatus, scheduleResume]); |
| | | |
| | | useEffect(() => { |
| | | if (!isActiveStatus) { |
| | | clearResumeTimer(); |
| | | return; |
| | | } |
| | | const speedKmh = Number(deps.speedKmh); |
| | | if (!Number.isFinite(speedKmh)) { |
| | | return; |
| | | } |
| | | const holdUntil = Number(deps.getDynamicScaleHoldUntil()); |
| | | const now = Date.now(); |
| | | if (Number.isFinite(holdUntil) && holdUntil > now) { |
| | | scheduleResume(holdUntil - now + SCALE_RESUME_BUFFER_MS); |
| | | return; |
| | | } |
| | | const targetScale = resolveScaleBySpeed(speedKmh); |
| | | if (Math.abs(targetScale - deps.mapScale) < MIN_SCALE_CHANGE) { |
| | | return; |
| | | } |
| | | deps.setMapScale(targetScale); |
| | | }, [deps, isActiveStatus, resumeVersion, scheduleResume]); |
| | | |
| | | useEffect(() => { |
| | | return () => { |
| | | clearResumeTimer(); |
| | | }; |
| | | }, []); |
| | | |
| | | return { |
| | | handleRegionChange, |
| | | handleOrderUpdateLineRendered |
| | | }; |
| | | } |
| | |
| | | isAndroid: boolean; |
| | | tripStatus: TripStatus; |
| | | mapScale: number; |
| | | routeSummaryLocation: { latitude: number; longitude: number } | null; |
| | | startSite: any; |
| | | endSite: any; |
| | | vehicleLocation: { latitude: number; longitude: number } | null; |
| | |
| | | // 到达气泡样式与显示状态 |
| | | const [arriveCalloutStyle, setArriveCalloutStyle] = useState<any>({}); |
| | | const [arriveCalloutVisible, setArriveCalloutVisible] = useState(false); |
| | | // 路线摘要气泡样式与显示状态 |
| | | const [routeSummaryCalloutStyle, setRouteSummaryCalloutStyle] = useState<any>({}); |
| | | const [routeSummaryCalloutVisible, setRouteSummaryCalloutVisible] = useState(false); |
| | | const mapRectRef = useRef<any>(null); |
| | | |
| | | // 经纬度转屏幕坐标 |
| | |
| | | setArriveCalloutVisible(true); |
| | | }; |
| | | |
| | | // 更新路线摘要气泡位置 |
| | | const updateRouteSummaryCalloutPosition = async () => { |
| | | if (!deps.isAndroid || !deps.routeSummaryLocation) { |
| | | setRouteSummaryCalloutVisible(false); |
| | | return; |
| | | } |
| | | const rect = await ensureMapRect(); |
| | | const pos = await computeScreenPosition(deps.routeSummaryLocation, rect, 0); |
| | | if (!pos) { |
| | | setRouteSummaryCalloutVisible(false); |
| | | return; |
| | | } |
| | | setRouteSummaryCalloutStyle({ |
| | | position: 'fixed', |
| | | left: pos.left, |
| | | top: pos.top, |
| | | transform: 'translate(-50%, -100%)' |
| | | }); |
| | | setRouteSummaryCalloutVisible(true); |
| | | }; |
| | | |
| | | // 地图区域变化时同步覆盖物显隐与重算 |
| | | const handleRegionChange = (e: any) => { |
| | | if (e?.type === 'begin') { |
| | | setWaitingRadarVisible(false); |
| | | setCarCalloutVisible(false); |
| | | setArriveCalloutVisible(false); |
| | | setRouteSummaryCalloutVisible(false); |
| | | return; |
| | | } |
| | | if (e?.type === 'end') { |
| | |
| | | updateWaitingRadarPosition(); |
| | | updateCarCalloutPosition(); |
| | | updateArriveCalloutPosition(); |
| | | updateRouteSummaryCalloutPosition(); |
| | | } |
| | | }; |
| | | |
| | |
| | | useEffect(() => { |
| | | updateArriveCalloutPosition(); |
| | | }, [deps.isAndroid, deps.tripStatus, deps.endSite?.latitude, deps.endSite?.longitude]); |
| | | |
| | | useEffect(() => { |
| | | updateRouteSummaryCalloutPosition(); |
| | | }, [deps.isAndroid, deps.routeSummaryLocation?.latitude, deps.routeSummaryLocation?.longitude, deps.mapScale]); |
| | | |
| | | useEffect(() => { |
| | | if (!deps.isAndroid || deps.tripStatus !== TripStatus.ArrivedDestination) return; |
| | |
| | | carCalloutVisible, |
| | | arriveCalloutStyle, |
| | | arriveCalloutVisible, |
| | | routeSummaryCalloutStyle, |
| | | routeSummaryCalloutVisible, |
| | | updateWaitingRadarPosition, |
| | | updateCarCalloutPosition, |
| | | updateArriveCalloutPosition, |
| | | updateRouteSummaryCalloutPosition, |
| | | handleRegionChange |
| | | }; |
| | | } |
| | |
| | | const carMarkerInitialized = useRef(false); |
| | | const tripStatus6MarkerRefreshedRef = useRef(false); |
| | | |
| | | // 行驶中/接驾中车辆 marker 统一交由 translateMarker 托管位置与角度 |
| | | const shouldUsePureTranslateCarMarker = [TripStatus.VehicleApproaching, TripStatus.InService].includes(deps.tripStatus) |
| | | && deps.statusRule.showCar; |
| | | |
| | | // 构建上车点 marker |
| | | const createPickupMarker = (pickupSite: any, useAndroidNativeTrip1Callout: boolean) => { |
| | | if (!(pickupSite?.latitude && pickupSite?.longitude)) return null; |
| | |
| | | // 应用 marker 状态并处理“行驶中”阶段的刷新标记 |
| | | const applyMarkerState = (nextMarkers: any[], nextMarkersSignature: string, shouldSkipUpdate: boolean, carMarker: any) => { |
| | | if (shouldSkipUpdate) { |
| | | // 跳过整包 markers 更新时,仍兜底同步车辆 marker 位置与角度,避免朝向卡旧值 |
| | | // 平移态下保留已创建车辆 marker 的当前位置和角度,只同步其他 marker 结构变化 |
| | | setMarkers((prevMarkers: any[]) => { |
| | | const targetCarIndex = prevMarkers.findIndex((item: any) => item.id === deps.vehicleMarkerId); |
| | | if (targetCarIndex === -1) { |
| | | const nextCarIndex = nextMarkers.findIndex((item: any) => item.id === deps.vehicleMarkerId); |
| | | if (nextCarIndex === -1) { |
| | | if (markersSignatureRef.current !== nextMarkersSignature) { |
| | | markersSignatureRef.current = nextMarkersSignature; |
| | | return nextMarkers; |
| | | } |
| | | return prevMarkers; |
| | | } |
| | | if (targetCarIndex === -1 || !shouldUsePureTranslateCarMarker) { |
| | | if (markersSignatureRef.current !== nextMarkersSignature) { |
| | | markersSignatureRef.current = nextMarkersSignature; |
| | | return nextMarkers; |
| | |
| | | return prevMarkers; |
| | | } |
| | | const prevCar = prevMarkers[targetCarIndex] || {}; |
| | | const nextCarRotate = typeof carMarker?.rotate === 'number' ? carMarker.rotate : prevCar.rotate; |
| | | const carLocationChanged = |
| | | Number(prevCar.latitude) !== Number(carMarker?.latitude) || |
| | | Number(prevCar.longitude) !== Number(carMarker?.longitude); |
| | | const carRotateChanged = Number(prevCar.rotate) !== Number(nextCarRotate); |
| | | if (!carLocationChanged && !carRotateChanged) { |
| | | if (markersSignatureRef.current === nextMarkersSignature) { |
| | | return prevMarkers; |
| | | } |
| | | const next = [...prevMarkers]; |
| | | next[targetCarIndex] = { |
| | | ...prevCar, |
| | | latitude: carMarker?.latitude, |
| | | longitude: carMarker?.longitude, |
| | | rotate: nextCarRotate |
| | | markersSignatureRef.current = nextMarkersSignature; |
| | | const mergedMarkers = [...nextMarkers]; |
| | | mergedMarkers[nextCarIndex] = { |
| | | ...mergedMarkers[nextCarIndex], |
| | | latitude: prevCar.latitude, |
| | | longitude: prevCar.longitude, |
| | | rotate: prevCar.rotate |
| | | }; |
| | | return next; |
| | | return mergedMarkers; |
| | | }); |
| | | } |
| | | if (!shouldSkipUpdate && markersSignatureRef.current !== nextMarkersSignature) { |
| | |
| | | if (deps.statusRule.showDropoff && dropoffMarker) nextMarkers.push(dropoffMarker); |
| | | if (beforeUserDropoffMarker) nextMarkers.push(beforeUserDropoffMarker); |
| | | const nextMarkersSignature = nextMarkers |
| | | .map((m: any) => [ |
| | | m.id, |
| | | Number(m.latitude).toFixed(6), |
| | | Number(m.longitude).toFixed(6), |
| | | m.name || '', |
| | | typeof m.rotate === 'number' ? m.rotate : '', |
| | | m?.customCallout?.anchorY ?? '', |
| | | ].join('|')) |
| | | .map((m: any) => { |
| | | const shouldIgnoreDynamicCarPosition = shouldUsePureTranslateCarMarker && m.id === deps.vehicleMarkerId; |
| | | return [ |
| | | m.id, |
| | | shouldIgnoreDynamicCarPosition ? 'translate' : Number(m.latitude).toFixed(6), |
| | | shouldIgnoreDynamicCarPosition ? 'translate' : Number(m.longitude).toFixed(6), |
| | | m.name || '', |
| | | shouldIgnoreDynamicCarPosition ? 'translate' : (typeof m.rotate === 'number' ? m.rotate : ''), |
| | | m?.customCallout?.anchorY ?? '', |
| | | ].join('|'); |
| | | }) |
| | | .join(';'); |
| | | const hasDropoffMarker = markersRef.current.some((m: any) => m.id === 2); |
| | | const shouldHoldMarkerByTranslate = [TripStatus.VehicleApproaching, TripStatus.InService].includes(deps.tripStatus) && carMarkerInitialized.current && deps.statusRule.showCar && (!deps.statusRule.showDropoff || hasDropoffMarker); |
| | |
| | | }; |
| | | |
| | | const DROPOFF_MAX_SCALE = 20; |
| | | const DEFAULT_DYNAMIC_SCALE_HOLD_MS = 2500; // 一次性视口调整后,暂停速度缩放的默认冷却时长 |
| | | |
| | | export default function useMapViewport(rawDeps?: Partial<Deps>) { |
| | | const deps = { ...defaultDeps, ...(rawDeps || {}) } as Deps; |
| | |
| | | const tripStatus6ViewportLockedRef = useRef(false); |
| | | const orderDetailEnterSignatureRef = useRef(''); |
| | | const orderDetailRouteUpdatedAfterEnterRef = useRef(false); |
| | | const dynamicScaleHoldUntilRef = useRef(0); // 速度缩放暂停截止时间 |
| | | |
| | | const isAndroid = useMemo(() => { |
| | | try { |
| | |
| | | } catch (error) { |
| | | return false; |
| | | } |
| | | }, []); |
| | | |
| | | /** |
| | | * 暂停速度驱动缩放,避免一次性视口调整后立刻被比例尺逻辑抢回。 |
| | | */ |
| | | const holdDynamicScale = useCallback((durationMs: number = DEFAULT_DYNAMIC_SCALE_HOLD_MS) => { |
| | | const safeDurationMs = Math.max(0, Number(durationMs) || 0); |
| | | dynamicScaleHoldUntilRef.current = Date.now() + safeDurationMs; |
| | | }, []); |
| | | |
| | | /** |
| | | * 获取速度缩放暂停截止时间。 |
| | | */ |
| | | const getDynamicScaleHoldUntil = useCallback(() => { |
| | | return dynamicScaleHoldUntilRef.current; |
| | | }, []); |
| | | |
| | | // 获取底部安全区高度 |
| | |
| | | const padding = viewportType === 'order_detail' |
| | | ? [topPadding, 50, bottomPadding, 50] |
| | | : [topPadding, 100, bottomPadding, 100]; |
| | | holdDynamicScale(); |
| | | Taro.nextTick(() => { |
| | | if (viewportType === 'dropoff') { |
| | | mapCtx.moveToLocation({ |
| | |
| | | if (deps.tripStatus === TripStatus.InService && deps.routePoints.length > 0) { |
| | | const bottomPadding = computeBottomPadding(deps.routePoints, 'route'); |
| | | const topPadding = Math.max(24, resolveSafeTopInset() + 32); |
| | | holdDynamicScale(); |
| | | mapCtx.includePoints({ |
| | | points: deps.routePoints, |
| | | padding: [topPadding, 100, bottomPadding, 100] |
| | |
| | | tripStatus6ViewportLockedRef.current = true; |
| | | return; |
| | | } |
| | | holdDynamicScale(); |
| | | mapCtx.moveToLocation(); |
| | | }; |
| | | |
| | |
| | | if (deps.tripStatus === TripStatus.InService && deps.routePoints.length > 0) { |
| | | const bottomPadding = computeBottomPadding(deps.routePoints, 'route'); |
| | | const topPadding = Math.max(24, resolveSafeTopInset() + 32); |
| | | holdDynamicScale(); |
| | | Taro.createMapContext('map').includePoints({ |
| | | points: deps.routePoints, |
| | | padding: [topPadding, 100, bottomPadding, 100] |
| | |
| | | tripStatus6ViewportLockedRef.current = true; |
| | | return; |
| | | } |
| | | holdDynamicScale(); |
| | | Taro.createMapContext('map').moveToLocation({ |
| | | latitude: res.latitude, |
| | | longitude: res.longitude |
| | |
| | | resolveSafeBottomInset, |
| | | resolveSafeTopInset, |
| | | computeBottomPadding, |
| | | holdDynamicScale, |
| | | getDynamicScaleHoldUntil, |
| | | updateMapViewportPadding, |
| | | toOrigin, |
| | | getLocationInfo |
| | |
| | | Taro.removeStorageSync(MODIFY_DESTINATION_OVERRIDE_KEY); |
| | | }; |
| | | |
| | | /** |
| | | * 解析修改目的地流程中的“原始目的地”,优先使用会话缓存,避免内存态丢失导致回滚失败。 |
| | | */ |
| | | const resolvePreviousEndSite = () => { |
| | | const session = Taro.getStorageSync(MODIFY_DESTINATION_SESSION_KEY); |
| | | const cachedPreviousEndSite = session?.previousEndSite; |
| | | if (isValidSite(cachedPreviousEndSite)) { |
| | | return cachedPreviousEndSite; |
| | | } |
| | | if (isValidSite(previousEndSite)) { |
| | | return previousEndSite; |
| | | } |
| | | return null; |
| | | }; |
| | | |
| | | // 退出修改目的地确认态并按需恢复修改前终点 |
| | | const exitModifyDestinationReview = (options: ExitModifyDestinationReviewOptions = {}) => { |
| | | const { shouldRestorePreviousEndSite = true } = options; |
| | | if (shouldRestorePreviousEndSite && isValidSite(previousEndSite) && !isSameSite(previousEndSite, deps.endSite)) { |
| | | Taro.setStorageSync('endSite', previousEndSite); |
| | | deps.setEndSite(previousEndSite); |
| | | const nextPreviousEndSite = resolvePreviousEndSite(); |
| | | if (shouldRestorePreviousEndSite && nextPreviousEndSite && !isSameSite(nextPreviousEndSite, deps.endSite)) { |
| | | Taro.setStorageSync('endSite', nextPreviousEndSite); |
| | | deps.setEndSite(nextPreviousEndSite); |
| | | } |
| | | setPreviewEndSite(null); |
| | | setPreviousEndSite(null); |
| | |
| | | import Taro from '@tarojs/taro'; |
| | | import { useEffect, useRef } from 'react'; |
| | | import { useCallback, useEffect, useRef, useState } from 'react'; |
| | | import { TripStatus } from '../constants/trip-status'; |
| | | |
| | | type RequestRouteOptions = { |
| | |
| | | */ |
| | | export default function useModifyDestinationPreviewRoute(deps: Deps) { |
| | | const previewRouteKeyRef = useRef(''); // 预览路线请求签名,避免重复请求 |
| | | const requoteTriggeredRef = useRef(false); // 确认态 60 秒重估价触发标记 |
| | | const [pendingConfirmRequote, setPendingConfirmRequote] = useState(false); // 确认态停留超过 60 秒后,点击确认前需要先重估价 |
| | | |
| | | useEffect(() => { |
| | | const routeContext = resolveModifyDestinationRouteContext(deps); |
| | | if (!routeContext) { |
| | | previewRouteKeyRef.current = ''; |
| | | setPendingConfirmRequote(false); |
| | | return; |
| | | } |
| | | if (routeContext.requestKey === previewRouteKeyRef.current) { |
| | | return; |
| | | } |
| | | previewRouteKeyRef.current = routeContext.requestKey; |
| | | setPendingConfirmRequote(false); |
| | | deps.requestRouteAndFare( |
| | | routeContext.originLat, |
| | | routeContext.originLng, |
| | |
| | | |
| | | useEffect(() => { |
| | | if (!deps.isModifyDestinationReviewVisible) { |
| | | requoteTriggeredRef.current = false; |
| | | setPendingConfirmRequote(false); |
| | | return; |
| | | } |
| | | |
| | | requoteTriggeredRef.current = false; |
| | | const timer = setTimeout(() => { |
| | | if (requoteTriggeredRef.current) { |
| | | return; |
| | | } |
| | | const routeContext = resolveModifyDestinationRouteContext(deps); |
| | | if (!routeContext) { |
| | | return; |
| | | } |
| | | requoteTriggeredRef.current = true; |
| | | deps.requestRouteAndFare( |
| | | routeContext.originLat, |
| | | routeContext.originLng, |
| | | routeContext.destLat, |
| | | routeContext.destLng, |
| | | { |
| | | startPointId: routeContext.startPointId, |
| | | force: true |
| | | } |
| | | ); |
| | | setPendingConfirmRequote(true); |
| | | }, MODIFY_DESTINATION_REQUOTE_EXPIRE_SECONDS * 1000); |
| | | |
| | | return () => { |
| | |
| | | deps.vehicleLocation?.latitude, |
| | | deps.vehicleLocation?.longitude |
| | | ]); |
| | | |
| | | /** |
| | | * 确认修改前消费 60 秒超时标记:先重估价一次,并要求用户再次确认。 |
| | | */ |
| | | const handleConfirmRequoteIfNeeded = useCallback(() => { |
| | | if (!pendingConfirmRequote) { |
| | | return false; |
| | | } |
| | | const routeContext = resolveModifyDestinationRouteContext(deps); |
| | | if (!routeContext) { |
| | | setPendingConfirmRequote(false); |
| | | return false; |
| | | } |
| | | setPendingConfirmRequote(false); |
| | | deps.requestRouteAndFare( |
| | | routeContext.originLat, |
| | | routeContext.originLng, |
| | | routeContext.destLat, |
| | | routeContext.destLng, |
| | | { |
| | | startPointId: routeContext.startPointId, |
| | | force: true |
| | | } |
| | | ); |
| | | Taro.showToast({ |
| | | title: '报价已更新,请再次确认修改', |
| | | icon: 'none' |
| | | }); |
| | | return true; |
| | | }, [deps, pendingConfirmRequote]); |
| | | |
| | | return { |
| | | pendingConfirmRequote, |
| | | handleConfirmRequoteIfNeeded |
| | | }; |
| | | } |
| | |
| | | import Taro from '@tarojs/taro'; |
| | | import { useState } from 'react'; |
| | | import { getDistance } from '../../../../utils/Robotaxi-public-method'; |
| | | import { normalizeDistanceKm, normalizeDurationSeconds } from '../../../utils/format'; |
| | | import { TripStatus } from '../constants/trip-status'; |
| | |
| | | type UseRouteInteractionParams = { |
| | | /** 路线管理依赖。 */ |
| | | deps: Deps; |
| | | /** 当前路线选项集合。 */ |
| | | routeOptions: any[]; |
| | | /** 设置路线选项集合。 */ |
| | | setRouteOptions: React.Dispatch<React.SetStateAction<any[]>>; |
| | | /** 当前选择的路线索引。 */ |
| | | selectedRouteIndex: number; |
| | | /** 设置当前选择的路线索引。 */ |
| | | setSelectedRouteIndex: React.Dispatch<React.SetStateAction<number>>; |
| | | /** 所有路线点集合。 */ |
| | | allRoutePoints: any[][]; |
| | | /** 设置所有路线点集合。 */ |
| | | setAllRoutePoints: React.Dispatch<React.SetStateAction<any[][]>>; |
| | | /** 当前步行路线点。 */ |
| | | walkingPoints: any[]; |
| | | /** 应用主路线点。 */ |
| | |
| | | setRouteDistance: React.Dispatch<React.SetStateAction<RouteDistanceValue>>; |
| | | /** 设置路线时长。 */ |
| | | setRouteDuration: React.Dispatch<React.SetStateAction<number | null>>; |
| | | }; |
| | | |
| | | /** 路线交互 Hook 返回值。 */ |
| | | type HandleSelectRouteOptions = { |
| | | /** 是否同步更新起终点。 */ |
| | | syncSites?: boolean; |
| | | /** 是否同步更新地图视口。 */ |
| | | syncViewport?: boolean; |
| | | }; |
| | | |
| | | /** 路线交互 Hook 返回值。 */ |
| | |
| | | /** 设置所有路线点集合。 */ |
| | | setAllRoutePoints: React.Dispatch<React.SetStateAction<any[][]>>; |
| | | /** 选择路线。 */ |
| | | handleSelectRoute: (index: number) => void; |
| | | handleSelectRoute: (index: number, options?: HandleSelectRouteOptions) => void; |
| | | /** 处理 polyline 点击。 */ |
| | | handlePolylineTap: (event: any) => void; |
| | | /** 处理地图点击。 */ |
| | |
| | | export default function useRouteInteraction(params: UseRouteInteractionParams): UseRouteInteractionResult { |
| | | const { |
| | | deps, |
| | | routeOptions, |
| | | setRouteOptions, |
| | | selectedRouteIndex, |
| | | setSelectedRouteIndex, |
| | | allRoutePoints, |
| | | setAllRoutePoints, |
| | | walkingPoints, |
| | | applyRoutePoints, |
| | | reducePoints, |
| | |
| | | setRouteDistance, |
| | | setRouteDuration, |
| | | } = params; |
| | | |
| | | /** 路线选项集合。 */ |
| | | const [routeOptions, setRouteOptions] = useState<any[]>([]); |
| | | /** 当前选择的路线索引。 */ |
| | | const [selectedRouteIndex, setSelectedRouteIndex] = useState<number>(0); |
| | | /** 所有路线点集合。 */ |
| | | const [allRoutePoints, setAllRoutePoints] = useState<any[][]>([]); |
| | | |
| | | /** |
| | | * 同步地图视口,确保路线和步行线都在可视范围内。 |
| | |
| | | /** |
| | | * 处理路线选择后的业务联动。 |
| | | */ |
| | | const handleSelectRoute = (index: number) => { |
| | | const handleSelectRoute = (index: number, options: HandleSelectRouteOptions = {}) => { |
| | | if (!routeOptions[index] || index === selectedRouteIndex) { |
| | | return; |
| | | } |
| | | const { syncSites = true, syncViewport = true } = options; |
| | | |
| | | const selectedRoute = routeOptions[index]; |
| | | setSelectedRouteIndex(index); |
| | |
| | | } |
| | | |
| | | if ( |
| | | syncSites |
| | | && ( |
| | | deps.tripStatusRef.current === TripStatus.PublishRoute |
| | | && Number.isFinite(nextStartLatitude) |
| | | && Number.isFinite(nextStartLongitude) |
| | | && typeof deps.setStartSite === 'function' |
| | | ) |
| | | ) { |
| | | deps.setStartSite({ |
| | | ...deps.startSite, |
| | |
| | | } |
| | | |
| | | if ( |
| | | syncSites |
| | | && ( |
| | | deps.tripStatusRef.current === TripStatus.PublishRoute |
| | | && Number.isFinite(nextEndLatitude) |
| | | && Number.isFinite(nextEndLongitude) |
| | | && typeof deps.setEndSite === 'function' |
| | | ) |
| | | ) { |
| | | deps.setEndSite({ |
| | | ...deps.endSite, |
| | |
| | | |
| | | if (points.length > 0) { |
| | | applyRoutePoints(points); |
| | | syncRouteViewport(allRoutePoints); |
| | | if (syncViewport) { |
| | | syncRouteViewport(allRoutePoints); |
| | | } |
| | | return; |
| | | } |
| | | applyRoutePoints([]); |
| | |
| | | const handlePolylineTap = (event: any) => { |
| | | const targetIndex = resolveRouteIndexByTap(event); |
| | | if (targetIndex >= 0 && targetIndex < allRoutePoints.length) { |
| | | handleSelectRoute(targetIndex); |
| | | handleSelectRoute(targetIndex, { syncSites: false }); |
| | | } |
| | | }; |
| | | |
| | |
| | | const handleMapTap = (event: any) => { |
| | | const targetIndex = resolveRouteIndexByTap(event); |
| | | if (targetIndex >= 0 && targetIndex < allRoutePoints.length) { |
| | | handleSelectRoute(targetIndex); |
| | | handleSelectRoute(targetIndex, { syncSites: false }); |
| | | } |
| | | }; |
| | | |
| | |
| | | cachedAt: number; |
| | | }; |
| | | |
| | | /** 当前用户定位结构。 */ |
| | | export type CurrentUserLocation = { |
| | | /** 纬度。 */ |
| | | latitude: number; |
| | | /** 经度。 */ |
| | | longitude: number; |
| | | } | null; |
| | | |
| | | /** 路线管理依赖。 */ |
| | | export type Deps = { |
| | | /** 是否为 Android 端。 */ |
| | |
| | | /** 速度,单位 km/h。 */ |
| | | speedKmh: number | null; |
| | | }) => void; |
| | | /** 设置当前用户定位。 */ |
| | | setCurrentUserLocation?: (location: CurrentUserLocation) => void; |
| | | }; |
| | | |
| | | /** 默认依赖。 */ |
| | |
| | | /** 路线交互能力。 */ |
| | | const interactionState = useRouteInteraction({ |
| | | deps, |
| | | routeOptions, |
| | | setRouteOptions, |
| | | selectedRouteIndex, |
| | | setSelectedRouteIndex, |
| | | allRoutePoints, |
| | | setAllRoutePoints, |
| | | walkingPoints: renderState.walkingPoints, |
| | | applyRoutePoints: renderState.applyRoutePoints, |
| | | reducePoints: renderState.reducePoints, |
| | |
| | | import Taro from '@tarojs/taro'; |
| | | import { useRef } from 'react'; |
| | | import { useEffect, useRef } from 'react'; |
| | | import { getWalkingRoute } from '../../../../utils/Robotaxi-public-method'; |
| | | import { queryOrderRoute, queryOrderTrajectory, queryRouteAndFare } from '../../../api/begin-route'; |
| | | import { queryOrderRoute, queryOrderTrajectory, queryRouteAndFare, queryRoutePreferences } from '../../../api/begin-route'; |
| | | import { ensureSelectedCityByLocation } from '../../../utils/city'; |
| | | import { normalizeDistanceKm, normalizeDurationSeconds } from '../../../utils/format'; |
| | | import { TripStatus } from '../constants/trip-status'; |
| | |
| | | }); |
| | | |
| | | /** |
| | | * 提取路线稳定匹配键,优先使用规划策略值。 |
| | | */ |
| | | const getRouteStableKey = (item: any) => { |
| | | const routeRule = item?.primaryRoutePlanRule; |
| | | if (routeRule !== undefined && routeRule !== null && String(routeRule) !== '') { |
| | | return `rule:${String(routeRule)}`; |
| | | } |
| | | const routeId = item?.route?.routeID; |
| | | if (routeId !== undefined && routeId !== null && String(routeId) !== '') { |
| | | return `route:${String(routeId)}`; |
| | | } |
| | | return ''; |
| | | }; |
| | | |
| | | /** |
| | | * 归一化路线偏好列表。 |
| | | */ |
| | | const normalizeRoutePreferenceOptions = (response: any) => { |
| | | const list = Array.isArray(response) |
| | | ? response |
| | | : (Array.isArray(response?.list) ? response.list : (Array.isArray(response?.data) ? response.data : [])); |
| | | return list.map((item: any) => { |
| | | const label = item?.name; |
| | | const value = item?.amapStrategy; |
| | | const code = item?.code; |
| | | if (!label || value === undefined || value === null) { |
| | | return null; |
| | | } |
| | | return { |
| | | label: String(label), |
| | | value: String(value), |
| | | code: String(code), |
| | | selected: item?.selected === true |
| | | }; |
| | | }).filter(Boolean); |
| | | }; |
| | | |
| | | /** |
| | | * 管理路线请求、订单轨迹请求与步行路线请求。 |
| | | */ |
| | | export default function useRouteRequest(params: UseRouteRequestParams): UseRouteRequestResult { |
| | |
| | | const lastRouteAndFareRequestAtRef = useRef<number>(0); |
| | | /** 步行起点定位缓存引用。 */ |
| | | const walkingOriginLocationRef = useRef<WalkingOriginLocation | null>(null); |
| | | /** 最新路线选项引用,避免异步回包使用旧闭包状态。 */ |
| | | const latestRouteOptionsRef = useRef<any[]>(routeOptions); |
| | | /** 最新选中路线索引引用,避免异步回包回退到旧索引。 */ |
| | | const latestSelectedRouteIndexRef = useRef<number>(selectedRouteIndex); |
| | | /** 路线偏好缓存引用,避免重复请求。 */ |
| | | const routePreferenceCacheRef = useRef<Record<string, any[]>>({}); |
| | | |
| | | useEffect(() => { |
| | | latestRouteOptionsRef.current = routeOptions; |
| | | latestSelectedRouteIndexRef.current = selectedRouteIndex; |
| | | }, [routeOptions, selectedRouteIndex]); |
| | | |
| | | /** |
| | | * 确保当前路线请求具备城市上下文。 |
| | |
| | | cachedLocation |
| | | && now - cachedLocation.cachedAt <= WALKING_ORIGIN_LOCATION_CACHE_DURATION_MS |
| | | ) { |
| | | deps.setCurrentUserLocation?.({ |
| | | latitude: cachedLocation.latitude, |
| | | longitude: cachedLocation.longitude |
| | | }); |
| | | return cachedLocation; |
| | | } |
| | | |
| | |
| | | |
| | | const nextLocation: WalkingOriginLocation = { latitude, longitude, cachedAt: now }; |
| | | walkingOriginLocationRef.current = nextLocation; |
| | | deps.setCurrentUserLocation?.({ latitude, longitude }); |
| | | return nextLocation; |
| | | }; |
| | | |
| | | /** |
| | | * 根据最新已选路线,在新路线集合中解析应保留的索引。 |
| | | */ |
| | | const resolveSelectedRouteIndex = (options: any[]) => { |
| | | if (!Array.isArray(options) || options.length === 0) { |
| | | return -1; |
| | | } |
| | | const latestRouteOptions = latestRouteOptionsRef.current; |
| | | const latestSelectedRouteIndex = latestSelectedRouteIndexRef.current; |
| | | const currentSelectedRoute = latestRouteOptions[latestSelectedRouteIndex]; |
| | | const currentStableKey = getRouteStableKey(currentSelectedRoute); |
| | | if (currentStableKey) { |
| | | const matchedIndex = options.findIndex((item) => getRouteStableKey(item) === currentStableKey); |
| | | if (matchedIndex !== -1) { |
| | | return matchedIndex; |
| | | } |
| | | } |
| | | const currentRouteId = currentSelectedRoute?.route?.routeID; |
| | | if (currentRouteId !== undefined && currentRouteId !== null && String(currentRouteId) !== '') { |
| | | const matchedIndex = options.findIndex((item) => String(item?.route?.routeID ?? '') === String(currentRouteId)); |
| | | if (matchedIndex !== -1) { |
| | | return matchedIndex; |
| | | } |
| | | } |
| | | return 0; |
| | | }; |
| | | |
| | | /** |
| | | * 读取当前城市的路线偏好,优先命中内存缓存。 |
| | | */ |
| | | const getRoutePreferenceOptions = async () => { |
| | | const openCityId = Number(Taro.getStorageSync('selectedCity')?.id || 0); |
| | | const cacheKey = String(openCityId); |
| | | if (routePreferenceCacheRef.current[cacheKey]) { |
| | | return routePreferenceCacheRef.current[cacheKey]; |
| | | } |
| | | try { |
| | | const response = await queryRoutePreferences({ openCityId }); |
| | | const normalizedOptions = normalizeRoutePreferenceOptions(response); |
| | | routePreferenceCacheRef.current[cacheKey] = normalizedOptions; |
| | | return normalizedOptions; |
| | | } catch (error) { |
| | | routePreferenceCacheRef.current[cacheKey] = []; |
| | | return []; |
| | | } |
| | | }; |
| | | |
| | | /** |
| | | * 首次请求路线时,按偏好策略确定默认选中项。 |
| | | */ |
| | | const resolveInitialSelectedRouteIndex = async (options: any[]) => { |
| | | if (!Array.isArray(options) || options.length === 0) { |
| | | return -1; |
| | | } |
| | | const preferenceOptions = await getRoutePreferenceOptions(); |
| | | const selectedPreferenceOption = preferenceOptions.find((item: any) => item?.selected === true); |
| | | if (!selectedPreferenceOption) { |
| | | return 0; |
| | | } |
| | | const matchedIndex = options.findIndex((item: any) => ( |
| | | String(item?.primaryRoutePlanRule ?? '') === String(selectedPreferenceOption.value) |
| | | )); |
| | | return matchedIndex >= 0 ? matchedIndex : 0; |
| | | }; |
| | | |
| | | /** |
| | |
| | | |
| | | let routeData = options[0] || null; |
| | | let allRoutePointGroups: any[][] = []; |
| | | |
| | | if (refreshMode === 'price_only') { |
| | | const currentRouteId = routeOptions[selectedRouteIndex]?.route?.routeID; |
| | | routeData = options.find((item: any) => item?.route?.routeID === currentRouteId) || options[0] || null; |
| | | const resolvedSelectedRouteIndex = resolveSelectedRouteIndex(options); |
| | | routeData = options[resolvedSelectedRouteIndex] || options[0] || null; |
| | | if (options.length > 0) { |
| | | setRouteOptions((previousRouteOptions) => { |
| | | if (!Array.isArray(previousRouteOptions) || previousRouteOptions.length === 0) { |
| | | return options; |
| | | } |
| | | const optionMap = new Map( |
| | | options.map((item: any) => [item?.route?.routeID, item]) |
| | | options.map((item: any) => [getRouteStableKey(item), item]) |
| | | ); |
| | | return previousRouteOptions.map((oldItem: any, index: number) => { |
| | | const matchedOption = optionMap.get(oldItem?.route?.routeID) || options[index]; |
| | | const matchedOption = optionMap.get(getRouteStableKey(oldItem)) || options[index]; |
| | | if (!matchedOption) { |
| | | return oldItem; |
| | | } |
| | |
| | | } |
| | | } else { |
| | | if (options.length > 0) { |
| | | const hasCurrentSelectedRoute = Array.isArray(latestRouteOptionsRef.current) && latestRouteOptionsRef.current.length > 0; |
| | | const resolvedSelectedRouteIndex = hasCurrentSelectedRoute |
| | | ? resolveSelectedRouteIndex(options) |
| | | : await resolveInitialSelectedRouteIndex(options); |
| | | setRouteOptions(options); |
| | | setSelectedRouteIndex(0); |
| | | setSelectedRouteIndex(resolvedSelectedRouteIndex); |
| | | allRoutePointGroups = options.map((item) => ( |
| | | Array.isArray(item?.route?.polyline) ? item.route.polyline : [] |
| | | )); |
| | | setAllRoutePoints(allRoutePointGroups); |
| | | routeData = options[resolvedSelectedRouteIndex] || options[0] || null; |
| | | } else { |
| | | setRouteOptions([]); |
| | | setAllRoutePoints([]); |
| | |
| | | arriveCalloutVisible: boolean; |
| | | /** 到达气泡样式 */ |
| | | arriveCalloutStyle: any; |
| | | /** 路线摘要气泡可见 */ |
| | | routeSummaryCalloutVisible: boolean; |
| | | /** 路线摘要气泡样式 */ |
| | | routeSummaryCalloutStyle: any; |
| | | /** 到达态气泡渲染就绪(角度与缩放均就绪) */ |
| | | arrivedTooltipReady: boolean; |
| | | /** 路线距离 */ |
| | |
| | | const renderMarkerCallout = (item: any, index: number) => { |
| | | const calloutKey = `callout-${item.id}-${item.name || ''}`; |
| | | // 路线摘要气泡需跨平台强制显示,避免被 Android 普通气泡隐藏规则误伤 |
| | | if (item.isRouteSummaryBubble) return renderDefaultCallout(item, calloutKey); |
| | | if (item.isRouteSummaryBubble) { |
| | | return deps.isAndroid ? null : renderDefaultCallout(item, calloutKey); |
| | | } |
| | | if (shouldHideAndroidMarkerCallout && !item.isBeforeUserSite) return null; |
| | | const isWaiting = [TripStatus.Publishing, TripStatus.CallingCar].includes(deps.tripStatus) && index === 0; |
| | | if (isWaiting) return renderWaitingCallout(item, calloutKey); |
| | |
| | | {deps.shouldShowWaitingRadar && deps.waitingRadarVisible && ( |
| | | <Image className="waiting-radar-img" src={deps.waitingRadarSrc} webp={true} style={deps.waitingRadarStyle} /> |
| | | )} |
| | | {deps.isAndroid && deps.routeSummaryCalloutVisible && routeSummaryText && ( |
| | | <View className="route-summary-callout" style={deps.routeSummaryCalloutStyle}> |
| | | <Image className="route-summary-callout-bg" src={summaryIcon} /> |
| | | <View className="route-summary-callout-text"> |
| | | {routeSummaryText} |
| | | </View> |
| | | </View> |
| | | )} |
| | | {deps.isAndroid && deps.carCalloutVisible && ( |
| | | <View className="car-callout-android" style={deps.carCalloutStyle}> |
| | | <View className="custom-callout-content car-callout-with-before" style={{ padding: '6px 10px' }}> |
| | |
| | | import Taro, { useDidHide, useDidShow, useShareAppMessage } from "@tarojs/taro" |
| | | import Taro, { useDidShow, useShareAppMessage } from "@tarojs/taro" |
| | | import { useEffect, useState, useRef, useMemo } from 'react'; |
| | | import { View, Image } from '@tarojs/components'; |
| | | import './index.less'; |
| | |
| | | import useAndroidOverlays from './hooks/use-android-overlays'; |
| | | // 地图视口状态管理 |
| | | import useMapViewport from './hooks/use-map-viewport'; |
| | | import useAutoRouteViewport from './hooks/use-auto-route-viewport'; |
| | | import useActiveTripScale from './hooks/use-active-trip-scale'; |
| | | // 页面恢复与初始化编排 |
| | | import usePageRestore from './hooks/use-page-restore'; |
| | | // 页面动作编排 |
| | |
| | | import { toTripStatus, TripStatus } from './constants/trip-status'; |
| | | import { MODIFY_DESTINATION_SOCKET_STATUS_KEY } from './constants/modify-destination'; |
| | | import { getDistance } from '../../../utils/Robotaxi-public-method'; |
| | | import { CurrentUserLocation } from './hooks/use-route-manager.shared'; |
| | | import { MODIFY_DESTINATION_SESSION_KEY } from './constants/modify-destination'; |
| | | |
| | | import backIcon from '../../../robotaxi/assets/back-icon.png' |
| | | import personalCenter from '../../../robotaxi/assets/personal-center.png' |
| | |
| | | durationSeconds: null, |
| | | speedKmh: null |
| | | }); |
| | | const [currentUserLocation, setCurrentUserLocation] = useState<CurrentUserLocation>(null); // 当前用户真实定位,避免被地图中心点覆盖 |
| | | const [modifyDestinationSocketStatus, setModifyDestinationSocketStatus] = useState<number | null>(null); // 后端推送的修改目的地展示状态 |
| | | |
| | | const tripStatusRef = useRef(tripStatus); // 行程状态引用 |
| | |
| | | setBeforeUserEndSite, |
| | | updateVehicleLocationThrottled: vehicleState.updateVehicleLocationThrottled, |
| | | setVehicleDirection, |
| | | setOrderRouteMetrics |
| | | setOrderRouteMetrics, |
| | | setCurrentUserLocation |
| | | }); // 路线与费用状态容器 |
| | | const { |
| | | polyline, |
| | |
| | | routeOptions, |
| | | setRouteOptions, |
| | | selectedRouteIndex, |
| | | handleSelectRoute, |
| | | handlePolylineTap, |
| | | handleMapTap, |
| | | handleSelectRoute: baseHandleSelectRoute, |
| | | handlePolylineTap: baseHandlePolylineTap, |
| | | handleMapTap: baseHandleMapTap, |
| | | fareEstimate, |
| | | setFareEstimate, |
| | | routeAndFareError, |
| | |
| | | setOriginOpacity, |
| | | showLocationFlag, |
| | | tripStatus6ViewportLockedRef, |
| | | holdDynamicScale, |
| | | getDynamicScaleHoldUntil, |
| | | updateMapViewportPadding, |
| | | toOrigin, |
| | | getLocationInfo, |
| | | computeBottomPadding, |
| | | resolveSafeTopInset |
| | | } = viewportState; // 视口状态解构 |
| | | const autoRouteViewport = useAutoRouteViewport({ |
| | | const activeTripScale = useActiveTripScale({ |
| | | tripStatus, |
| | | routePoints, |
| | | speedKmh: orderRouteMetrics?.speedKmh, |
| | | mapScale, |
| | | setMapScale: viewportState.setMapScale, |
| | | computeBottomPadding, |
| | | resolveSafeTopInset |
| | | }); // 自动路线视口巡航编排 |
| | | resolveSafeTopInset, |
| | | holdDynamicScale, |
| | | getDynamicScaleHoldUntil |
| | | }); // 行程中速度缩放与 ORDER_UPDATE_LINE 视口编排 |
| | | const arrivedMarkerReady = tripStatus !== TripStatus.ArrivedDestination || typeof vehicleDirection === 'number'; |
| | | const arrivedTooltipReady = tripStatus !== TripStatus.ArrivedDestination || (arrivedMarkerReady && mapScale >= 20); |
| | | const markerState = useMapMarkers({ |
| | |
| | | carCalloutVisible, |
| | | arriveCalloutStyle, |
| | | arriveCalloutVisible, |
| | | routeSummaryCalloutStyle, |
| | | routeSummaryCalloutVisible, |
| | | handleRegionChange: handleAndroidOverlayRegionChange |
| | | } = useAndroidOverlays({ |
| | | shouldShowWaitingRadar, |
| | | isAndroid, |
| | | tripStatus, |
| | | mapScale, |
| | | routeSummaryLocation: routeSummaryMarker ? { |
| | | latitude: routeSummaryMarker.latitude, |
| | | longitude: routeSummaryMarker.longitude |
| | | } : null, |
| | | startSite, |
| | | endSite: effectiveEndSite, |
| | | vehicleLocation, |
| | |
| | | polylineKeyRef.current = ''; |
| | | lastPolylineUpdateAtRef.current = 0; |
| | | }; |
| | | /** |
| | | * 手动切线前先暂停速度缩放,避免一次性全览后立即被速度接管。 |
| | | */ |
| | | const handleSelectRoute = (index: number, options?: any) => { |
| | | holdDynamicScale(); |
| | | baseHandleSelectRoute(index, options); |
| | | }; |
| | | |
| | | /** |
| | | * 处理路线 polyline 点击切换。 |
| | | */ |
| | | const handlePolylineTap = (event: any) => { |
| | | holdDynamicScale(); |
| | | baseHandlePolylineTap(event); |
| | | }; |
| | | |
| | | /** |
| | | * 处理地图点击切换路线。 |
| | | */ |
| | | const handleMapTap = (event: any) => { |
| | | holdDynamicScale(); |
| | | baseHandleMapTap(event); |
| | | }; |
| | | |
| | | const { |
| | | applyRouteSnapshot: applySmoothRouteSnapshot, |
| | | resetSmoothRouteMotion |
| | |
| | | erasePassedRouteSegments: routeErasePassedRouteSegments, |
| | | applyRoutePoints: routeApplyRoutePoints, |
| | | updateVehicleLocationThrottled: vehicleState.updateVehicleLocationThrottled, |
| | | onOrderUpdateLineRendered: autoRouteViewport.handleOrderUpdateLineRendered, |
| | | onOrderUpdateLineRendered: activeTripScale.handleOrderUpdateLineRendered, |
| | | applySmoothRouteSnapshot, |
| | | resetSmoothRouteMotion |
| | | }); |
| | |
| | | bluetoothAuthSuccess, |
| | | startBluetoothAuth |
| | | }); |
| | | useModifyDestinationPreviewRoute({ |
| | | const { handleConfirmRequoteIfNeeded } = useModifyDestinationPreviewRoute({ |
| | | tripStatus, |
| | | isModifyDestinationReviewVisible, |
| | | hasLocalDestinationOverride, |
| | |
| | | setPageDidShowVersion((v) => v + 1); |
| | | handleDidShow(router.params?.skipPageFlag); |
| | | updateMapViewportPadding(); |
| | | autoRouteViewport.resume(); |
| | | }); |
| | | useDidHide(() => { |
| | | autoRouteViewport.dispose(); |
| | | }); |
| | | |
| | | // 打开评价弹窗 |
| | |
| | | } |
| | | |
| | | const handleRegionChange = (e) => { |
| | | autoRouteViewport.handleRegionChange(e); |
| | | activeTripScale.handleRegionChange(e); |
| | | handleAndroidOverlayRegionChange(e); |
| | | }; |
| | | |
| | |
| | | const handleAcknowledgeModifyDestinationFail = () => { |
| | | setShowModifyDestinationFailPopup(false); |
| | | setModifyDestinationFailMessage(''); |
| | | const modifyDestinationSession = Taro.getStorageSync(MODIFY_DESTINATION_SESSION_KEY); |
| | | const fallbackPreviousEndSite = modifyDestinationSession?.previousEndSite; |
| | | if ( |
| | | fallbackPreviousEndSite |
| | | && Number.isFinite(Number(fallbackPreviousEndSite?.latitude)) |
| | | && Number.isFinite(Number(fallbackPreviousEndSite?.longitude)) |
| | | ) { |
| | | Taro.setStorageSync('endSite', fallbackPreviousEndSite); |
| | | setEndSite(fallbackPreviousEndSite); |
| | | } |
| | | exitModifyDestinationReview(); |
| | | setIsRouteSelectExpanded(false); |
| | | setRouteOptions([]); |
| | |
| | | |
| | | if (hitDistanceLimit || hitDurationLimit) { |
| | | openModifyDestinationFailPopup('即将到达终点,已无法修改目的地'); |
| | | return; |
| | | } |
| | | if (handleConfirmRequoteIfNeeded()) { |
| | | return; |
| | | } |
| | | try { |
| | |
| | | fareEstimate?.couponId || fareEstimate?.userCouponId || Taro.getStorageSync('selectedCouponId') || '' |
| | | ); |
| | | const couponOrderMoney = Number(fareEstimate?.originalFare || fareEstimate?.finalFare || 0); // 优惠券可用性计算订单金额 |
| | | const showWalkNavigationButton = useMemo(() => { |
| | | const canWalkNavigateToPickup = useMemo(() => { |
| | | if (isModifyDestinationReviewVisible) { |
| | | return false; |
| | | } |
| | |
| | | } |
| | | const startLatitude = Number(startSite?.latitude); |
| | | const startLongitude = Number(startSite?.longitude); |
| | | const currentLatitude = Number(latitude); |
| | | const currentLongitude = Number(longitude); |
| | | const currentLatitude = Number(currentUserLocation?.latitude); |
| | | const currentLongitude = Number(currentUserLocation?.longitude); |
| | | if (!Number.isFinite(startLatitude) || !Number.isFinite(startLongitude)) { |
| | | return false; |
| | | } |
| | |
| | | } |
| | | const walkingDistance = getDistance(currentLatitude, currentLongitude, startLatitude, startLongitude); |
| | | return Number.isFinite(walkingDistance) && walkingDistance >= walkingNavigationDistance; |
| | | }, [isModifyDestinationReviewVisible, tripStatus, startSite?.latitude, startSite?.longitude, latitude, longitude, walkingNavigationDistance]); |
| | | }, [isModifyDestinationReviewVisible, tripStatus, startSite?.latitude, startSite?.longitude, currentUserLocation?.latitude, currentUserLocation?.longitude, walkingNavigationDistance]); |
| | | const showWalkNavigationButton = canWalkNavigateToPickup; // 步行入口展示与点击统一使用真实用户定位语义 |
| | | |
| | | /** |
| | | * 接驾态下补充刷新当前用户真实定位,避免按钮显隐依赖步行路线请求时机。 |
| | | */ |
| | | useEffect(() => { |
| | | const shouldRefreshWalkLocation = [TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup].includes(tripStatus); |
| | | if (!shouldRefreshWalkLocation) { |
| | | return; |
| | | } |
| | | if (!startSite?.latitude || !startSite?.longitude) { |
| | | return; |
| | | } |
| | | if (currentUserLocation?.latitude && currentUserLocation?.longitude) { |
| | | return; |
| | | } |
| | | let cancelled = false; |
| | | Taro.getLocation({ |
| | | type: 'gcj02', |
| | | isHighAccuracy: true, |
| | | highAccuracyExpireTime: 35 * 1000 |
| | | } as any).then((location) => { |
| | | if (cancelled) { |
| | | return; |
| | | } |
| | | const nextLatitude = Number(location?.latitude); |
| | | const nextLongitude = Number(location?.longitude); |
| | | if (!Number.isFinite(nextLatitude) || !Number.isFinite(nextLongitude)) { |
| | | return; |
| | | } |
| | | setCurrentUserLocation({ |
| | | latitude: nextLatitude, |
| | | longitude: nextLongitude |
| | | }); |
| | | }).catch(() => { |
| | | }); |
| | | return () => { |
| | | cancelled = true; |
| | | }; |
| | | }, [tripStatus, startSite?.latitude, startSite?.longitude, currentUserLocation?.latitude, currentUserLocation?.longitude]); |
| | | |
| | | const { renderMapOverlays, renderTripCard } = useViewDelegates({ |
| | | isAndroid, |
| | | tripStatus, |
| | |
| | | carCalloutStyle, |
| | | arriveCalloutVisible, |
| | | arriveCalloutStyle, |
| | | routeSummaryCalloutVisible, |
| | | routeSummaryCalloutStyle, |
| | | arrivedTooltipReady, |
| | | routeDistance, |
| | | routeDuration, |
| | |
| | | toPersonalCenter(); |
| | | }; |
| | | const handleWalkNavigation = () => { |
| | | if (![TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup].includes(tripStatus) || !startSite?.latitude || !startSite?.longitude) { |
| | | if (!canWalkNavigateToPickup || !startSite?.latitude || !startSite?.longitude) { |
| | | return; |
| | | } |
| | | |
| | |
| | | <View className='order-action-row'> |
| | | { |
| | | formatOrderStatus(item.status) == '待支付' && |
| | | <View onClick={(e) => e.stopPropagation()}> |
| | | <View onClick={(e) => { e.stopPropagation(); againOrder(item) }}> |
| | | <View className='order-pay-text' hoverClass='common-hover-class'>去支付</View> |
| | | </View> |
| | | } |
| | | { |
| | | ['已完成', '已取消'].includes(formatOrderStatus(item.status)) && |
| | | ['已完成', '已取消', '待评价'].includes(formatOrderStatus(item.status)) && |
| | | <View onClick={(e) => { e.stopPropagation(); againOrder(item) }}> |
| | | <View className='order-again-text' hoverClass='common-hover-class'>再来一单</View> |
| | | </View> |
| | |
| | | try { |
| | | const res = await getCollectPoints({ |
| | | pageNum: page, |
| | | pageSize: 4, |
| | | pageSize: 999, |
| | | }); |
| | | if (res && res.records) { |
| | | setList(prev => page === 1 ? res.records : [...prev, ...res.records]); |
| | |
| | | }; |
| | | |
| | | const checkCollectLimit = async () => { |
| | | // 只取总数即可,避免为校验拉完整列表 |
| | | // 只取总数即可 |
| | | const res = await getCollectPoints({ |
| | | pageNum: 1, |
| | | pageSize: 1 |
| | | pageSize: 999, |
| | | }); |
| | | |
| | | if (Number(res?.total) >= MAX_COLLECT_COUNT) { |
| | |
| | | </View> |
| | | </View> |
| | | {showDistance ? |
| | | <View className='collect-box'> |
| | | <Image onClick={(e) => { |
| | | <View |
| | | className='collect-box' |
| | | hoverStopPropagation |
| | | hoverClass='common-hover-class' |
| | | onClick={(e) => { |
| | | e.stopPropagation(); |
| | | handleCollect(e); |
| | | }} className='collect-icon' src={isCollected ? starSrc : noStarSrc} /> |
| | | }}> |
| | | <Image className='collect-icon' src={isCollected ? starSrc : noStarSrc} /> |
| | | {renderHighlightedText(item.distance || '-', 'history-distance')} |
| | | </View> |
| | | : null} |
| | |
| | | MODIFY_DESTINATION_SESSION_KEY |
| | | } from '../begin-route/constants/modify-destination'; |
| | | |
| | | |
| | | /** |
| | | * 安全解析路由关键字,兼容已编码和未编码两种入参。 |
| | | */ |
| | | const decodeRouteKeyword = (keyword: unknown) => { |
| | | const normalizedKeyword = String(keyword || '').trim(); |
| | | if (!normalizedKeyword) { |
| | | return ''; |
| | | } |
| | | try { |
| | | return decodeURIComponent(normalizedKeyword); |
| | | } catch (error) { |
| | | return normalizedKeyword; |
| | | } |
| | | }; |
| | | |
| | | export default function SiteSearchPage() { |
| | | const [searchValue, setSearchValue] = useState('') //搜索值 |
| | |
| | | const MAX_EXPANDED_COUNT = 10 |
| | | |
| | | const router = Taro.useRouter(); |
| | | const initialKeyword = String(router.params?.keyword || '').trim(); |
| | | const initialKeyword = decodeRouteKeyword(router.params?.keyword); |
| | | const { |
| | | pageType, |
| | | siteList, |