| New file |
| | |
| | | # 行程中修改目的地 — 完整逻辑梳理 |
| | | |
| | | --- |
| | | |
| | | ## 一、整体流程概览 |
| | | |
| | | ``` |
| | | 用户点击"修改目的地" |
| | | → 写入 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()` 调用。 |