hejianhao
2026-06-02 165a0d76d82582408ff35c83ed9a7034b9946dd7
refactor(robotaxi): 优化地图缩放逻辑,修复地图抖动并更新蓝牙操作卡片

- 重构 useStationScaleFocus 钩子,替换强制地图缩放逻辑为缩放等级跟踪,优化场站围栏展示规则
- 为首页和选点页接入地图缩放回调,统一处理缩放事件同步
- 优化地图经纬度更新判断,仅坐标变化超阈值时更新状态,解决组件抖动问题
- 更新蓝牙操作卡片,使用本地图片替代远程资源,新增 authSuccess 属性控制图标显示
- 新增关闭蓝牙图标本地资源文件
1个文件已添加
7个文件已修改
148 ■■■■■ 已修改文件
src/pages/robotaxi/home/hooks/use-region-change.ts 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/index.tsx 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/hooks/use-station-scale-focus.ts 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car-modules/bluetooth-operate-card.tsx 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car-modules/close-bluetooth.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car.tsx 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/select-map-site/hooks/use-select-map-site.ts 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/select-map-site/index.tsx 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/hooks/use-region-change.ts
@@ -12,12 +12,13 @@
  getMarkerCommitDelayMs?: () => number;
  onRegionBegin?: () => void;
  onRegionSettled?: (payload: { lat: number; lon: number; isInside: boolean }) => void;
  onScaleChange?: (scale: number) => void;
};
/**
 * 地图 region-change 编排:处理地图拖拽/缩放后的吸附、回填和自动归位。
 */
export default function useRegionChange({ mapState, siteState, toOrigin, getMarkerCommitDelayMs, onRegionBegin, onRegionSettled }: Deps) {
export default function useRegionChange({ mapState, siteState, toOrigin, getMarkerCommitDelayMs, onRegionBegin, onRegionSettled, onScaleChange }: Deps) {
  /**
   * 查询完成后提交站点 marker:命中吸附时立即提交,未吸附时避让车辆动画窗口再提交。
   */
@@ -94,8 +95,9 @@
              toOrigin();
            }, 6000);
          }
        } else if (causedBy === 'gesture' || causedBy === 'scale') {
          mapState.setMapScale(res.scale);
        }
        if (typeof onScaleChange === 'function') {
          onScaleChange(res.scale);
        }
      }
    });
