fix(map): 修复目的地选点大头针信息滞留问题,可选的请求序号,传入时仅当其为最新一次拖动请求才会回写页面状态,用于丢弃连续/快速拖动时乱序返回的过期结果,避免旧站点数据回填。

Co-Authored-By: ai-coding <ai-coding@devgenie.local>
3个文件已修改
93 ■■■■■ 已修改文件
docs/修改目的地选点页-大头针信息滞留分析报告.md 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/select-map-site/hooks/use-select-map-site-state.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/select-map-site/hooks/use-select-map-site.ts 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/修改目的地选点页-大头针信息滞留分析报告.md
@@ -202,4 +202,39 @@
| `use-select-map-site-state.ts` | 新增 `regionSeqRef`(最新请求序号) |
| `select-map-site-view.tsx` | 一般无需改动(回显逻辑已正确依赖 `selectedSite.name`);如方案 B 则改为消费派生值 |
> 注:以上为分析与方案,**未改动任何代码**。如需我按方案 A 落地修复,请告知。
> 注:以上为分析与方案。最终落地情况见下方「六、落地实现」。
---
## 六、落地实现(已实施)
经评审,方案 A 的 1、2 两点不采用,仅落地第 3 点「最新请求序号守卫」:
| 方案 A 子项 | 是否采用 | 原因 |
|------------|---------|------|
| 1. `begin` 前置清空 | ❌ 不采用 | 拖动一开始就清空会导致底部表单先变「附近无可选站点」再回填,**界面闪烁**,体验不友好 |
| 2. 拓宽/去除 `causedBy` 白名单 | ❌ 不采用 | 吸附分支 `moveToLocation` 会再次触发 `regionchange`,放开 `causedBy` 可能造成 **`handleRegionChange` 循环** |
| 3. 最新请求序号守卫(latest-wins) | ✅ 已实施 | 不动 `begin`、不碰 `causedBy`,无闪烁、无循环风险,直接消除乱序覆盖与过期数据回填 |
### 6.1 改动清单
| 文件 | 改动 |
|------|------|
| `use-select-map-site-state.ts` | 新增 `regionSeqRef = useRef(0)`(区域变化请求序号)并在容器 `return` 中导出 |
| `use-select-map-site.ts` | `updateMapData` 增加可选入参 `seq`,仅当其为最新序号才回写 `markers/nearbySites/isOperatingArea/stationSiteList/mapScale` 等状态(过期请求只返回数据、不写状态);`handleRegionChange` 拖动分支进入时同步 `state.regionSeqRef.current += 1` 生成 `currentRegionSeq`,`await updateMapData(lat, lng, currentRegionSeq)` 后判定 `currentRegionSeq !== state.regionSeqRef.current` 则直接 `return` 丢弃过期结果 |
### 6.2 修复原理
- **同步生成序号**:每个 `regionchange end`(拖动/缩放)进入分支时**同步**自增 `regionSeqRef`,序号严格反映事件发生顺序,最后一次拖动序号最大。
- **异步返回校验**:`getCenterLocation` + `updateMapData`(两次网络请求)返回后,比对捕获的 `currentRegionSeq` 与当前最新序号;不一致即说明已有更新的拖动发生,当前为过期链,**直接丢弃**,不执行吸附/清空决策。
- **过期数据不落状态**:`updateMapData` 内部对过期 `seq` 跳过所有 `setState`,避免过期 `nearbySites` 触发场站副作用把旧站点回填到 `selectedSite`(对应 §3.3 诱因)。
- **不影响其他入口**:`updateMapData` 的 `seq` 为可选参数,初始化、`useDidShow`、`getLocationInfo` 等调用不传 `seq`,行为不变。
- **无循环**:吸附分支 `moveToLocation` 触发的 `regionchange` 多为 `causedBy='update'`,不进白名单分支、不自增序号,不会形成递归。
### 6.3 效果
连续/快速拖动后,只有「最终落点」对应的异步结果会生效:
- 最终落点远离所有点位 → `selectedSite` 被清空,气泡消失、底部显示「附近无可选站点」、确认按钮置灰;
- 早先靠近点位的过期链即便晚到也被丢弃,不再把旧点位回填到气泡与底部表单。
> ⚠️ 未验证(构建/类型检查):当前工作区未安装 `node_modules`(无本地 `tsc`/Taro 工具链),无法运行 `tsc --noEmit` 或项目构建。改动均为局部增量(新增一个 ref + 一个可选参数 + 一处序号比对),已人工复核类型与语法一致;建议在具备依赖的环境执行 `npm install` 后跑 `npm run build:weapp` 回归。
src/robotaxi/pages/select-map-site/hooks/use-select-map-site-state.ts
@@ -51,6 +51,7 @@
  const geofenceListRef = useRef<any[]>([]); // 原始围栏数据缓存(用于跨运营区态重算展示围栏)
  const pickupConfirmTypeRef = useRef<PickupConfirmType>('drag'); // 最近一次确认站点来源类型
  const pinLocationRef = useRef<{ lat: number; lng: number }>({ lat: 0, lng: 0 }); // 当前指针确认位置
  const regionSeqRef = useRef(0); // 地图区域变化请求序号,保证仅最新一次拖动的异步结果生效,丢弃乱序返回的过期结果
  const router = Taro.useRouter(); // 路由实例
  const mapPolylines = useMemo(() => {
@@ -120,6 +121,7 @@
    geofenceListRef,
    pickupConfirmTypeRef,
    pinLocationRef,
    regionSeqRef,
    router,
    mapPolylines,
  };
src/robotaxi/pages/select-map-site/hooks/use-select-map-site.ts
@@ -378,8 +378,10 @@
  /**
   * 更新地图业务数据:围栏内判断、站点列表、自动缩放。
   * @param seq 可选的请求序号,传入时仅当其为最新一次拖动请求才会回写页面状态,
   *            用于丢弃连续/快速拖动时乱序返回的过期结果,避免旧站点数据回填。
   */
  const updateMapData = async (lat: number, lng: number) => {
  const updateMapData = async (lat: number, lng: number, seq?: number) => {
    const selectedCity = Taro.getStorageSync('selectedCity');
    const pointOnOff = state.pageType === 'start'
      ? SELECT_MAP_POINT_ON_OFF.start
@@ -394,26 +396,31 @@
        pointOnOff,
        { allowedElectronicAreaId: referenceElectronicAreaId }
      );
      state.setIsOperatingArea(isInside);
      state.setMarkers(newMarkers);
      state.setNearbySites(sites);
      if (state.selectionMode === 'station' && state.selectedStation?.stationId) {
        const nextStationSites = filterSelectableSites(sites).filter((item: any) => String(item?.stationId || '') === String(state.selectedStation.stationId));
        state.setStationSiteList(nextStationSites);
      }
      const isStationFocused = state.selectedSite && state.selectedSite.stationId;
      if (!isStationFocused && !state.autoScaleLockedRef.current) {
        const autoScale = getAutoScaleBySites(sites?.length > 0 ? sites : newMarkers, lat, lng);
        if (typeof autoScale === 'number') {
          state.setMapScale((prev) => {
            const nextScale = Math.max(AUTO_SCALE_MIN, Math.min(AUTO_SCALE_MAX, autoScale));
            if (Math.abs(prev - nextScale) < 0.01) return prev;
            return nextScale;
          });
      // 仅最新一次拖动请求才回写状态;过期请求只返回数据、不污染 markers/nearbySites 等状态,
      // 防止过期 nearbySites 触发场站副作用回填 selectedSite,导致大头针离开点位后旧信息滞留。
      const isStaleRequest = typeof seq === 'number' && seq !== state.regionSeqRef.current;
      if (!isStaleRequest) {
        state.setIsOperatingArea(isInside);
        state.setMarkers(newMarkers);
        state.setNearbySites(sites);
        if (state.selectionMode === 'station' && state.selectedStation?.stationId) {
          const nextStationSites = filterSelectableSites(sites).filter((item: any) => String(item?.stationId || '') === String(state.selectedStation.stationId));
          state.setStationSiteList(nextStationSites);
        }
        state.autoScaleLockedRef.current = true;
        const isStationFocused = state.selectedSite && state.selectedSite.stationId;
        if (!isStationFocused && !state.autoScaleLockedRef.current) {
          const autoScale = getAutoScaleBySites(sites?.length > 0 ? sites : newMarkers, lat, lng);
          if (typeof autoScale === 'number') {
            state.setMapScale((prev) => {
              const nextScale = Math.max(AUTO_SCALE_MIN, Math.min(AUTO_SCALE_MAX, autoScale));
              if (Math.abs(prev - nextScale) < 0.01) return prev;
              return nextScale;
            });
          }
          state.autoScaleLockedRef.current = true;
        }
      }
      return { isInside, newMarkers, sites, electronicAreaId };
    } finally {
@@ -501,10 +508,15 @@
    if (causedBy === 'gesture' || causedBy === 'drag' || causedBy === 'scale') {
      state.pickupConfirmTypeRef.current = 'drag';
      // 同步自增请求序号,标记本次拖动为「最新」。连续/快速拖动时每个 end 事件都会派生一条
      // 独立异步链,借助该序号在异步返回后判定是否为最新一次,丢弃乱序返回的过期结果。
      const currentRegionSeq = (state.regionSeqRef.current += 1);
      mapCtx.getCenterLocation({
        success: async (res) => {
          const { latitude: lat, longitude: lng } = res;
          const { newMarkers, sites, isInside, electronicAreaId } = await updateMapData(lat, lng);
          const { newMarkers, sites, isInside, electronicAreaId } = await updateMapData(lat, lng, currentRegionSeq);
          // 已有更新的拖动发生,当前为过期结果,直接丢弃,避免用旧中心点的吸附/清空结论覆盖最新状态。
          if (currentRegionSeq !== state.regionSeqRef.current) return;
          const selectableMarkers = getSelectableMarkers(newMarkers, sites);
          const { targetMarker, minDistance } = findNearestMarker(lat, lng, selectableMarkers);
          if (targetMarker && minDistance < state.distanceConfig.adsorptionDistance) {