hejianhao
2026-05-28 11456e523d2916cb350bb54ace90bb70317886ca
feat(robotaxi): 完善站点选择与蓝牙解锁全流程

- 为站点搜索、选点页面新增页面类型过滤,仅拉取对应上车/下车点的收藏和附近站点
- 修复发单页面底部布局偏移问题,动态计算选点区域的底部内边距
- 新增手动车门解锁回调逻辑,避免重复触发蓝牙鉴权流程
- 重构埋点参数归一化工具,支持递归展开结构化埋点数据
- 更新地图数据拉取接口,新增点位类型过滤参数
13个文件已修改
224 ■■■■■ 已修改文件
src/pages/robotaxi/home/hooks/use-map-state.ts 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car-modules/door-trunk-control.tsx 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car.tsx 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/css-folder/release-route.less 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/release-route.tsx 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-bluetooth-car-control.ts 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/index.tsx 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/select-map-site/hooks/use-select-map-site.ts 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/site-search/components/favorite-popup/index.tsx 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/site-search/hooks/use-site-search.ts 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/site-search/index.tsx 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/Robotaxi-public-method.ts 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/hooks/use-map-state.ts
@@ -4,6 +4,7 @@
const AUTO_SCALE_MAX = 17; // 自动缩放最大等级
const AUTO_SCALE_MIN = 11; // 自动缩放最小等级
const HOME_MAP_POINT_ON_OFF = 1; // 首页地图固定拉取上车点
/**
 * 比较两批站点 marker 的关键展示字段是否一致。
@@ -103,7 +104,7 @@
    pendingNearbyRequestCountRef.current += 1;
    setNearbyLoading(true);
    try {
      const result = await fetchMapData(lat, lng, code);
      const result = await fetchMapData(lat, lng, code, HOME_MAP_POINT_ON_OFF);
      setIsOperatingArea(result.isInside);
      setElectronicAreaId(result.electronicAreaId);
      setNearbySites(Array.isArray(result?.sites) ? result.sites : []);
src/robotaxi/pages/begin-route/components/calling-car-modules/door-trunk-control.tsx
@@ -10,13 +10,15 @@
    orderId?: string;
    orderTrunkStatus?: boolean;
    handleOpenOverRoutePopup?: () => void;
    onDoorUnlockSuccess?: () => void;
};
export default function DoorTrunkControl({
    tripStatus,
    orderId,
    orderTrunkStatus,
    handleOpenOverRoutePopup
    handleOpenOverRoutePopup,
    onDoorUnlockSuccess
}: Props) {
    const [trunkStatus, setTrunkStatus] = useState(orderTrunkStatus); // 后备箱开关状态
    const [lockStatus, setLockStatus] = useState(false); // 车门解锁状态
@@ -168,6 +170,8 @@
            if (doorType === 'door') {
                Taro.hideLoading();
                Taro.showToast({ title: '右侧车门已解锁', icon: 'none' });
                onDoorUnlockSuccess?.();
                onSuccess?.();
            }
        }).catch(() => {
            Taro.hideLoading();
src/robotaxi/pages/begin-route/components/calling-car.tsx
@@ -72,6 +72,7 @@
    handleCloseAuthBluetoothPopup?: () => void;
    handleCloseSuccessAuthPopup?: () => void;
    handleAuthorizeBluetooth?: () => void;
    onDoorUnlockSuccess?: () => void;
    pageDidShowVersion?: number;
    onOpenCouponSelect?: () => void;
    queuePollingTimeSeconds?: number;
@@ -109,6 +110,7 @@
    handleCloseAuthBluetoothPopup,
    handleCloseSuccessAuthPopup,
    handleAuthorizeBluetooth,
    onDoorUnlockSuccess,
    pageDidShowVersion = 0,
    onOpenCouponSelect,
    queuePollingTimeSeconds = 10
@@ -195,6 +197,7 @@
                        orderId={orderId}
                        orderTrunkStatus={orderTrunkStatus}
                        handleOpenOverRoutePopup={handleOpenOverRoutePopup}
                        onDoorUnlockSuccess={onDoorUnlockSuccess}
                    />
                    <FareDetailCard
@@ -222,6 +225,7 @@
                orderId={orderId}
                orderTrunkStatus={orderTrunkStatus}
                handleOpenOverRoutePopup={handleOpenOverRoutePopup}
                onDoorUnlockSuccess={onDoorUnlockSuccess}
            />
            <EvaluationCard
src/robotaxi/pages/begin-route/components/css-folder/release-route.less
@@ -143,7 +143,6 @@
    .select-site {
        position: relative;
        padding: 32px;
        padding-bottom: calc(232px + env(safe-area-inset-bottom));
        padding-top: 40px;
        background: #F2F3F7;
        border-top: 2px solid #fff;
@@ -250,6 +249,7 @@
        .route-content {
            position: relative;
            margin-top: 28px;
            margin-bottom: 24px;
            z-index: 10;
            display: flex;
            align-items: center;
src/robotaxi/pages/begin-route/components/release-route.tsx
@@ -22,6 +22,7 @@
export default function ReleaseRoute({ startSite, endSite, callImmediately, fareEstimate = {}, onOpenCouponSelect, handleOpenPassengerNotice, walkingTime = 0, isCallDisabled = false, isRouteSelectExpanded = false, onToggleRouteSelect, routeOptions = [], selectedRouteIndex = 0, handleSelectRoute }: Props) {
    const [boxHeight, setBoxHeight] = useState<number | null>(null);
    const [bottomPaddingHeight, setBottomPaddingHeight] = useState<number | null>(null);
    const startY = useRef(0);
    const startHeight = useRef(0);
    const fullHeight = useRef(0);
@@ -32,6 +33,7 @@
    const selectedRoute = routeOptions?.[selectedRouteIndex];
    const isOperateFree = Number(selectedRoute?.operateFree) === 1;
    const hasDiscountAmount = !isOperateFree && Number(fareEstimate?.discountAmount || 0) > 0;
    const formatDiscountAmountText = () => {
        const couponType = Number(fareEstimate?.couponType);
@@ -61,11 +63,16 @@
            const query = Taro.createSelectorQuery();
            query.select('.route-select').boundingClientRect();
            query.select('.select-site').boundingClientRect();
            query.select('.confirm-btn-box-content').boundingClientRect();
            query.select('.discount-amount').boundingClientRect();
            query.exec(res => {
                if (res[1]) {
                if (res[1] && res[2]) {
                    // 当路线选择没有渲染时,res[0] 为 null,预估高度给 230
                    const routeSelectHeight = res[0] ? res[0].height : 230;
                    const siteHeight = res[1].height;
                    const confirmBtnBoxContentHeight = res[2].height;
                    const discountAmountHeight = res[3] ? res[3].height : 0;
                    const nextBottomPaddingHeight = confirmBtnBoxContentHeight + discountAmountHeight;
                    // 最小高度:仅为选点区域的高度
                    const baseHeight = siteHeight;
@@ -73,18 +80,21 @@
                    // 最大高度:选点区域加上上方路线选择的高度
                    const contentHeight = baseHeight + routeSelectHeight;
                    if (bottomPaddingHeight !== nextBottomPaddingHeight) {
                        setBottomPaddingHeight(nextBottomPaddingHeight);
                    }
                    minHeight.current = baseHeight;
                    fullHeight.current = contentHeight;
                    // 只在初次加载未展开时设置基础高度,不要在这里主动将其撑开
                    // 否则在展开(isRouteSelectExpanded变为true)后,这里又会跑一次把它弹高
                    if (boxHeight === null && !isRouteSelectExpanded) {
                    // 收起态下同步更新基础高度,避免底部区域高度变化后容器高度仍停留在旧值
                    if (!isRouteSelectExpanded) {
                        setBoxHeight(baseHeight);
                    }
                }
            });
        }, 100);
    }, [isRouteSelectExpanded, fareEstimate, walkingTime]);
    }, [isRouteSelectExpanded, fareEstimate, walkingTime, bottomPaddingHeight]);
    const handleTouchStart = (e: any) => {
        // 如果没有展开,禁止滑动
@@ -170,7 +180,7 @@
        <View className="release-route-container">
            <View
                className='release-route-wrapper'
                style={{ height: boxHeight ? `${boxHeight}px` : 'auto' }}
                style={{ height: typeof boxHeight === 'number' ? `${boxHeight}px` : 'auto' }}
                catchMove={isRouteSelectExpanded}
                onTouchStart={(e) => {
                    // 如果没有展开,禁止滑动
@@ -197,7 +207,7 @@
                            handleSelectRoute={handleSelectRoute}
                        />
                    )}
                    <View className="select-site">
                    <View className="select-site" style={{ paddingBottom: typeof bottomPaddingHeight === 'number' ? `${bottomPaddingHeight}px` : undefined }}>
                        <Image className="site-top-bg" src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/site-top-bg-20260507.png' />
                        <View className="site-boarding-point">
                            {/* 起点 */}