@@ -112,8 +114,11 @@
        }
        
        if (causedBy === 'gesture' || causedBy === 'drag' || causedBy === 'scale') {
          mapState.setLatitude(lat);
          mapState.setLongitude(lng);
          // 只有发生显著位移时才同步经纬度,避免微小抖动引发的二次重绘
          if (Math.abs(mapState.latitude - lat) > 0.0001 || Math.abs(mapState.longitude - lng) > 0.0001) {
            mapState.setLatitude(lat);
            mapState.setLongitude(lng);
          }
        }
        try {
src/pages/robotaxi/home/index.tsx
@@ -176,9 +176,8 @@
    syncSelectedCityFromStorage,
    consumeCitySwitchFlag
  } = useCitySwitch();
  const { displayPolygons, displayFencePolylines } = useStationScaleFocus({
  const { displayPolygons, displayFencePolylines, handleScaleChange } = useStationScaleFocus({
    currentSite: siteState.currentSite,
    setMapScale: mapState.setMapScale,
    originalPolygons: mapState.polygons,
    originalFencePolylines: mapState.fencePolylines
  }); // 场站缩放聚焦与围栏展示控制
@@ -308,7 +307,8 @@
    onRegionSettled: (payload) => {
      handleVehicleRegionSettled(payload);
      syncStationFenceVisibility(payload.lat, payload.lon, payload.isInside);
    }
    },
    onScaleChange: handleScaleChange
  });
  const {
    toSelectSitePage,
@@ -452,6 +452,7 @@
      setBounce(false);
      setIsAdsorbed(true);
      setCurrentSite(normalizedTargetMarker);
      syncStationFenceVisibility(normalizedTargetMarker.latitude, normalizedTargetMarker.longitude, isOperatingArea);
      setPreferredStartSite(normalizedTargetMarker);
      Taro.setStorageSync('startSite', normalizedTargetMarker);
      writeConfirmRideSiteSourceType('start', RECOMMENDED_CONFIRM_RIDE_SOURCE_TYPE);
src/robotaxi/hooks/use-station-scale-focus.ts
@@ -1,11 +1,9 @@
import { useEffect, useRef, useMemo } from 'react';
import { useMemo, useState, useEffect, useRef, useCallback } from 'react';
import { GEOFENCE_STATION_FILL_COLOR, GEOFENCE_STATION_STROKE_COLOR } from '../constants/geofence';
type Deps = {
  /** 当前选中的站点对象 */
  currentSite: any;
  /** 设置地图缩放等级的方法 */
  setMapScale: (scale: number | ((prev: number) => number)) => void;
  /** 原始的多边形围栏列表 */
  originalPolygons: any[];
  /** 原始的虚线边框列表 */
@@ -13,62 +11,76 @@
};
/**
 * 场站站点地图缩放与围栏展示聚焦控制 Hook。
 * 场站站点地图围栏展示聚焦控制 Hook。
 * 
 * 职责:
 * 1. 当选中“场站内”的站点时,自动将地图缩放等级设置为 18,并仅展示场站围栏(隐藏蓝色运营区围栏)。
 * 2. 当选中非场站站点或取消选中时,恢复展示所有围栏。
 * 3. 使用 ref 记录最后一次处理的站点,避免用户手动缩放地图后被反复拉回。
 * 1. 当选中“场站内”的站点,并且发生地图放大操作使得缩放达到 17~18 级(即 >= 16.9)时,去掉运营区围栏蒙层,仅展示场站围栏。
 * 2. 移出场站时、选择其他站点时,或缩放比例小于 17 时,恢复展示所有围栏。
 */
export default function useStationScaleFocus({ currentSite, setMapScale, originalPolygons, originalFencePolylines }: Deps) {
  const lastHandledStationSiteIdRef = useRef<string>('');
export default function useStationScaleFocus({ currentSite, originalPolygons, originalFencePolylines }: Deps) {
  const isStationSite = useMemo(() => {
    if (!currentSite) return false;
    return !!currentSite.stationId;
  }, [currentSite]);
  const [shouldFocusFence, setShouldFocusFence] = useState(false);
  const lastScaleRef = useRef<number | undefined>(undefined);
  useEffect(() => {
    const stationId = currentSite?.stationId ? String(currentSite.stationId) : '';
    // 如果当前选中的是场站子站点,并且是一个新的场站(或者第一次进来),则触发缩放
    if (isStationSite && stationId) {
      if (lastHandledStationSiteIdRef.current !== stationId) {
        // 通过微调强制触发 React 状态与原生组件属性的同步
        // 避免因为用户手动缩放地图但未同步到 React 状态,导致 setMapScale(18) 被认为没改变而不生效
        setMapScale(prev => {
          if (prev === 18) return 17.99999;
          if (prev === 17.99999) return 18;
          return 18;
        });
        lastHandledStationSiteIdRef.current = stationId;
    if (!isStationSite) {
      setShouldFocusFence(false);
    } else {
      // 刚选中场站时,重置一次参照物,避免拿上一次旧的值去对比
      lastScaleRef.current = undefined;
    }
  }, [isStationSite]);
  const handleScaleChange = useCallback((scale: number) => {
    if (!isStationSite) {
      lastScaleRef.current = scale;
      return;
    }
    if (lastScaleRef.current !== undefined) {
      if (Math.abs(scale - lastScaleRef.current) > 0.001) {
        // 如果是有效放大操作(规避细微浮点误差),并且达到了 17 级及以上
        if (scale - lastScaleRef.current > 0.001 && scale >= 16.9) {
          setShouldFocusFence(true);
        }
        // 如果缩放比例掉到了 17 以下,恢复围栏
        if (scale < 16.9) {
          setShouldFocusFence(false);
        }
      }
    } else {
      // 离开场站站点,清除记录
      lastHandledStationSiteIdRef.current = '';
      // 初始化第一次赋予初始值
      lastScaleRef.current = scale;
      return;
    }
  }, [isStationSite, currentSite?.stationId, setMapScale]);
    lastScaleRef.current = scale;
  }, [isStationSite]);
  // 根据当前是否选中场站站点,过滤围栏数据
  // 根据当前是否选中场站站点且满足聚焦条件时,过滤围栏数据
  const displayPolygons = useMemo(() => {
    if (!isStationSite) {
    if (!shouldFocusFence) {
      return originalPolygons;
    }
    // 选中场站时,仅保留场站的围栏(通过颜色或透明度特征区分,因为 format 后的多边形已经丢失了 type 属性)
    // 选中场站且满足条件时,仅保留场站的围栏
    return originalPolygons.filter(polygon => polygon.fillColor === GEOFENCE_STATION_FILL_COLOR);
  }, [isStationSite, originalPolygons]);
  }, [shouldFocusFence, originalPolygons]);
  const displayFencePolylines = useMemo(() => {
    if (!isStationSite) {
    if (!shouldFocusFence) {
      return originalFencePolylines;
    }
    // 选中场站时,仅保留场站的虚线边框(通常全都是场站的,这里保险起见通过颜色过滤)
    // 选中场站且满足条件时,仅保留场站的虚线边框
    return originalFencePolylines.filter(polyline => polyline.color === GEOFENCE_STATION_STROKE_COLOR);
  }, [isStationSite, originalFencePolylines]);
  }, [shouldFocusFence, originalFencePolylines]);
  return {
    displayPolygons,
    displayFencePolylines,
    isStationSite
    isStationSite,
    handleScaleChange
  };
}
src/robotaxi/pages/begin-route/components/calling-car-modules/bluetooth-operate-card.tsx
@@ -1,14 +1,17 @@
import { View, Image } from "@tarojs/components";
import closeBluetoothIcon from './close-bluetooth.png';
type Props = {
    description: string;
    actionText?: string;
    onClick: () => void;
    authSuccess?: boolean;
};
export default function BluetoothOperateCard({ description, actionText = '', onClick }: Props) {
    const iconSrc = actionText === '去开启'
        ? "https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/close-bluetooth.png"
export default function BluetoothOperateCard({ description, actionText = '', onClick, authSuccess = false }: Props) {
    // 只有连接成功时才展示蓝色状态的图标
    const iconSrc = actionText === '去开启' || actionText === '去授权' || !authSuccess
        ? closeBluetoothIcon
        : "https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/bluetooth-icon.png";
    return (
src/robotaxi/pages/begin-route/components/calling-car-modules/close-bluetooth.png
src/robotaxi/pages/begin-route/components/calling-car.tsx
@@ -165,6 +165,7 @@
                    description={bluetoothCardDescription}
                    actionText={bluetoothCardActionText}
                    onClick={handleBluetoothCardClick || (() => { })}
                    authSuccess={authSuccess}
                />
            }
src/robotaxi/pages/select-map-site/hooks/use-select-map-site.ts
@@ -49,7 +49,7 @@
/**
 * 选点页业务编排:聚合状态容器与副作用 hooks,并对外提供稳定页面接口。
 */
export default function useSelectMapSite() {
export default function useSelectMapSite(props?: { onScaleChange?: (scale: number) => void }) {
  const state = useSelectMapSiteState(); // 选点页状态容器
  /**
@@ -232,9 +232,19 @@
    }
    if (type !== 'end') return;
    state.setBounce(true);
    if (causedBy === 'gesture' || causedBy === 'drag') {
    const mapCtx = Taro.createMapContext('map');
    // 获取真实的缩放等级并通过回调传递出去,彻底阻断与 mapScale 状态的绑定,避免触发原生组件抖动
    mapCtx.getScale({
      success: (res) => {
        if (typeof props?.onScaleChange === 'function') {
          props.onScaleChange(res.scale);
        }
      }
    });
    if (causedBy === 'gesture' || causedBy === 'drag' || causedBy === 'scale') {
      state.pickupConfirmTypeRef.current = 'drag';
      const mapCtx = Taro.createMapContext('map');
      mapCtx.getCenterLocation({
        success: async (res) => {
          const { latitude: lat, longitude: lng } = res;
@@ -255,8 +265,11 @@
            resetStationSelection();
            state.setSelectedSite({});
            state.setOutOfServiceRange(getOutOfServiceRange(isInside, {}));
            state.setLatitude(lat);
            state.setLongitude(lng);
            // 只有当坐标发生实质性变化才去更新 React 状态中的经纬度,避免频繁反推给原生组件导致抖动
            if (Math.abs(state.latitude - lat) > 0.0001 || Math.abs(state.longitude - lng) > 0.0001) {
              state.setLatitude(lat);
              state.setLongitude(lng);
            }
            syncPinLocation(lat, lng);
            state.setShowToolTip(true);
          }
src/robotaxi/pages/select-map-site/index.tsx
@@ -14,15 +14,22 @@
 * 地图选点页入口:负责 hook 编排与视图组装。
 */
function SelectMapSite() {
  const selectMapSite = useSelectMapSite();
  const handleScaleChangeRef = useRef<(scale: number) => void>();
  const { displayPolygons, displayFencePolylines } = useStationScaleFocus({
  const selectMapSite = useSelectMapSite({
    onScaleChange: (scale) => handleScaleChangeRef.current?.(scale)
  });
  const { displayPolygons, displayFencePolylines, handleScaleChange } = useStationScaleFocus({
    currentSite: selectMapSite.selectedSite,
    setMapScale: selectMapSite.setMapScale,
    originalPolygons: selectMapSite.polygons,
    originalFencePolylines: selectMapSite.mapPolylines
  }); // 场站缩放聚焦与围栏展示控制
  useEffect(() => {
    handleScaleChangeRef.current = handleScaleChange;
  }, [handleScaleChange]);
  const hasReportedPickupPageViewRef = useRef(false);
  const hasReportedDropoffPageViewRef = useRef(false);