docs/修改目的地流程梳理.md
New file @@ -0,0 +1,218 @@ # 行程中修改目的地 — 完整逻辑梳理 --- ## 一、整体流程概览 ``` 用户点击"修改目的地" → 写入 session 缓存 → 跳转选点页 → 用户选终点 → 写入 candidate 缓存 → reLaunch 回行程页 → 页面恢复 + 预览态恢复 → 展示预览确认卡片(新路线 + 新费用) → 用户确认 / 取消 → 确认:调后端 orderChange → 更新终点 → 退出预览态 → 取消:恢复旧终点 → 退出预览态 ``` --- ## 二、入口条件 | 条件 | 说明 | |------|------| | 按钮可见 | `tripStatus` 为接驾中(4)、车辆已到达(5)、行程中(6) | | 按钮可点击(非置灰) | 以下条件全部不命中:<br>① socket 推送 status=0(临近终点)<br>② 剩余距离 < `modifyDistanceLimitMeters`<br>③ 剩余时长 < `modifyDurationLimitMinutes` | **代码位置**:`index.tsx` 第 1139-1177 行 `modifyDestinationDisabledToastText` --- ## 三、阶段一:启动流程(handleStartModifyDestination) **文件**:`hooks/use-modify-destination-flow.ts` 1. 写入 **session 缓存**(storage key: `modifyDestinationSession`): ```json { "active": true, "previousEndSite": "<当前终点>", "previousFareEstimate": "<当前费用>", "orderId": "<当前订单ID>", "tripStatus": "<当前行程状态>", "updatedAt": 1234567890 } ``` 2. 写入 **路线点缓存**(`modifyDestinationRoutePoints`):当前路线点 → 用于取消后回滚路线 3. 清空 **候选终点缓存**(`modifyDestinationCandidate`) 4. 设置流程状态 → `SELECTING` 5. 调用 `toSelectSite('end')` → `Taro.navigateTo` 到站点搜索页 --- ## 四、阶段二:选点页选终点 **文件**:`pages/site-search/index.tsx`、`pages/select-map-site/hooks/use-select-map-site.ts` 用户选择新终点后: 1. 判断新终点是否与原终点相同 → 写入 `modifyDestinationPendingSameSiteToast` 标记 2. 将选中站点写入 **候选终点缓存**(`modifyDestinationCandidate`) 3. 写入跳过空调查询标记(`modifyDestinationSkipAcQueryOnReturn`) 4. 通过 **`Taro.reLaunch`** 返回行程页: ``` /robotaxi/pages/begin-route/index?skipPageFlag=1&fromModifyDestination=1 ``` > 注意:是 reLaunch(销毁全部页面栈重新创建),不是 navigateBack --- ## 五、阶段三:行程页恢复 + 预览态建立 ### 5.1 页面恢复(use-page-restore.ts → restorePageStateFromStorage) 页面 `useDidShow` 触发后: 1. `ensurePageSocketReady()` 确保 WebSocket 连接 2. 如果 `fromModifyDestination=1` + 有相同终点 toast 标记 → 弹提示 3. 调用 `restorePageStateFromStorage()`: - 读取 storage 中的 `startSite/endSite/orderId/tripStatus` - 如果 `tripStatus` 有值且不等于 1 → **调用 `getOrderActive()` 接口** - 接口返回数据 → 用真实订单数据更新页面状态 - ⚠️ 接口返回空 → **清缓存并 redirect 到首页**(模拟器环境下无真实订单会踩到此分支) ### 5.2 预览态恢复(use-modify-destination-flow.ts → useEffect) 依赖 `[tripStatus, orderId, endSite]` 变化后执行: 1. 读取缓存快照(session + candidate + override + routePoints) 2. 调用 `resolveFlowState` 推导状态: - session.active + orderId 匹配 + candidate 有效坐标 → `PREVIEWING` - session.active + 无 candidate → `SELECTING` - 其他 → `IDLE` 3. 如果 candidate 与 previousEndSite 相同 → 自动取消(触发 `onCancelReview`) 4. 否则 → `applyPreviewingState`:设置 `previewEndSite`,`isModifyDestinationReviewVisible = true` ### 5.3 预览路线请求(use-modify-destination-preview-route.ts) `isModifyDestinationReviewVisible` 变为 true 后: 1. 确定起点: - 接驾中/车辆已到达 → 使用 `startSite`(上车点) - 行程中 → 使用 `vehicleLocation`(车辆实时位置) 2. 调用 `requestRouteAndFare` 请求新路线 + 费用估算 3. 启动 **60 秒重估价倒计时**:超时后用户再点确认需要先刷新报价 --- ## 六、阶段四:确认修改(handleConfirmModifyDestinationWithGuard) **文件**:`index.tsx` 第 1855-1896 行 ### 前置校验(任一命中则中断): 1. socket status === 0 → 弹失败弹窗 + 退出预览态 2. 距离/时长阈值校验(`shouldBypassLocalLimit` 为 false 时)→ 弹失败弹窗 3. `pendingConfirmRequote`(60s 超时)→ 重新估价 + 要求二次确认 ### 提交流程(handleConfirmModifyDestination): 1. 锁定按钮(`isConfirmingModifyDestination = true`) 2. 全局 loading:「正在修改目的地」 3. 调用 API:`POST /itinerary/api/v1/mini-program/order/orderChange` ```json { "endPointId": "新终点站点ID", "orderId": "订单ID", "selectedRouteId": "选中路线ID", "selectedRoutePlanRule": "路线规划策略", "navigationSnapshotId": "导航快照ID" } ``` 4. **成功**: - 构建新终点 → 更新 storage + state - `exitModifyDestinationReview({ shouldRestorePreviousEndSite: false })` - showToast「行程已更新」 - 触发 `onModifySuccess`:清空路线选择 + 重新拉取后端行程路线 5. **失败**:弹修改失败弹窗 ### 确认按钮禁用条件: - 提交请求中 - 修改次数查询中 / 未就绪 - 路线关键参数未就绪(routeID / routePlanRule / snapshotId) - 剩余修改次数为 0 --- ## 七、阶段五:取消/退出预览态 | 触发场景 | 来源 | 行为 | |---------|------|------| | 点击返回按钮 | `handleBack` | 恢复旧终点 + 旧路线 → 退出预览态 | | socket 推送不可修改 | `effectiveModifyDestinationSocketStatus === 0` | 恢复 + 弹失败弹窗 | | 新旧终点相同 | 恢复流程中自动检测 | 触发 `onCancelReview` 恢复 | | 失败弹窗确认 | `handleAcknowledgeModifyDestinationFail` | 恢复旧路线 | | 页面离开/销毁 | `beforeBackRef` | 清理所有修改目的地缓存 | | 预览态内点"编辑" | `onEditDestination` | 重新执行 `handleStartModifyDestination`(跳回选点页) | **`exitModifyDestinationReview` 核心逻辑**: - 参数 `shouldRestorePreviousEndSite`(默认 true):从 session 取旧终点恢复给页面 - 清空所有内存 state(previewEndSite / previousEndSite / previousFareEstimate / previousRoutePoints) - 清空所有 storage 缓存(session / candidate / override / routePoints) - 清空本地终点覆盖(overrideEndSite) - 流程状态 → `IDLE` --- ## 八、关键 Storage Keys 一览 | Key | 用途 | 生命周期 | |-----|------|---------| | `modifyDestinationSession` | 修改会话(active / previousEndSite / orderId) | 进入选点页 → 确认/取消后清除 | | `modifyDestinationCandidate` | 候选新终点(选点页写入) | 选点后写入 → 预览态读取后消费 | | `modifyDestinationRoutePoints` | 旧路线点快照 | 启动时写入 → 取消后用于回滚路线 | | `modifyDestinationOverride` | 本地终点覆盖值 | 修改确认后写入 → 后端推送同步后清除 | | `modifyDestinationSkipAcQueryOnReturn` | 返回时跳过空调查询 | 选点返回时写入 → 消费后清除 | | `modifyDestinationSocketStatus` | socket 推送的修改资格状态 | 实时更新 | | `modifyDestinationPendingSameSiteToast` | 新旧终点相同 toast 标记 | 选点时写入 → 返回后消费 | --- ## 九、涉及的核心文件 | 文件 | 职责 | |------|------| | `hooks/use-modify-destination-flow.ts` | 流程状态管理、缓存读写、预览态恢复/退出 | | `hooks/use-modify-destination-preview-route.ts` | 预览路线请求 + 60s 重估价倒计时 | | `hooks/use-page-restore.ts` | 页面恢复编排(含 `getOrderActive()` 守卫) | | `hooks/use-view-delegates.tsx` | 视图层委托(修改目的地确认卡片渲染) | | `components/modify-destination-review.tsx` | 预览确认 UI 卡片组件 | | `components/modify-destination-fail-popup.tsx` | 修改失败弹窗 | | `components/calling-car-modules/trip-action-bar.tsx` | 底部操作栏(修改目的地按钮) | | `pages/site-search/index.tsx` | 站点搜索页(选终点入口之一) | | `pages/select-map-site/hooks/use-select-map-site.ts` | 地图选点页(选终点入口之二) | | `constants/modify-destination.ts` | 所有 storage key 常量 + `isSameModifyDestinationSite` 工具函数 | | `api/begin-route.ts` | `orderChange` API(修改目的地后端接口) | --- ## 十、模拟器环境已知问题 **问题**:在 DebugOrderSimulatorPanel 模拟器中,订单走到送驾状态后点击「修改目的地」,选完终点返回行程页时会自动退出回首页。 **根因**: 1. 模拟器仅注入 socket 事件驱动前端状态,不会在后端创建真实订单 2. 选点页返回后 reLaunch 重建行程页 → `useDidShow` → `restorePageStateFromStorage` 3. 因为 storage 中 `tripStatus = 6`(行程中),满足查询条件,调用 `getOrderActive()` 接口 4. 接口查不到活跃订单(模拟器无真实订单)→ 返回 null 5. 代码认为「订单已结束」→ 清缓存 + `Taro.redirectTo('/pages/robotaxi/home/index')` **代码位置**:`hooks/use-page-restore.ts` 第 288-297 行 **修复建议**:在 `syncSitesFromActiveOrder` 中增加对模拟订单 ID(`SIM-` 前缀)的豁免,跳过 `getOrderActive()` 调用。 src/robotaxi/pages/begin-route/components/css-folder/calling-car.less
@@ -498,6 +498,7 @@ &-item { display: flex; align-items: center; box-sizing: border-box; &-disabled { @@ -511,6 +512,7 @@ } &-destination { .trip-action-bar-item-icon { width: 44px; height: 44px; @@ -520,7 +522,7 @@ &-text { flex-shrink: 0; font-size: 24px; line-height: 40px; line-height: 24px; color: rgba(0, 0, 0, 0.9); } } @@ -794,4 +796,4 @@ height: 17.5px; } } } } src/robotaxi/pages/begin-route/components/evaluate-popup.tsx
@@ -81,7 +81,11 @@ }; const evaluateDimension = evaluateDimensionMap[currentOrderRating] || 3; // 接口维度:1 满意,2 一般,3 不满意 const shouldRequireFeedbackType = !hideFeedbackType; const canSubmit = shouldRequireFeedbackType ? selectedTypeIds.length > 0 && !!feedbackContent.trim() : !!feedbackContent.trim(); const shouldHideFeedbackTextarea = submitMode === 'comment' && [2, 3].includes(currentOrderRating); const hasFeedbackContent = !!feedbackContent.trim(); const canSubmit = shouldRequireFeedbackType ? selectedTypeIds.length > 0 && (shouldHideFeedbackTextarea || hasFeedbackContent) : (shouldHideFeedbackTextarea || hasFeedbackContent); const normalizeFeedbackTypeId = (typeId: unknown) => { if (typeId === null || typeId === undefined || typeId === '') { return ''; @@ -120,7 +124,7 @@ }); return; } if (!feedbackContent.trim()) { if (!shouldHideFeedbackTextarea && !feedbackContent.trim()) { Taro.showToast({ title: '请输入问题描述', icon: 'none' @@ -221,16 +225,18 @@ </View> </View> )} <View className='evaluate-problem-desc-box'> <Textarea className='evaluate-problem-desc' maxlength={500} placeholderClass='placeholder-class' placeholder={popupDisplayConfig.placeholder} value={feedbackContent} onInput={(e) => setFeedbackContent(e.detail.value)} /> <View className='evaluate-problem-desc-count'>{feedbackContent.length}/500</View> </View> {!shouldHideFeedbackTextarea && ( <View className='evaluate-problem-desc-box'> <Textarea className='evaluate-problem-desc' maxlength={500} placeholderClass='placeholder-class' placeholder={popupDisplayConfig.placeholder} value={feedbackContent} onInput={(e) => setFeedbackContent(e.detail.value)} /> <View className='evaluate-problem-desc-count'>{feedbackContent.length}/500</View> </View> )} <View className='evaluate-problem-pic-box'> {pictures.map((p, idx) => ( <View key={idx} className='evaluate-problem-pic-box-item'> src/robotaxi/pages/begin-route/hooks/use-map-viewport.ts
@@ -395,8 +395,10 @@ return Promise.all([getMapScale(), getMapCenterLocation()]) .then(([scaleRes, centerRes]) => { const actualScale = Number(scaleRes?.scale); const roundedScale = Number.isFinite(actualScale) ? Math.round(actualScale) : null; if (Number.isFinite(actualScale)) { const roundedScale = Math.round(actualScale); setMapScale(prev => (prev === roundedScale ? prev : roundedScale)); } const centerLatitude = Number(centerRes?.latitude); @@ -409,7 +411,7 @@ } }) .catch(() => {}); }, [getMapScale, getMapCenterLocation, setMapScale, deps.setLatitude, deps.setLongitude]); }, [getMapScale, getMapCenterLocation, setMapScale, deps.setLatitude, deps.setLongitude, deps.tripStatus]); /** * 将经纬度映射为当前地图上的屏幕坐标。 @@ -753,10 +755,7 @@ // order_detail 视口(行程已结束/支付成功)下的上车点 marker 会显示向上突出的 customCallout 气泡, // 需要在系统安全区之上再叠加 ≈灵动岛 + 气泡高度的缓冲,避免气泡被灵动岛/状态栏遮挡。 // 数值与接驾态 ACTIVE_TRIP_CAMERA_TOP_EXTRA_PADDING 保持一致(80),其它视口沿用原有 32。 const topInsetExtra = viewportType === "order_detail" ? ROUTE_VIEWPORT_TOP_EXTRA_PADDING + 10 : ROUTE_VIEWPORT_TOP_EXTRA_PADDING; const topInsetExtra = ROUTE_VIEWPORT_TOP_EXTRA_PADDING const topPadding = Math.max( baseTopPadding, resolveSafeTopInset() + topInsetExtra src/robotaxi/pages/begin-route/hooks/use-page-restore.ts
@@ -358,7 +358,9 @@ } else if (s && s.latitude && s.longitude) { if([TripStatus.VehicleApproaching, TripStatus.InService].includes(cachedTripStatus)) { deps.getLocationInfo(); } else { } else if (![TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(cachedTripStatus)) { // 完成态(TripFinished/PaymentSuccess)地图中心由 useMapViewport 的 order_detail includePoints 主导, // 这里不再用上车点覆盖,否则受控中心会被拉到路线一端导致路线偏离屏幕中央。 deps.setLatitude(s.latitude); deps.setLongitude(s.longitude); } src/robotaxi/pages/begin-route/hooks/use-route-manager.shared.ts
@@ -19,7 +19,7 @@ /** 地图视口最小顶部内边距。 */ export const ROUTE_VIEWPORT_MIN_TOP_PADDING = 32; /** 地图视口顶部附加安全区内边距。 */ export const ROUTE_VIEWPORT_TOP_EXTRA_PADDING = 50; export const ROUTE_VIEWPORT_TOP_EXTRA_PADDING = 60; /** 地图视口底部附加安全区内边距,因为地图的高度是 87vh,到时底部 padding 偏高,要减去一点 */ export const ROUTE_VIEWPORT_BOTTOM_EXTRA_PADDING = 0; /** 地图视口左右内边距。 */ src/robotaxi/pages/begin-route/hooks/use-vehicle-tracking.ts
@@ -152,7 +152,7 @@ longitude: task.longitude, }, autoRotate: false, moveWithRotate: false, moveWithRotate: true, rotate: Math.round(task.rotate), duration, animationEnd: () => { @@ -191,11 +191,23 @@ // 降低车辆位置更新频率 return; } if (Number.isFinite(lat) && Number.isFinite(lng)) { lastVehiclePointRef.current = { latitude: lat, longitude: lng }; lastVehicleUpdateAtRef.current = now; setVehicleLocation({ latitude: lat, longitude: lng }); if (!Number.isFinite(lat) || !Number.isFinite(lng)) { return; } const lastPoint = lastVehiclePointRef.current; // 坐标与上次一致时跳过 setState:避免静止 / 重复点位推送产生无效重渲染 // (朝向变化由 vehicleDirection 单独驱动,不依赖此处的 vehicleLocation 写入)。 if ( lastPoint && Math.abs(lastPoint.latitude - lat) <= VEHICLE_LOCATION_EPSILON && Math.abs(lastPoint.longitude - lng) <= VEHICLE_LOCATION_EPSILON ) { lastVehicleUpdateAtRef.current = now; return; } lastVehiclePointRef.current = { latitude: lat, longitude: lng }; lastVehicleUpdateAtRef.current = now; setVehicleLocation({ latitude: lat, longitude: lng }); }; useEffect(() => { src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx
@@ -213,11 +213,10 @@ const { hasBeforeUserEndSite, routeDistance, durationDisplay } = props; return ( <View className={`custom-callout-content ${ hasBeforeUserEndSite className={`custom-callout-content ${hasBeforeUserEndSite ? "car-callout-with-before car-callout-double-line" : "" }`.trim()} }`.trim()} style={{ padding: "6px 10px" }} > {hasBeforeUserEndSite && ( @@ -292,11 +291,9 @@ const routeSummaryText = useMemo( () => deps.routeDistance && deps.routeDuration !== null ? `${vehicleMetricDurationDisplay.value}${ vehicleMetricDurationDisplay.unit } ${deps.routeDistance.value}${ deps.routeDistance.unit === "m" ? "米" : "公里" }` ? `${vehicleMetricDurationDisplay.value}${vehicleMetricDurationDisplay.unit } ${deps.routeDistance.value}${deps.routeDistance.unit === "m" ? "米" : "公里" }` : "", [deps.routeDistance, deps.routeDuration, vehicleMetricDurationDisplay] ); @@ -448,8 +445,9 @@ }} > {item.name} {!deps.isAndroid && item.name.length > 16 && ( <CoverView style={{ color: "transparent",width: "3px" }}>占</CoverView> {/* 包含非中文字符时占位 因为非中文字符渲染时存在额外间距(比如:‘地名123地名’ 会被渲染为:‘地名 123 地名’ ) 但是气泡宽度又没有自动计算上这个间距 导致名称会被截断 */} {!deps.isAndroid && /[^\u4e00-\u9fa5]/.test(item.name) && ( <CoverView style={{ color: "transparent", width: "3px" }}>占</CoverView> )} </CoverView> {deps.isAndroid && ( @@ -474,11 +472,10 @@ !deps.isAndroid && shouldShowVehicleMetricCallout && ( <CoverView className={`custom-callout-content ${ deps.hasBeforeUserEndSite className={`custom-callout-content ${deps.hasBeforeUserEndSite ? "car-callout-with-before car-callout-double-line" : "" }`.trim()} }`.trim()} style={{ padding: "6px 10px", minWidth: "40px", @@ -591,15 +588,15 @@ {androidCarCalloutContent} </View> )} {deps.isAndroid && deps.arriveCalloutVisible && deps.arrivedTooltipReady && ( <View className="car-callout-android" style={deps.arriveCalloutStyle}> <View className="custom-callout-content"> 已到达 {deps.isAndroid && deps.arriveCalloutVisible && deps.arrivedTooltipReady && ( <View className="car-callout-android" style={deps.arriveCalloutStyle}> <View className="custom-callout-content"> 已到达 </View> </View> </View> )} </> ), [ )} </> ), [ androidCarCalloutContent, deps.arriveCalloutStyle, deps.arriveCalloutVisible, src/robotaxi/pages/begin-route/hooks/use-web-socket-events.ts
@@ -65,10 +65,20 @@ setRouteDistance: (value: any) => void; setRouteDuration: (value: number | null) => void; setModifyDestinationSocketStatus: (value: number | null) => void; setOrderRouteMetrics?: (metrics: { distanceMeters: number | null; durationSeconds: number | null; }) => void; setOrderRouteMetrics?: ( metrics: | { distanceMeters: number | null; durationSeconds: number | null; } | ((prev: { distanceMeters: number | null; durationSeconds: number | null; }) => { distanceMeters: number | null; durationSeconds: number | null; }) ) => void; setBeforeUserEndSite: (site: { longitude: number; latitude: number } | null) => void; setHasPreviousOrder: (value: boolean) => void; isModifyDestinationReviewVisible?: boolean; src/robotaxi/pages/begin-route/hooks/web-socket-handlers/create-order-route-socket-handler.ts
@@ -70,16 +70,31 @@ const durationSecondsRaw = Number(route?.duration); const distanceKm = normalizeDistanceKm(distanceMeters); if (distanceKm !== null) { deps.setRouteDistance(distanceKm); // 行程中后端会持续推送 ORDER_ROUTE(车辆静止时也在上报),normalizeDistanceKm 每次都会返回新对象, // 直接 setRouteDistance 会让每条消息都因新引用触发无效重渲染。这里用函数式更新做按值去重: // value/unit 未变化时返回旧引用,React 据此跳过本次重渲染。 deps.setRouteDistance((prev: { value: number; unit: 'km' | 'm' } | null) => prev && prev.value === distanceKm.value && prev.unit === distanceKm.unit ? prev : distanceKm ); } const durationSeconds = normalizeDurationSeconds(durationSecondsRaw); if (durationSeconds !== null) { // number:值相同时 React 自动跳过重渲染,无需额外去重。 deps.setRouteDuration(durationSeconds); } deps.setOrderRouteMetrics?.({ distanceMeters: Number.isFinite(distanceMeters) ? distanceMeters : null, durationSeconds: Number.isFinite(durationSecondsRaw) ? durationSecondsRaw : null }); const nextDistanceMeters = Number.isFinite(distanceMeters) ? distanceMeters : null; const nextDurationSeconds = Number.isFinite(durationSecondsRaw) ? durationSecondsRaw : null; // orderRouteMetrics 同理:函数式更新按值去重,未变化保持旧引用以跳过重渲染。 deps.setOrderRouteMetrics?.((prev) => prev && prev.distanceMeters === nextDistanceMeters && prev.durationSeconds === nextDurationSeconds ? prev : { distanceMeters: nextDistanceMeters, durationSeconds: nextDurationSeconds } ); }; /** src/robotaxi/pages/begin-route/index.tsx
@@ -93,6 +93,19 @@ const COUPON_POPUP_CLOSE_VIEWPORT_REFRESH_DELAY_MS = 320; // 优惠券弹窗退场动画结束后,再刷新完成态视口 const ROUTE_SELECTABLE_TRIP_STATUSES = [TripStatus.PublishRoute, TripStatus.VehicleApproaching] as const; // 允许展示并切换候选路线的行程状态 const PICKUP_ROUTE_CHANGE_ELIGIBLE_ORDER_ID_KEY = 'pickupRouteChangeEligibleOrderId'; // 接驾态允许切路线的订单 ID,仅在当前订单生命周期内有效 const PASSENGER_NOTICE_REGIONCHANGE_COOLDOWN_MS = 1000; /** * 归一化原生地图回传的 causedBy,兼容 update 的异常拼写。 */ const normalizeRegionChangeCause = (causedBy: unknown) => { const normalized = String(causedBy || '').toLowerCase(); if (['update', 'udpate', 'upudate'].includes(normalized)) { return 'update'; } return normalized; }; type StatusRule = { showPickup: boolean; showDropoff: boolean; @@ -238,6 +251,7 @@ longitudeRef.current = value; }, []); const globalFitSyncRef = useRef(false); // 标记全局 includePoints 触发,用于在 regionchange end 时同步原生缩放 const passengerNoticeRegionChangeIgnoreUntilRef = useRef(0); // 乘车须知开关后的地图 regionchange(update) 冷却截止时间 const markViewportFitSyncing = useCallback(() => { globalFitSyncRef.current = true; }, []); @@ -1606,6 +1620,11 @@ setDoorUnlockedInCurrentPage(false); handleDidShow(router.params?.skipPageFlag, router.params?.fromModifyDestination); updateMapViewportPadding(); // 完成态下隐藏小程序再恢复时,受控中心点可能与原生地图实际中心脱钩,主动触发一次 // order_detail includePoints 纠偏,避免路线被压在底部支付卡片附近。 if ([TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(tripStatusRef.current)) { setOrderDetailViewportRefreshVersion((v) => v + 1); } }); /** @@ -1679,19 +1698,21 @@ setShowEvaluatePopup(true); }; const handleOpenPassengerNotice = useCallback(async () => { if (tripStatus === TripStatus.PublishRoute) { await syncNativeViewportToControlledState(); } const handleOpenPassengerNotice = useCallback(() => { pendingFitSyncRef.current = false; globalFitSyncRef.current = false; passengerNoticeRegionChangeIgnoreUntilRef.current = Date.now() + PASSENGER_NOTICE_REGIONCHANGE_COOLDOWN_MS; rawHandleOpenPassengerNotice(); }, [tripStatus, syncNativeViewportToControlledState, rawHandleOpenPassengerNotice]); }, [rawHandleOpenPassengerNotice, pendingFitSyncRef]); const handleClosePassengerNotice = useCallback(async () => { if (tripStatus === TripStatus.PublishRoute) { await syncNativeViewportToControlledState(); } const handleClosePassengerNotice = useCallback(() => { pendingFitSyncRef.current = false; globalFitSyncRef.current = false; passengerNoticeRegionChangeIgnoreUntilRef.current = Date.now() + PASSENGER_NOTICE_REGIONCHANGE_COOLDOWN_MS; rawHandleClosePassengerNotice(); }, [tripStatus, syncNativeViewportToControlledState, rawHandleClosePassengerNotice]); }, [rawHandleClosePassengerNotice, pendingFitSyncRef]); const handleCloseEvaluatePopup = () => { setEvaluateCloseVersion(v => v + 1); @@ -1709,13 +1730,18 @@ } const handleRegionChange = (e) => { const causedBy = normalizeRegionChangeCause(e?.causedBy || e?.detail?.causedBy || ''); const isPassengerNoticeRegionChangeCoolingDown = passengerNoticeRegionChangeIgnoreUntilRef.current > Date.now(); if (causedBy === 'update' && isPassengerNoticeRegionChangeCoolingDown) { return; } activeTripScale.handleRegionChange(e); handleRouteSummaryRegionChange(e); handleAndroidOverlayRegionChange(e); // 用户手势导致的缩放/拖拽结束后,把原生地图的实际 scale 同步回 React 状态, // 避免受控 <Map scale={mapScale}> 与原生实际缩放漂移,影响后续视口编排判断。 const causedBy = String(e?.causedBy || e?.detail?.causedBy || '').toLowerCase(); const eventType = String(e?.type || e?.detail?.type || '').toLowerCase(); if (eventType === 'end' && (causedBy === 'scale' || causedBy === 'drag' || causedBy === 'gesture')) { notifyUserMapGesture(); src/robotaxi/pages/select-map-site/components/select-map-site-view.tsx
@@ -71,7 +71,7 @@ return ( <View className="home-container"> <Map id="map" id="select-site-map" longitude={deps.longitude} latitude={deps.latitude} scale={deps.mapScale} src/robotaxi/pages/select-map-site/hooks/use-select-map-site.ts
@@ -459,7 +459,7 @@ * 返回当前位置并重新吸附最近站点。 */ const toOrigin = () => { const mapCtx = Taro.createMapContext('map'); const mapCtx = Taro.createMapContext('select-site-map'); mapCtx.moveToLocation({ success: () => { handleRegionChange({ type: 'end', causedBy: 'gesture' }); @@ -476,7 +476,7 @@ Taro.showToast({ title: '暂无可选运营区域', icon: 'none' }); return; } const mapCtx = Taro.createMapContext('map'); const mapCtx = Taro.createMapContext('select-site-map'); mapCtx.includePoints({ points, padding: INCLUDE_POINTS_PADDING @@ -496,7 +496,7 @@ if (type !== 'end') return; state.setBounce(true); const mapCtx = Taro.createMapContext('map'); const mapCtx = Taro.createMapContext('select-site-map'); // 获取真实的缩放等级并通过回调传递出去,彻底阻断与 mapScale 状态的绑定,避免触发原生组件抖动 mapCtx.getScale({ success: (res) => { @@ -600,7 +600,7 @@ state.setLatitude(site.latitude); state.setLongitude(site.longitude); syncPinLocation(site.latitude, site.longitude); const mapCtx = Taro.createMapContext('map'); const mapCtx = Taro.createMapContext('select-site-map'); mapCtx.moveToLocation({ latitude: site.latitude, longitude: site.longitude, @@ -874,7 +874,7 @@ updateMapData(lat, lng).then(({ isInside, electronicAreaId }) => { state.setRangeStatus(getRangeStatus(isInside, {}, electronicAreaId)); }); const mapCtx = Taro.createMapContext('map'); const mapCtx = Taro.createMapContext('select-site-map'); const points = extractPointsFromPolygons(newPolygons); mapCtx.includePoints({ points, src/robotaxi/pages/share-trip/hooks/use-vehicle-tracking.ts
@@ -142,7 +142,7 @@ longitude: task.longitude, }, autoRotate: false, moveWithRotate: false, moveWithRotate: true, rotate: task.rotate, duration: Math.max(task.duration, 100), animationEnd: () => { src/robotaxi/pages/site-search/index.tsx
@@ -79,6 +79,7 @@ const MAX_SHOW_COUNT = 5 // 默认最多显示5条 const MAX_EXPANDED_COUNT = 10 const MAX_SHOW_HISTORY = 3 // 默认历史记录最多3条 const RECOMMEND_SHOW_COUNT = 15 // 推荐站点默认展示数量 const router = Taro.useRouter(); @@ -504,7 +505,7 @@ : recommendSourceList; const sortedHistorySite = sortSitesByCollectedFirst(historySite); const visibleHistory = showAllHistory ? sortedHistorySite.slice(0, MAX_EXPANDED_COUNT) : sortedHistorySite.slice(0, MAX_SHOW_HISTORY); const visibleRecommend = showAllRecommend ? recommendList.slice(0, MAX_EXPANDED_COUNT) : recommendList.slice(0, MAX_SHOW_COUNT); const visibleRecommend = showAllRecommend ? recommendList : recommendList.slice(0, RECOMMEND_SHOW_COUNT); const canShowSearchFallback = !searchValue || (!isSearching && lastCompletedKeyword === searchValue.trim()); useEffect(() => { @@ -614,7 +615,7 @@ }, true, true) } </View> {recommendList.length > MAX_SHOW_COUNT && ( {recommendList.length > RECOMMEND_SHOW_COUNT && ( <View className='show-more' hoverClass='none' onClick={toggleRecommendShow}> <Text className='more-text'>{showAllRecommend ? '收起' : '展示更多'}</Text> <Image className={`more-icon ${showAllRecommend ? 'rotate-up' : ''}`} src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/arrow.png"></Image>