@@ -237,7 +247,7 @@
                </View>
                <View className="confirm-btn-box">
                    {
                        isOperateFree ? null : fareEstimate.discountAmount > 0 &&
                        hasDiscountAmount &&
                            <View className="discount-amount" onClick={onOpenCouponSelect}>
                                <View className="flex-center">
                                    <View className="discount-amount-text">最佳优惠</View>
src/robotaxi/pages/begin-route/hooks/use-bluetooth-car-control.ts
@@ -17,6 +17,7 @@
type ConnectStatus = 0 | 1 | 2 | 3;
const RETRY_UNLOCK_MESSAGE = '请点击解锁按钮解锁车门'; // 鉴权失败/解锁失败统一提示文案
const UNLOCK_SUCCESS_MESSAGE = '车门已解锁,请上车'; // 解锁成功提示文案
const BLUETOOTH_AUTH_ACTIVE_TRIP_STATUSES = [TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup] as const; // 仅这两个状态允许处理蓝牙鉴权回调
/**
 * 归一化 MAC 地址,统一去空格并转大写。
@@ -55,7 +56,10 @@
  const certificateRef = useRef(''); // 当前会话证书缓存
  const macAddressRef = useRef(''); // 当前会话主蓝牙地址缓存
  const deviceModelRef = useRef(resolveDeviceModel()); // 当前设备型号缓存
  const latestTripStatusRef = useRef(tripStatus); // 最新行程状态引用,供异步蓝牙回调读取
  const unlockFinishedRef = useRef(false); // 当前轮次是否已出现解锁成功
  const manualDoorUnlockRef = useRef(false); // 当前轮次是否已手动解锁,命中后停止后续蓝牙鉴权
  const acceptBluetoothCallbacksRef = useRef(false); // 当前轮次是否继续接收蓝牙回调,离开 4/5 或收口后立即关闭
  const [connectStatus, setConnectStatus] = useState<ConnectStatus>(0); // 蓝牙连接状态:0断开/1连接中/2已连接/3已鉴权
  const [authSuccess, setAuthSuccess] = useState(false); // RtAuth 鉴权是否成功
  const [isAuthorizing, setIsAuthorizing] = useState(false); // 鉴权流程进行中
@@ -70,12 +74,30 @@
      icon: 'none'
    });
  }; // 统一展示蓝牙解锁结果提示
  latestTripStatusRef.current = tripStatus;
  /**
   * 判断当前蓝牙异步回调是否仍属于有效轮次。
   */
  const shouldIgnoreBluetoothCallback = (callbackName: string) => {
    const isTripStatusActive = BLUETOOTH_AUTH_ACTIVE_TRIP_STATUSES.includes(latestTripStatusRef.current);
    if (acceptBluetoothCallbacksRef.current && isTripStatusActive) {
      return false;
    }
    console.log('[蓝牙] 忽略过期回调:', {
      callbackName,
      tripStatus: latestTripStatusRef.current,
      acceptBluetoothCallbacks: acceptBluetoothCallbacksRef.current
    });
    return true;
  };
  /**
   * 主动断开蓝牙连接并重置状态。
   */
  const closeConnection = () => {
    console.log('[蓝牙] 主动断开连接');
    acceptBluetoothCallbacksRef.current = false;
    if (managerRef.current) {
      managerRef.current.closeConnection();
    }
@@ -98,6 +120,9 @@
    managerRef.current = new RtAuthBluetoothManager({
      onStatusChange: (status: ConnectStatus) => {
        if (shouldIgnoreBluetoothCallback('onStatusChange')) {
          return;
        }
        console.log('[蓝牙] 连接状态变更:', status);
        setConnectStatus(status);
        if (status === 3) {
@@ -110,6 +135,9 @@
        }
      },
      onError: (error: any) => {
        if (shouldIgnoreBluetoothCallback('onError')) {
          return;
        }
        console.log('[蓝牙] 连接错误:', error);
        isAuthorizingRef.current = false;
        if (unlockFinishedRef.current) {
@@ -119,6 +147,9 @@
        setIsAuthorizing(false);
      },
      onRtAuthResult: (result: any) => {
        if (shouldIgnoreBluetoothCallback('onRtAuthResult')) {
          return;
        }
        console.log('[蓝牙] 鉴权结果:', result);
        if (result?.success) {
          authSuccessRef.current = true;
@@ -135,6 +166,9 @@
        setIsAuthorizing(false);
      },
      onRtNotification: (result: any) => {
        if (shouldIgnoreBluetoothCallback('onRtNotification')) {
          return;
        }
        console.log('[蓝牙] 解锁通知:', result);
        if (result?.type === 'pollingUnlock') {
          const isAlreadyUnlocked = Number(result?.data?.result) === 2; // RtNotification 挂载在 data,result=2 表示车辆已解锁
@@ -180,20 +214,31 @@
      isAuthorizing: isAuthorizingRef.current,
      authSuccess: authSuccessRef.current
    });
    if (![TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup].includes(tripStatus)) return;
    if (!BLUETOOTH_AUTH_ACTIVE_TRIP_STATUSES.includes(tripStatus)) return;
    if (!orderId) return;
    if (manualDoorUnlockRef.current) {
      console.log('[蓝牙] 当前轮次已手动解锁,跳过后续鉴权请求');
      return;
    }
    if (startAuthTaskRef.current) return startAuthTaskRef.current;
    const startKey = `${orderId}`;
    if (isAuthorizingRef.current || (authSuccessRef.current && lastStartKeyRef.current === startKey)) return;
    lastStartKeyRef.current = startKey;
    unlockFinishedRef.current = false;
    acceptBluetoothCallbacksRef.current = true;
    isAuthorizingRef.current = true;
    setIsAuthorizing(true);
    setAuthError('');
    const runTask = async () => {
      try {
        const tokenRes = await getBluetoothUnlockToken(orderId);
        if (manualDoorUnlockRef.current) {
          console.log('[蓝牙] 用户已手动解锁,忽略鉴权凭证返回结果');
          isAuthorizingRef.current = false;
          setIsAuthorizing(false);
          return;
        }
        const tokenPayload: BluetoothTokenPayload = typeof tokenRes === 'object' && tokenRes !== null ? tokenRes as BluetoothTokenPayload : {};
        const certificate = typeof tokenPayload.token === 'string' ? tokenPayload.token : String(tokenPayload.token || '');
        const mainMacAddress = normalizeMac(tokenPayload.macAddress);
@@ -219,9 +264,21 @@
          certificate,
          deviceModel: deviceModelRef.current
        });
        if (manualDoorUnlockRef.current) {
          console.log('[蓝牙] 用户已手动解锁,停止打开蓝牙适配器');
          isAuthorizingRef.current = false;
          setIsAuthorizing(false);
          return;
        }
        console.log('[蓝牙] 准备打开蓝牙适配器');
        manager.openBluetoothAdapter();
      } catch (error: any) {
        if (manualDoorUnlockRef.current) {
          console.log('[蓝牙] 手动解锁后忽略鉴权异常:', error);
          isAuthorizingRef.current = false;
          setIsAuthorizing(false);
          return;
        }
        console.log('[蓝牙] 鉴权流程异常:', error);
        isAuthorizingRef.current = false;
        setAuthError(error?.msg || '获取蓝牙鉴权凭证失败');
@@ -240,19 +297,37 @@
   */
  const retryAuth = useCallback(() => {
    console.log('[蓝牙] 重试鉴权');
    if (manualDoorUnlockRef.current) {
      console.log('[蓝牙] 当前轮次已手动解锁,忽略重试鉴权');
      return;
    }
    if (!managerRef.current) {
      startAuth();
      return;
    }
    acceptBluetoothCallbacksRef.current = true;
    isAuthorizingRef.current = true;
    setIsAuthorizing(true);
    setAuthError('');
    managerRef.current.retryConnection();
  }, [startAuth]);
  /**
   * 用户手动解锁成功后,停止当前轮次剩余的蓝牙鉴权流程。
   */
  const markManualDoorUnlock = useCallback(() => {
    console.log('[蓝牙] 用户已手动解锁,停止后续蓝牙鉴权');
    manualDoorUnlockRef.current = true;
    unlockFinishedRef.current = true;
    acceptBluetoothCallbacksRef.current = false;
    setAuthError('');
    closeConnectionRef.current();
  }, []);
  useEffect(() => {
    if (![TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup].includes(tripStatus)) {
    if (!BLUETOOTH_AUTH_ACTIVE_TRIP_STATUSES.includes(tripStatus)) {
      console.log('[蓝牙] 当前状态非4/5,断开连接:', tripStatus);
      acceptBluetoothCallbacksRef.current = false;
      closeConnectionRef.current();
      lastStartKeyRef.current = '';
      certificateRef.current = '';
@@ -261,6 +336,7 @@
      authSuccessRef.current = false;
      isAuthorizingRef.current = false;
      unlockFinishedRef.current = false;
      manualDoorUnlockRef.current = false;
      setAuthError('');
    }
  }, [tripStatus]);
@@ -271,6 +347,7 @@
    authError,
    isAuthorizing,
    startAuth,
    markManualDoorUnlock,
    retryAuth,
    closeConnection
  };
src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx
@@ -149,6 +149,8 @@
  handleCloseSuccessAuthPopup: () => void;
  /** 去授权蓝牙 */
  handleAuthorizeBluetooth: () => void;
  /** 手动解锁车门成功 */
  onDoorUnlockSuccess: () => void;
  /** 页面 didShow 版本号 */
  pageDidShowVersion: number;
  /** 排队位次轮询频率(秒) */
@@ -448,6 +450,7 @@
          handleCloseAuthBluetoothPopup={deps.handleCloseAuthBluetoothPopup}
          handleCloseSuccessAuthPopup={deps.handleCloseSuccessAuthPopup}
          handleAuthorizeBluetooth={deps.handleAuthorizeBluetooth}
          onDoorUnlockSuccess={deps.onDoorUnlockSuccess}
          pageDidShowVersion={deps.pageDidShowVersion}
          queuePollingTimeSeconds={deps.queuePollingTimeSeconds}
        />
src/robotaxi/pages/begin-route/index.tsx
@@ -859,7 +859,8 @@
  } = orderActions;
  const {
    authSuccess: bluetoothAuthSuccess,
    startAuth: startBluetoothAuth
    startAuth: startBluetoothAuth,
    markManualDoorUnlock
  } = useBluetoothCarControl({
    orderId,
    tripStatus
@@ -1219,6 +1220,7 @@
    handleCloseAuthBluetoothPopup,
    handleCloseSuccessAuthPopup,
    handleAuthorizeBluetooth,
    onDoorUnlockSuccess: markManualDoorUnlock,
    pageDidShowVersion,
    queuePollingTimeSeconds
  }); // 视图渲染委托
src/robotaxi/pages/select-map-site/hooks/use-select-map-site.ts
@@ -30,6 +30,10 @@
const AUTO_SCALE_MAX = 17; // 自动缩放最大等级
const AUTO_SCALE_MIN = 11; // 自动缩放最小等级
const SELECT_MAP_POINT_ON_OFF = {
  start: 1,
  end: 2
} as const; // 选点地图站点开关:1=上车点,2=下车点
/**
 * 选点页业务编排:聚合状态容器与副作用 hooks,并对外提供稳定页面接口。
@@ -89,10 +93,13 @@
   */
  const updateMapData = async (lat: number, lng: number) => {
    const selectedCity = Taro.getStorageSync('selectedCity');
    const pointOnOff = state.pageType === 'start'
      ? SELECT_MAP_POINT_ON_OFF.start
      : SELECT_MAP_POINT_ON_OFF.end;
    state.pendingNearbyRequestCountRef.current += 1;
    state.setNearbyLoading(true);
    try {
      const { isInside, newMarkers, sites } = await fetchMapData(lat, lng, selectedCity?.code);
      const { isInside, newMarkers, sites } = await fetchMapData(lat, lng, selectedCity?.code, pointOnOff);
      state.setIsOperatingArea(isInside);
      state.setMarkers(newMarkers);
      state.setNearbySites(sites);
src/robotaxi/pages/site-search/components/favorite-popup/index.tsx
@@ -4,13 +4,19 @@
import './index.less';
import { getCollectPoints, collectPoint } from '../../../../api/site-search';
const COLLECT_POINT_ON_OFF = {
  start: 1,
  end: 2
} as const; // 收藏点位开关:1=上车点,2=下车点
type Props = {
  show: boolean;
  onClose: () => void;
  onSelect: (item: any) => void;
  pageType: string;
};
export default function FavoritePopup({ show, onClose, onSelect }: Props) {
export default function FavoritePopup({ show, onClose, onSelect, pageType }: Props) {
  const [list, setList] = useState<any[]>([]);
  const [visible, setVisible] = useState(false);
  const [animationData, setAnimationData] = useState<any>();
@@ -25,10 +31,14 @@
  const fetchData = async (page = 1) => {
    if (loading) return;
    setLoading(true);
    const pointOnOff = pageType === 'start'
      ? COLLECT_POINT_ON_OFF.start
      : COLLECT_POINT_ON_OFF.end;
    try {
      const res = await getCollectPoints({
        pageNum: page,
        pageSize: 999,
        pointOnOff,
      });
      if (res && res.records) {
        setList(prev => page === 1 ? res.records : [...prev, ...res.records]);
src/robotaxi/pages/site-search/hooks/use-site-search.ts
@@ -18,6 +18,11 @@
  maxShowCount: number;
};
const SEARCH_POINT_ON_OFF = {
  start: 1,
  end: 2
} as const; // 搜索点位开关:1=上车点,2=下车点
export default function useSiteSearch(deps: Deps) {
  // 推荐站点列表
  const [siteList, setSiteList] = useState<any[]>([]);
@@ -47,12 +52,17 @@
    canUpdate?: () => boolean
  ) => {
    const selectedCity = Taro.getStorageSync('selectedCity');
    const currentPageType = pageType || deps.router.params?.pageType || '';
    const pointOnOff = currentPageType === 'start'
      ? SEARCH_POINT_ON_OFF.start
      : SEARCH_POINT_ON_OFF.end;
    try {
      const res = await getNearestSite({
        cityCode: selectedCity?.code || '',
        latitude: Number(lat),
        longitude: Number(lon),
        electronicAreaId
        electronicAreaId,
        pointOnOff
      });
      if (canUpdate && !canUpdate()) {
        return;
@@ -82,12 +92,17 @@
    setIsSearching(true);
    const { latitude, longitude, electronicAreaId } = deps.router.params;
    const selectedCity = Taro.getStorageSync('selectedCity');
    const currentPageType = pageType || deps.router.params?.pageType || '';
    const pointOnOff = currentPageType === 'start'
      ? SEARCH_POINT_ON_OFF.start
      : SEARCH_POINT_ON_OFF.end;
    const payload = {
      cityCode: selectedCity?.code || '',
      electronicAreaId: electronicAreaId ? Number(electronicAreaId) : 0,
      lat: Number(latitude),
      lon: Number(longitude),
      pointPoi: normalizedQuery
      pointPoi: normalizedQuery,
      pointOnOff
    };
    try {
      const res = await queryPointByName(payload);
@@ -238,7 +253,7 @@
    };
  }, []);
  // 初始化:拉取附近站点、历史记录、页面类型
  useEffect(() => {
    refreshData();
    const { pageType: type } = deps.router.params;
src/robotaxi/pages/site-search/index.tsx
@@ -418,6 +418,7 @@
        show={showFavoritePopup}
        onClose={handleCloseFavoritePopup}
        onSelect={handleSelectFavorite}
        pageType={pageType}
      />
    </View>
  )
src/utils/Robotaxi-public-method.ts
@@ -743,7 +743,7 @@
}
// 获取地图数据(围栏检查+附近站点)
export const fetchMapData = async (lat: number, lng: number, cityCode?: string) => {
export const fetchMapData = async (lat: number, lng: number, cityCode?: string, pointOnOff?: number) => {
  try {
    // 1. 先检查是否在运营区域内
    const fenceRes: any = await getFenceCheck({ latitude: lat, longitude: lng, cityCode }).catch(e => {
@@ -756,7 +756,7 @@
    // 2. 只有在运营区域内才获取附近站点
    let sites: any = { points: [] };
    if (isInside) {
      sites = await getNearestSite({ latitude: lat, longitude: lng, electronicAreaId }).catch(e => {
      sites = await getNearestSite({ latitude: lat, longitude: lng, electronicAreaId, pointOnOff }).catch(e => {
        return { points: [] };
      });
      // 兼容处理
@@ -1115,33 +1115,43 @@
  };
};
/** 判断埋点字段是否为需要自动 JSON 化的结构化数据。 */
const isStructuredReportValue = (value: any) => {
/** 判断值是否为普通对象。 */
const isPlainReportObject = (value: any) => Object.prototype.toString.call(value) === '[object Object]';
/** 判断值是否为可继续展开的结构化数据。 */
const isExpandableReportValue = (value: any) => Array.isArray(value) || isPlainReportObject(value);
/** 递归展开埋点字段:对象使用前缀拼接,数组使用索引展开。 */
const flattenReportParam = (
  result: Record<string, any>,
  keyPrefix: string,
  value: any
) => {
  if (!keyPrefix) {
    return;
  }
  if (!isExpandableReportValue(value)) {
    result[keyPrefix] = value;
    return;
  }
  if (Array.isArray(value)) {
    return true;
    value.forEach((item, index) => {
      flattenReportParam(result, `${keyPrefix}_${index}`, item);
    });
    return;
  }
  return Object.prototype.toString.call(value) === '[object Object]';
  Object.keys(value).forEach((childKey) => {
    flattenReportParam(result, `${keyPrefix}_${childKey}`, value[childKey]);
  });
};
/** 序列化单个埋点字段,避免对象被底层隐式转成 [object Object]。 */
const serializeReportValue = (value: any) => {
  if (!isStructuredReportValue(value)) {
    return value;
  }
  try {
    return JSON.stringify(value);
  } catch (error) {
    return Array.isArray(value) ? '[]' : '{}';
  }
};
/** 归一化自定义埋点参数:仅自动 JSON 化顶层对象/数组字段。 */
/** 归一化自定义埋点参数:对象按前缀法递归展开。 */
const normalizeReportParams = (para: any) => {
  if (!para || Object.prototype.toString.call(para) !== '[object Object]') {
  if (!para || !isPlainReportObject(para)) {
    return {};
  }
  return Object.keys(para).reduce((result: Record<string, any>, key: string) => {
    result[key] = serializeReportValue(para[key]);
    flattenReportParam(result, key, para[key]);
    return result;
  }, {});
};