hejianhao
2026-04-23 17375aad9cc950002998d31ec858e1bf757e2031
refactor(pages): 重构地图选点页为状态容器与视图分离架构

- 将原选点页拆分为状态容器、业务编排、视图组件与工具函数
- 新增场站模式支持,可展示场站子站点并支持收藏操作
- 统一围栏过滤逻辑,仅渲染运营区围栏(type=1)
- 优化步行路线计算,添加安卓端性能优化
- 修复收藏弹窗中场站选择逻辑,支持子站点分流
- 调整站点列表样式,支持场站子站点展开收起
5个文件已添加
8个文件已修改
1926 ■■■■■ 已修改文件
src/pages/robotaxi/home/hooks/use-navigation.ts 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/index.tsx 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/select-map-site/components/select-map-site-view.tsx 194 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/select-map-site/hooks/use-select-map-site-state.ts 113 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/select-map-site/hooks/use-select-map-site-utils.ts 86 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/select-map-site/hooks/use-select-map-site-walking-route.ts 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/select-map-site/hooks/use-select-map-site.ts 559 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/select-map-site/index.less 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/select-map-site/index.tsx 730 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/site-search/components/favorite-popup/index.tsx 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/site-search/components/site-list-item.tsx 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/site-search/index.less 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/site-search/index.tsx 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/hooks/use-navigation.ts
@@ -3,6 +3,16 @@
import { jumpTo } from '../../../../utils/jump';
import { getGeofence } from '../../../../robotaxi/api/home';
const OPERATING_GEOFENCE_TYPE = 1; // 仅渲染运营区围栏类型
/**
 * 过滤可渲染的围栏数据,仅保留 type=1 的运营区围栏。
 */
const filterRenderableGeofence = (geofenceList: any[]) => {
  if (!Array.isArray(geofenceList)) return [];
  return geofenceList.filter((item) => Number(item?.type) === OPERATING_GEOFENCE_TYPE);
};
type Deps = {
  /** 是否未登录 */
  isGuest: boolean;
@@ -207,8 +217,9 @@
    let nextPolygons = deps.polygons;
    try {
      const res: any = await getGeofence({ cityCode: deps.selectedCity.code });
      if (res && Array.isArray(res)) {
        nextPolygons = formatGeofencePolygons(res);
      const geofenceList = filterRenderableGeofence(res);
      if (geofenceList.length > 0) {
        nextPolygons = formatGeofencePolygons(geofenceList);
        deps.setPolygons(nextPolygons);
      } else {
        nextPolygons = [];
src/pages/robotaxi/home/index.tsx
@@ -62,6 +62,15 @@
import { checkOtp } from '../../../utils/tools';
type HomeTripStatus = TripStatus | 0;
const OPERATING_GEOFENCE_TYPE = 1; // 仅渲染围栏类型 2为场站类型
/**
 * 过滤可渲染的围栏数据,仅保留 type=1 的运营区围栏。
 */
const filterRenderableGeofence = (geofenceList: any[]) => {
  if (!Array.isArray(geofenceList)) return [];
  return geofenceList.filter((item) => Number(item?.type) === OPERATING_GEOFENCE_TYPE);
};
export default function Home() {
  const renderTailPreserveText = (text: string, tailCount = 3) => {
@@ -252,8 +261,9 @@
    const allowMapMove = !isLocationAuthDenied;
    getGeofence({ cityCode: selectedCity.code }).then((res: any) => {
      if (res && Array.isArray(res) && res.length > 0) {
        const newPolygons = formatGeofencePolygons(res);
      const geofenceList = filterRenderableGeofence(res);
      if (geofenceList.length > 0) {
        const newPolygons = formatGeofencePolygons(geofenceList);
        setPolygons(newPolygons);
      } else {
        setPolygons([]);
src/robotaxi/pages/select-map-site/components/select-map-site-view.tsx
New file
@@ -0,0 +1,194 @@
import { View, Map, Text, Image, ScrollView } from '@tarojs/components';
import CustomerServicePopup from '../../../components/customer-service';
import TipPopup from '../components/tip-popup';
import { formatTripDistance, formatTripDurationFromSeconds } from '../../../../utils/Robotaxi-public-method';
type Deps = {
  longitude: number | null;
  latitude: number | null;
  mapScale: number;
  markers: any[];
  mapPolylines: any[];
  polygons: any[];
  handleRegionChange: (e: any) => void;
  handleMarkerTap: (e: any) => void;
  toBack: () => void;
  backIcon: string;
  openService: () => void;
  toCoupon: () => void;
  toOrder: () => void;
  personalCenter: string;
  coupon: string;
  suggest: string;
  nearbyLoading: boolean;
  isAndroid: boolean;
  waitingRadarSrc: string;
  showToolTip: boolean;
  selectedSite: any;
  selectionMode: 'point' | 'station';
  selectedStation: any;
  stationSiteList: any[];
  outOfServiceRange: boolean;
  tooltipBgSrc: string;
  pageType: string;
  bounce: boolean;
  pointer: string;
  toOrigin: () => void;
  originIcon: string;
  walkingDuration: number | null;
  walkingDistance: number | null;
  nearbySites: any[];
  handleCollect: (e: any) => void;
  changeStationSite: (site: any) => void;
  activeStar: string;
  noStar: string;
  confirmSite: () => void;
  showCustomerService: boolean;
  handleCloseCustomerService: () => void;
  showTipPopup: boolean;
  handleCloseTipPopup: () => void;
  tipType: string;
};
/**
 * 选点页视图组件:仅负责渲染地图与页面 UI,不包含业务状态编排。
 */
export default function SelectMapSiteView(deps: Deps) {
  return (
    <View className="home-container">
      <Map
        id="map"
        longitude={deps.longitude}
        latitude={deps.latitude}
        scale={deps.mapScale}
        markers={deps.markers.map((m) => ({ ...m, title: '', name: '' }))}
        polyline={deps.mapPolylines}
        polygons={deps.polygons}
        showLocation={!!deps.longitude}
        onRegionChange={deps.handleRegionChange}
        onMarkerTap={deps.handleMarkerTap}
        style={{ width: '100%', height: '100%' }}
      >
        {/* 返回 */}
        <View onClick={deps.toBack} className="select-city">
          <Image className="select-city-icon" src={deps.backIcon} />
        </View>
        {/* 地图浮层入口 */}
        <View className="map-entry">
          <Image onClick={deps.openService} className="map-entry-icon" src={deps.personalCenter} />
          <View className="split-line"></View>
          <Image onClick={deps.toCoupon} className="map-entry-icon" src={deps.coupon} />
          <View className="split-line"></View>
          <Image onClick={deps.toOrder} className="map-entry-icon" src={deps.suggest} />
        </View>
        {deps.nearbyLoading && deps.isAndroid && (
          <Image className="waiting-radar-img" src={deps.waitingRadarSrc} />
        )}
        {/* 指针 */}
        <View className="pointer-box">
          {deps.nearbyLoading && !deps.isAndroid && (
            <Image className="waiting-radar" src={deps.waitingRadarSrc} />
          )}
          {((deps.showToolTip && deps.selectedSite.name) || deps.outOfServiceRange) && (
            <View className="toolTip">
              {(deps.outOfServiceRange || deps.selectedSite.time) && (
                <View className="toolTip-bg" style={{ backgroundImage: `url(${deps.tooltipBgSrc})` }}>
                  <Text className="toolTip-bg-text">
                    {deps.outOfServiceRange ? '不在运营区' : `限停${deps.selectedSite.time}分钟`}
                  </Text>
                </View>
              )}
              <View className="toolTip-txt">
                <View className="toolTip-txt-content">
                  {deps.outOfServiceRange ? `请拖动至${deps.pageType == 'end' ? '下车' : '上车'}点` : deps.selectedSite.name}
                </View>
              </View>
            </View>
          )}
          <Image
            className={`pointer-icon ${deps.bounce ? 'bounce' : ''}`}
            src={deps.pointer}
          />
        </View>
        <View className="container">
          <View className="container-inner">
            {/* 复位按钮 */}
            <View className="origin-box">
              <Image onClick={deps.toOrigin} className="origin-icon" src={deps.originIcon} />
            </View>
            <View catchMove onClick={(e) => e.stopPropagation()}>
              {/* 确认站点 */}
              <View className="content-box">
                <View className="content-box-item">
                  <View className='content-box-item-title'>
                    <View className="content-title">{deps.selectionMode === 'station' ? deps.selectedStation?.name : (deps.pageType === 'start' ? '确认上车站点' : '确认目的地')}</View>
                    {deps.pageType === 'start' && deps.walkingDuration !== null && deps.walkingDistance !== null && (
                      <View className="limit-tip">
                        从当前位置出发步行{formatTripDurationFromSeconds(deps.walkingDuration)}({formatTripDistance(deps.walkingDistance)})上车
                      </View>
                    )}
                    {(deps.nearbySites.length > 0 && deps.pageType === 'end') && (
                      <View className="limit-tip">点击其他选项或紫色原点,可更改{deps.pageType === 'start' ? '上车' : '下车'}点</View>
                    )}
                  </View>
                  {deps.selectionMode === 'station' && (
                    <Image
                      onClick={deps.handleCollect}
                      className='collect-icon'
                      src={deps.selectedStation?.isCollect ? deps.activeStar : deps.noStar}
                    />
                  )}
                </View>
                <View className='site-list-wrap'>
                  <ScrollView
                    className="site-list"
                    scrollY
                    enhanced
                    showScrollbar={true}
                  >
                    <View className='site-list-inner'>
                      {deps.selectionMode === 'station' ? (
                        deps.stationSiteList.length > 0 ? deps.stationSiteList.map((item) => (
                          <View
                            key={item.id || item.pointId || `${item.latitude}_${item.longitude}`}
                            className={`site-item ${String(item.id || item.pointId || '') === String(deps.selectedSite?.id || deps.selectedSite?.pointId || '') ? 'site-active' : ''}`}
                            onClick={() => deps.changeStationSite(item)}
                          >
                            <View>{item.name}</View>
                          </View>
                        )) : <View className="no-service">当前场站暂无可选站点</View>
                      ) : deps.selectedSite.name ? (
                        <View className='site-item site-active'>
                          <View>{deps.selectedSite.name}</View>
                          {deps.selectionMode === 'point' && (
                            <Image onClick={deps.handleCollect} className='collect-icon' src={deps.selectedSite.isCollect === 1 ? deps.activeStar : deps.noStar} />
                          )}
                        </View>
                      ) : (
                        <View className="no-service">超出服务范围,请在运营区内选择</View>
                      )}
                    </View>
                  </ScrollView>
                </View>
                <View onClick={deps.confirmSite} className={`site-confirm ${deps.selectedSite.name ? '' : 'no-confirm'}`}>
                  {deps.pageType === 'start' ? '确认上车站点' : '确认目的地站点'}
                </View>
              </View>
            </View>
          </View>
        </View>
      </Map>
      {/* 客服弹窗 */}
      <CustomerServicePopup
        show={deps.showCustomerService}
        onClose={deps.handleCloseCustomerService}
      />
      {/* 提示弹窗 */}
      <TipPopup
        show={deps.showTipPopup}
        onClose={deps.handleCloseTipPopup}
        tipType={deps.tipType}
      />
    </View>
  );
}
src/robotaxi/pages/select-map-site/hooks/use-select-map-site-state.ts
New file
@@ -0,0 +1,113 @@
import Taro from "@tarojs/taro";
import { useMemo, useRef, useState } from 'react';
/**
 * 选点页状态容器:仅维护页面状态、refs 与轻量派生数据,不承载业务副作用。
 */
export default function useSelectMapSiteState() {
  const [showCustomerService, setShowCustomerService] = useState(false); // 客服弹窗是否显示
  const [longitude, setLongitude] = useState<number | null>(null); // 经度
  const [latitude, setLatitude] = useState<number | null>(null); // 纬度
  const [mapScale, setMapScale] = useState(14); // 地图缩放等级
  const [showToolTip, setShowToolTip] = useState(true); // 提示是否显示
  const [bounce, setBounce] = useState(false); // 是否弹跳
  const [markers, setMarkers] = useState<any[]>([]); // 标记点列表
  const [pageType, setPageType] = useState('end'); // 起点start 终点end
  const [selectedSite, setSelectedSite] = useState<any>({}); // 选择的站点
  const [selectionMode, setSelectionMode] = useState<'point' | 'station'>('point'); // 当前选择模式
  const [selectedStation, setSelectedStation] = useState<any>(null); // 当前选中的场站
  const [stationSiteList, setStationSiteList] = useState<any[]>([]); // 当前场站下站点列表
  const [showTipPopup, setShowTipPopup] = useState(false); // 提示弹窗是否显示
  const [tipType, setTipType] = useState(''); // 提示类型 1:上下车点近 2:距上车点远
  const [polygons, setPolygons] = useState<any[]>([]); // 地图围栏
  const [fencePolylines, setFencePolylines] = useState<any[]>([]); // 围栏虚线边框
  const [isOperatingArea, setIsOperatingArea] = useState(false); // 是否在运营区域内
  const [nearbySites, setNearbySites] = useState<any[]>([]); // 附近站点列表
  const [distanceConfig, setDistanceConfig] = useState<any>({}); // 距离限制配置
  const [polyline, setPolyline] = useState<any[]>([]); // 步行路线
  const [userLongitude, setUserLongitude] = useState<number | null>(null); // 用户经度
  const [userLatitude, setUserLatitude] = useState<number | null>(null); // 用户纬度
  const [outOfServiceRange, setOutOfServiceRange] = useState(false); // 是否超出运营范围
  const [walkingDistance, setWalkingDistance] = useState<number | null>(null); // 步行距离
  const [walkingDuration, setWalkingDuration] = useState<number | null>(null); // 步行时长
  const tooltipBgUrl = require('../../../../robotaxi/assets/tooltip-bg.png'); // 提示背景图资源
  const [tooltipBgSrc, setTooltipBgSrc] = useState(tooltipBgUrl); // 提示背景图路径
  const [isAndroid, setIsAndroid] = useState(false); // 是否安卓机型
  const waitingRadarSrc = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/waiting-radar.webp'; // 加载雷达图
  const [nearbyLoading, setNearbyLoading] = useState(false); // 附近站点加载态
  const pendingNearbyRequestCountRef = useRef(0); // 待完成附近站点请求数
  const autoScaleLockedRef = useRef(false); // 自动缩放锁:仅初始化计算一次
  const stationMetaMapRef = useRef<Record<string, any>>({}); // 场站元数据映射(来自围栏)
  const router = Taro.useRouter(); // 路由实例
  const mapPolylines = useMemo(() => {
    return [...fencePolylines, ...polyline];
  }, [fencePolylines, polyline]); // 地图折线集合(围栏边框 + 步行线)
  return {
    showCustomerService,
    setShowCustomerService,
    longitude,
    setLongitude,
    latitude,
    setLatitude,
    mapScale,
    setMapScale,
    showToolTip,
    setShowToolTip,
    bounce,
    setBounce,
    markers,
    setMarkers,
    pageType,
    setPageType,
    selectedSite,
    setSelectedSite,
    selectionMode,
    setSelectionMode,
    selectedStation,
    setSelectedStation,
    stationSiteList,
    setStationSiteList,
    showTipPopup,
    setShowTipPopup,
    tipType,
    setTipType,
    polygons,
    setPolygons,
    fencePolylines,
    setFencePolylines,
    isOperatingArea,
    setIsOperatingArea,
    nearbySites,
    setNearbySites,
    distanceConfig,
    setDistanceConfig,
    polyline,
    setPolyline,
    userLongitude,
    setUserLongitude,
    userLatitude,
    setUserLatitude,
    outOfServiceRange,
    setOutOfServiceRange,
    walkingDistance,
    setWalkingDistance,
    walkingDuration,
    setWalkingDuration,
    tooltipBgUrl,
    tooltipBgSrc,
    setTooltipBgSrc,
    isAndroid,
    setIsAndroid,
    waitingRadarSrc,
    nearbyLoading,
    setNearbyLoading,
    pendingNearbyRequestCountRef,
    autoScaleLockedRef,
    stationMetaMapRef,
    router,
    mapPolylines,
  };
}
src/robotaxi/pages/select-map-site/hooks/use-select-map-site-utils.ts
New file
@@ -0,0 +1,86 @@
const FENCE_TYPE_SITE = 2; // 场站围栏类型
const FENCE_FILL_COLOR_TYPE_SITE = 'rgba(255, 255, 255, 0.3)'; // 场站围栏填充色
const FENCE_STROKE_COLOR_TYPE_SITE = '#FFFFFF'; // 场站围栏边框色
/**
 * 按围栏类型格式化地图覆盖物:type=2 使用白色填充与白色虚线边框。
 */
export const formatGeofenceByType = (geofenceList: any[]) => {
  if (!Array.isArray(geofenceList)) {
    return { polygons: [], fencePolylines: [] };
  }
  const polygons = geofenceList.map((item) => {
    const type = Number(item?.type);
    const points = Array.isArray(item?.points) ? item.points : [];
    return {
      points: points.map((p: any) => ({
        latitude: p.lat,
        longitude: p.lng
      })),
      strokeWidth: type === FENCE_TYPE_SITE ? 1 : 2,
      strokeColor: type === FENCE_TYPE_SITE ? FENCE_STROKE_COLOR_TYPE_SITE : '#A1B1FF',
      fillColor: type === FENCE_TYPE_SITE ? FENCE_FILL_COLOR_TYPE_SITE : '#6d7fd933',
      zIndex: 1
    };
  });
  const fencePolylines = geofenceList.reduce((acc: any[], item: any, index: number) => {
    const type = Number(item?.type);
    if (type !== FENCE_TYPE_SITE || !Array.isArray(item?.points) || item.points.length < 2) {
      return acc;
    }
    const linePoints = item.points.map((p: any) => ({
      latitude: p.lat,
      longitude: p.lng
    }));
    // 虚线边框通过 polyline dottedLine 实现,首尾闭合形成围栏边界。
    acc.push({
      id: 900000 + index,
      points: [...linePoints, linePoints[0]],
      color: FENCE_STROKE_COLOR_TYPE_SITE,
      width: 2,
      dottedLine: true,
      arrowLine: false
    });
    return acc;
  }, []);
  return { polygons, fencePolylines };
};
/**
 * 减少安卓端绘制点位,降低地图折线渲染压力。
 */
export const reduceRoutePoints = (points: any[], maxCount: number) => {
  if (!Array.isArray(points) || points.length <= maxCount) {
    return points;
  }
  const step = Math.ceil(points.length / maxCount);
  const reduced = [];
  for (let i = 0; i < points.length; i += step) {
    reduced.push(points[i]);
  }
  const last = points[points.length - 1];
  if (reduced[reduced.length - 1] !== last) {
    reduced.push(last);
  }
  return reduced;
};
/**
 * 从围栏数据提取场站元信息映射:stationId -> 场站名称/收藏状态。
 */
export const buildStationMetaMap = (geofenceList: any[]) => {
  const stationMetaMap: Record<string, any> = {};
  if (!Array.isArray(geofenceList)) return stationMetaMap;
  geofenceList.forEach((item) => {
    const stationId = item?.stationId;
    if (!stationId) return;
    stationMetaMap[String(stationId)] = {
      stationId: String(stationId),
      name: item?.name || '',
      isCollect: !!item?.stationIsCollect, // 围栏返回的场站收藏字段:stationIsCollect(boolean)
    };
  });
  return stationMetaMap;
};
src/robotaxi/pages/select-map-site/hooks/use-select-map-site-walking-route.ts
New file
@@ -0,0 +1,72 @@
import { useEffect } from 'react';
import { getWalkingRoute } from '../../../../utils/Robotaxi-public-method';
import { reduceRoutePoints } from './use-select-map-site-utils';
type Deps = {
  pageType: string;
  userLatitude: number | null;
  userLongitude: number | null;
  latitude: number | null;
  longitude: number | null;
  selectedSite: any;
  isAndroid: boolean;
  setPolyline: (value: any[]) => void;
  setWalkingDistance: (value: number | null) => void;
  setWalkingDuration: (value: number | null) => void;
};
/**
 * 步行路线副作用:根据当前位置与上车点变化计算步行路线并渲染折线。
 */
export default function useSelectMapSiteWalkingRoute(deps: Deps) {
  useEffect(() => {
    if (deps.pageType !== 'start') {
      deps.setPolyline([]);
      deps.setWalkingDistance(null);
      deps.setWalkingDuration(null);
      return;
    }
    const originLat = deps.userLatitude ?? deps.latitude;
    const originLng = deps.userLongitude ?? deps.longitude;
    if (!originLat || !originLng || !deps.selectedSite?.latitude || !deps.selectedSite?.longitude) {
      deps.setPolyline([]);
      deps.setWalkingDistance(null);
      deps.setWalkingDuration(null);
      return;
    }
    const origin = `${originLng},${originLat}`;
    const destination = `${deps.selectedSite.longitude},${deps.selectedSite.latitude}`;
    getWalkingRoute(origin, destination).then(({ points, distance, duration }) => {
      deps.setWalkingDistance(distance);
      deps.setWalkingDuration(duration);
      if (!Array.isArray(points) || points.length === 0) {
        deps.setPolyline([]);
        return;
      }
      const safePoints = points.filter((item: any) => Number.isFinite(item?.latitude) && Number.isFinite(item?.longitude));
      const displayPoints = deps.isAndroid ? reduceRoutePoints(safePoints, 120) : safePoints;
      if (displayPoints.length === 0) {
        deps.setPolyline([]);
        return;
      }
      deps.setPolyline([{
        points: displayPoints,
        color: '#6D7FD9',
        width: 6,
        dottedLine: true,
        arrowLine: false,
      }]);
    }).catch(() => {
      deps.setPolyline([]);
    });
  }, [
    deps.pageType,
    deps.userLatitude,
    deps.userLongitude,
    deps.latitude,
    deps.longitude,
    deps.selectedSite?.latitude,
    deps.selectedSite?.longitude,
    deps.isAndroid
  ]);
}
src/robotaxi/pages/select-map-site/hooks/use-select-map-site.ts
New file
@@ -0,0 +1,559 @@
import Taro from "@tarojs/taro";
import { useEffect } from 'react';
import {
  findNearestMarker,
  getAutoScaleBySites,
  getDistance,
  fetchMapData,
  extractPointsFromPolygons,
  preloadImageToTemp,
} from '../../../../utils/Robotaxi-public-method';
import { getGeofence } from '../../../api/home';
import { getOrderDistance } from '../../../api/select-map-site';
import { collectPoint } from '../../../api/site-search';
import { checkOtp } from '../../../../utils/tools';
import { jumpTo } from '../../../../utils/jump';
import useSelectMapSiteState from './use-select-map-site-state';
import useSelectMapSiteWalkingRoute from './use-select-map-site-walking-route';
import { buildStationMetaMap, formatGeofenceByType } from './use-select-map-site-utils';
const AUTO_SCALE_MAX = 17; // 自动缩放最大等级
const AUTO_SCALE_MIN = 11; // 自动缩放最小等级
/**
 * 选点页业务编排:聚合状态容器与副作用 hooks,并对外提供稳定页面接口。
 */
export default function useSelectMapSite() {
  const state = useSelectMapSiteState(); // 选点页状态容器
  /**
   * 重置场站选择模式,回到普通站点模式。
   */
  const resetStationSelection = () => {
    state.setSelectionMode('point');
    state.setSelectedStation(null);
    state.setStationSiteList([]);
  };
  /**
   * 根据当前选中站点判断是否进入场站模式,并设置默认站点与场站信息。
   */
  const applySelectionBySite = (site: any, candidateSites?: any[]) => {
    const stationId = site?.stationId ? String(site.stationId) : '';
    if (!stationId) {
      resetStationSelection();
      state.setSelectedSite(site || {});
      return site || {};
    }
    const stationMeta = state.stationMetaMapRef.current[stationId];
    const sourceSites = Array.isArray(candidateSites) ? candidateSites : state.nearbySites;
    const stationSites = sourceSites.filter((item: any) => String(item?.stationId || '') === stationId);
    if (!stationMeta || stationSites.length === 0) {
      resetStationSelection();
      state.setSelectedSite(site || {});
      return site || {};
    }
    state.setSelectionMode('station');
    state.setSelectedStation(stationMeta);
    state.setStationSiteList(stationSites);
    const defaultStationSite = stationSites[0]; // 进入场站模式默认选中首个站点
    state.setSelectedSite(defaultStationSite);
    return defaultStationSite;
  };
  /**
   * 计算是否超出可选范围(运营区外或起终点电子围栏不一致)。
   */
  const getOutOfServiceRange = (inside: boolean, site: any) => {
    const cachedStartSite = Taro.getStorageSync('startSite');
    const cachedEndSite = Taro.getStorageSync('endSite');
    const isCrossArea = (state.pageType === 'start'
      ? (cachedEndSite?.electronicAreaId && site?.electronicAreaId && cachedEndSite.electronicAreaId !== site.electronicAreaId)
      : (cachedStartSite?.electronicAreaId && site?.electronicAreaId && cachedStartSite.electronicAreaId !== site.electronicAreaId));
    return !inside || isCrossArea;
  };
  /**
   * 更新地图业务数据:围栏内判断、站点列表、自动缩放。
   */
  const updateMapData = async (lat: number, lng: number) => {
    const selectedCity = Taro.getStorageSync('selectedCity');
    state.pendingNearbyRequestCountRef.current += 1;
    state.setNearbyLoading(true);
    try {
      const { isInside, newMarkers, sites } = await fetchMapData(lat, lng, selectedCity?.code);
      state.setIsOperatingArea(isInside);
      state.setMarkers(newMarkers);
      state.setNearbySites(sites);
      if (state.selectionMode === 'station' && state.selectedStation?.stationId) {
        const nextStationSites = sites.filter((item: any) => String(item?.stationId || '') === String(state.selectedStation.stationId));
        state.setStationSiteList(nextStationSites);
      }
      if (!state.autoScaleLockedRef.current) {
        const autoScale = getAutoScaleBySites(sites?.length > 0 ? sites : newMarkers);
        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 };
    } finally {
      state.pendingNearbyRequestCountRef.current = Math.max(0, state.pendingNearbyRequestCountRef.current - 1);
      if (state.pendingNearbyRequestCountRef.current === 0) {
        state.setNearbyLoading(false);
      }
    }
  };
  /**
   * 获取用户定位并作为地图中心点。
   */
  const getLocationInfo = () => {
    Taro.getLocation({ type: 'gcj02' }).then((res) => {
      state.setLatitude(res.latitude);
      state.setLongitude(res.longitude);
      updateMapData(res.latitude, res.longitude).then(({ isInside }) => {
        state.setOutOfServiceRange(getOutOfServiceRange(isInside, state.selectedSite));
      });
    });
  };
  /**
   * 获取用户定位,仅用于步行路线起点计算。
   */
  const fetchUserLocation = () => {
    Taro.getLocation({ type: 'gcj02' }).then((res) => {
      state.setUserLatitude(res.latitude);
      state.setUserLongitude(res.longitude);
    });
  };
  /**
   * 返回当前位置。
   */
  const toOrigin = () => {
    const mapCtx = Taro.createMapContext('map');
    mapCtx.moveToLocation();
  };
  /**
   * 地图区域变化时触发,结束后按阈值吸附到最近站点。
   */
  const handleRegionChange = (e: any) => {
    const type = e?.type || e?.detail?.type;
    const causedBy = e?.causedBy || e?.detail?.causedBy;
    if (type === 'begin') {
      state.setShowToolTip(false);
      return;
    }
    if (type !== 'end') return;
    state.setBounce(true);
    if (causedBy === 'gesture' || causedBy === 'drag') {
      const mapCtx = Taro.createMapContext('map');
      mapCtx.getCenterLocation({
        success: async (res) => {
          const { latitude: lat, longitude: lng } = res;
          const { newMarkers, sites, isInside } = await updateMapData(lat, lng);
          const { targetMarker, minDistance } = findNearestMarker(lat, lng, newMarkers);
          if (targetMarker && minDistance < state.distanceConfig.adsorptionDistance) {
            mapCtx.moveToLocation({
              latitude: targetMarker.latitude,
              longitude: targetMarker.longitude,
            });
            const activeSite = applySelectionBySite(targetMarker, sites);
            state.setOutOfServiceRange(getOutOfServiceRange(isInside, activeSite));
            state.setLatitude(activeSite?.latitude || targetMarker.latitude);
            state.setLongitude(activeSite?.longitude || targetMarker.longitude);
            state.setShowToolTip(true);
          } else {
            resetStationSelection();
            state.setSelectedSite({});
            state.setOutOfServiceRange(getOutOfServiceRange(isInside, {}));
            state.setLatitude(lat);
            state.setLongitude(lng);
            state.setShowToolTip(true);
          }
        }
      });
    }
    setTimeout(() => {
      state.setBounce(false);
      state.setShowToolTip(true);
    }, 600);
  };
  /**
   * 地图 marker 点击:直接聚焦并更新当前选中站点。
   */
  const handleMarkerTap = (e: any) => {
    const markerId = e?.detail?.markerId;
    if (markerId === undefined) return;
    const targetMarker = state.markers.find((m) => m.id === markerId);
    if (!targetMarker) return;
    state.setShowToolTip(false);
    state.setBounce(true);
    state.setLatitude(targetMarker.latitude);
    state.setLongitude(targetMarker.longitude);
    setTimeout(() => {
      state.setBounce(false);
      const activeSite = applySelectionBySite(targetMarker, state.nearbySites);
      state.setOutOfServiceRange(getOutOfServiceRange(state.isOperatingArea, activeSite));
      state.setLatitude(activeSite?.latitude || targetMarker.latitude);
      state.setLongitude(activeSite?.longitude || targetMarker.longitude);
      state.setShowToolTip(true);
    }, 600);
  };
  /**
   * 场站模式切换子站点:仅更新选中站点,不切换场站。
   */
  const changeStationSite = (site: any) => {
    if (!site?.latitude || !site?.longitude) return;
    state.setSelectedSite(site);
    state.setLatitude(site.latitude);
    state.setLongitude(site.longitude);
    const mapCtx = Taro.createMapContext('map');
    mapCtx.moveToLocation({
      latitude: site.latitude,
      longitude: site.longitude,
    });
  };
  /**
   * 关闭客服弹窗。
   */
  const handleCloseCustomerService = () => {
    state.setShowCustomerService(false);
  };
  /**
   * 打开客服电话拨打。
   */
  const openService = () => {
    const phoneNumber = Taro.getStorageSync('customerServicePhone');
    Taro.makePhoneCall({ phoneNumber });
  };
  /**
   * 返回上一页。
   */
  const toBack = () => {
    Taro.navigateBack({ delta: 1 });
  };
  /**
   * 确认选择站点(包含起终点距离与电子围栏校验)。
   */
  const confirmSite = async () => {
    if (!state.selectedSite.name) {
      Taro.showToast({ title: '请选择站点', icon: 'none' });
      return;
    }
    if (state.pageType === 'start') {
      const endSiteCache = Taro.getStorageSync('endSite');
      if (endSiteCache?.electronicAreaId && state.selectedSite?.electronicAreaId && endSiteCache.electronicAreaId !== state.selectedSite.electronicAreaId) {
        Taro.showToast({ title: '起终点需在同一运营区域', icon: 'none' });
        return;
      }
      if (state.userLatitude && state.userLongitude) {
        const distance = getDistance(state.userLatitude, state.userLongitude, state.selectedSite.latitude, state.selectedSite.longitude);
        if (distance > state.distanceConfig.maxDistance) {
          state.setTipType('2');
          state.setShowTipPopup(true);
          return;
        }
      }
      Taro.setStorageSync('startSite', state.selectedSite);
      if (state.router.params.skipSource) {
        Taro.navigateBack();
        return;
      }
      Taro.navigateBack({ delta: 2 });
      return;
    }
    const otp = await checkOtp(false);
    if (!otp) {
      jumpTo({ url: '/pages/user/login?hideBack=1' });
      return;
    }
    const startSiteCache = Taro.getStorageSync('startSite');
    if (startSiteCache?.electronicAreaId && state.selectedSite?.electronicAreaId && startSiteCache.electronicAreaId !== state.selectedSite.electronicAreaId) {
      Taro.showToast({ title: '起终点需在同一运营区域', icon: 'none' });
      return;
    }
    if (startSiteCache && startSiteCache.longitude && startSiteCache.latitude) {
      const distance = getDistance(startSiteCache.latitude, startSiteCache.longitude, state.selectedSite.latitude, state.selectedSite.longitude);
      if (distance < state.distanceConfig.minDistance) {
        state.setTipType('1');
        state.setShowTipPopup(true);
        return;
      }
    }
    Taro.setStorageSync('endSite', state.selectedSite);
    Taro.reLaunch({
      url: Taro.getStorageSync('orderId') ? '/robotaxi/pages/begin-route/index?skipPageFlag=1' : '/robotaxi/pages/begin-route/index',
    });
  };
  /**
   * 关闭提示弹窗。
   */
  const handleCloseTipPopup = () => {
    state.setShowTipPopup(false);
    setTimeout(() => {
      state.setTipType('');
    }, 600);
  };
  /**
   * 跳转优惠券列表,未登录时先引导登录。
   */
  const toCoupon = async () => {
    const otp = await checkOtp(false);
    if (!otp) {
      jumpTo({ url: '/pages/user/login?hideBack=1' });
      return;
    }
    Taro.navigateTo({ url: '/robotaxi/pages/coupon/index' });
  };
  /**
   * 跳转订单页,未登录时先引导登录。
   */
  const toOrder = async () => {
    const otp = await checkOtp(false);
    if (!otp) {
      jumpTo({ url: '/pages/user/login?hideBack=1' });
      return;
    }
    Taro.navigateTo({ url: '/robotaxi/pages/order/index' });
  };
  /**
   * 收藏/取消收藏当前选中站点。
   */
  const handleCollect = async (e: any) => {
    e.stopPropagation();
    try {
      const isStationMode = state.selectionMode === 'station' && state.selectedStation?.stationId;
      const collectSource = isStationMode ? state.selectedStation : state.selectedSite;
      const type = isStationMode
        ? (collectSource?.isCollect ? 2 : 1)
        : (collectSource?.isCollect === 1 ? 2 : 1);
      const payload: any = isStationMode
        ? { itemType: 2, stationId: String(state.selectedStation.stationId), type } // 场站收藏只传 stationId
        : {
          itemType: 1,
          latitude: state.selectedSite.latitude,
          longitude: state.selectedSite.longitude,
          type
        };
      if (!isStationMode) {
        if (state.selectedSite.pointId) {
          payload.pointId = state.selectedSite.pointId;
        } else if (state.selectedSite.id) {
          payload.pointId = state.selectedSite.id;
        }
      }
      await collectPoint(payload);
      if (isStationMode) {
        const nextStation = { ...state.selectedStation, isCollect: type === 1 };
        state.setSelectedStation(nextStation);
        state.stationMetaMapRef.current[String(nextStation.stationId)] = nextStation;
      } else {
        const nextSite = { ...state.selectedSite, isCollect: type };
        state.setSelectedSite(nextSite);
        if (state.pageType === 'start') {
          Taro.setStorageSync('startSite', nextSite);
        }
        if (state.pageType === 'end') {
          Taro.setStorageSync('endSite', nextSite);
        }
      }
      Taro.showToast({
        title: type === 2 ? '已取消收藏' : '收藏成功',
        icon: 'none'
      });
    } catch (error) {
      console.log('收藏操作失败', error);
    }
  };
  useEffect(() => {
    state.setOutOfServiceRange(getOutOfServiceRange(state.isOperatingArea, state.selectedSite));
  }, [state.isOperatingArea, state.pageType, state.selectedSite?.electronicAreaId]);
  useEffect(() => {
    const systemInfo = Taro.getSystemInfoSync();
    state.setIsAndroid(systemInfo?.platform === 'android');
    const pType = state.router.params.pageType || 'end';
    state.setPageType(pType);
    let cachedSite: any = null;
    if (pType === 'start') {
      cachedSite = Taro.getStorageSync('startSite');
    } else if (pType === 'end') {
      cachedSite = Taro.getStorageSync('endSite');
      if (!cachedSite) {
        cachedSite = Taro.getStorageSync('startSite');
      }
    }
    const hasCachedSite = !!(cachedSite && cachedSite.latitude && cachedSite.longitude);
    if (hasCachedSite) {
      state.setLatitude(cachedSite.latitude);
      state.setLongitude(cachedSite.longitude);
      applySelectionBySite(cachedSite);
      updateMapData(cachedSite.latitude, cachedSite.longitude).then(({ isInside }) => {
        state.setOutOfServiceRange(getOutOfServiceRange(isInside, cachedSite));
      });
    }
    fetchUserLocation();
    getOrderDistance().then((res: any) => {
      if (res) {
        state.setDistanceConfig({
          maxDistance: res.maxDistance,
          minDistance: res.minDistance,
          adsorptionDistance: res.adsorptionDistance
        });
      }
    }).catch((err) => {
      console.error('获取距离配置失败', err);
    });
    const selectedCity = Taro.getStorageSync('selectedCity');
    if (selectedCity && selectedCity.code) {
      getGeofence({ cityCode: selectedCity.code }).then((res: any) => {
        if (res && Array.isArray(res)) {
          const { polygons: newPolygons, fencePolylines: nextFencePolylines } = formatGeofenceByType(res);
          state.stationMetaMapRef.current = buildStationMetaMap(res);
          state.setPolygons(newPolygons);
          state.setFencePolylines(nextFencePolylines);
          if (!hasCachedSite && res.length > 0 && res[0]?.points?.length > 0) {
            const firstPoint = res[0].points[0];
            const lat = Number(firstPoint.lat);
            const lng = Number(firstPoint.lng);
            state.setLatitude(lat);
            state.setLongitude(lng);
            updateMapData(lat, lng).then(({ isInside }) => {
              state.setOutOfServiceRange(getOutOfServiceRange(isInside, {}));
            });
            const mapCtx = Taro.createMapContext('map');
            const points = extractPointsFromPolygons(newPolygons);
            mapCtx.includePoints({
              points,
              padding: [100, 100, 100, 100]
            });
          } else if (!hasCachedSite) {
            getLocationInfo();
          }
        } else {
          state.stationMetaMapRef.current = {};
          state.setPolygons([]);
          state.setFencePolylines([]);
          if (!hasCachedSite) {
            getLocationInfo();
          }
        }
      }).catch((err) => {
        console.error('获取电子围栏失败', err);
        state.stationMetaMapRef.current = {};
        state.setPolygons([]);
        state.setFencePolylines([]);
        if (!hasCachedSite) {
          getLocationInfo();
        }
      });
    } else if (!hasCachedSite) {
      getLocationInfo();
    }
  }, []);
  useEffect(() => {
    let cancelled = false;
    const run = async () => {
      const local = await preloadImageToTemp(state.tooltipBgUrl);
      if (!cancelled) state.setTooltipBgSrc(local);
    };
    run();
    return () => { cancelled = true; };
  }, []);
  useSelectMapSiteWalkingRoute({
    pageType: state.pageType,
    userLatitude: state.userLatitude,
    userLongitude: state.userLongitude,
    latitude: state.latitude,
    longitude: state.longitude,
    selectedSite: state.selectedSite,
    isAndroid: state.isAndroid,
    setPolyline: state.setPolyline,
    setWalkingDistance: state.setWalkingDistance,
    setWalkingDuration: state.setWalkingDuration,
  });
  useEffect(() => {
    if (state.selectionMode !== 'station') return;
    if (!state.selectedStation?.stationId) return;
    const stationId = String(state.selectedStation.stationId);
    const nextStationSites = state.nearbySites.filter((item: any) => String(item?.stationId || '') === stationId);
    state.setStationSiteList(nextStationSites);
    if (!state.selectedSite?.stationId || String(state.selectedSite.stationId) !== stationId) {
      if (nextStationSites.length > 0) {
        state.setSelectedSite(nextStationSites[0]);
      }
    }
  }, [state.nearbySites, state.selectionMode, state.selectedStation?.stationId]);
  useEffect(() => {
    const stationId = state.selectedSite?.stationId ? String(state.selectedSite.stationId) : '';
    if (!stationId || state.selectionMode === 'station') return;
    if (!state.stationMetaMapRef.current[stationId]) return;
    if (!Array.isArray(state.nearbySites) || state.nearbySites.length === 0) return;
    applySelectionBySite(state.selectedSite, state.nearbySites);
  }, [state.nearbySites, state.selectedSite?.stationId, state.selectionMode]);
  return {
    longitude: state.longitude,
    latitude: state.latitude,
    mapScale: state.mapScale,
    markers: state.markers,
    mapPolylines: state.mapPolylines,
    polygons: state.polygons,
    handleRegionChange,
    handleMarkerTap,
    toBack,
    openService,
    toCoupon,
    toOrder,
    nearbyLoading: state.nearbyLoading,
    isAndroid: state.isAndroid,
    waitingRadarSrc: state.waitingRadarSrc,
    showToolTip: state.showToolTip,
    selectedSite: state.selectedSite,
    selectionMode: state.selectionMode,
    selectedStation: state.selectedStation,
    stationSiteList: state.stationSiteList,
    outOfServiceRange: state.outOfServiceRange,
    tooltipBgSrc: state.tooltipBgSrc,
    pageType: state.pageType,
    bounce: state.bounce,
    toOrigin,
    walkingDuration: state.walkingDuration,
    walkingDistance: state.walkingDistance,
    nearbySites: state.nearbySites,
    handleCollect,
    changeStationSite,
    confirmSite,
    showCustomerService: state.showCustomerService,
    handleCloseCustomerService,
    showTipPopup: state.showTipPopup,
    handleCloseTipPopup,
    tipType: state.tipType,
  };
}
src/robotaxi/pages/select-map-site/index.less
@@ -194,6 +194,23 @@
        padding-bottom: 82px;
        background: #FFFFFF;
        .content-box-item {
            display: flex;
            justify-content: space-between;
            .content-box-item-title {
                flex: 1;
                flex-shrink: 0;
            }
            .collect-icon {
                margin-top: 6px;
                flex-shrink: 0;
                width: 30px;
                height: 28.64px;
            }
        }
        .content-limit-bg {
            position: absolute;
            top: 0;
@@ -233,9 +250,18 @@
            }
        }
        .site-list-wrap {
            position: relative;
        }
        .site-list {
            // height: 240px;
            overflow-y: auto;
            max-height: 360px;
            padding-right: 16px;
            box-sizing: border-box;
        }
        .site-list-inner {
            min-height: 100%;
        }
        .site-item {
@@ -310,4 +336,4 @@
            background: rgba(0, 0, 0, 0.15);
        }
    }
}
}
src/robotaxi/pages/select-map-site/index.tsx
@@ -1,676 +1,68 @@
import Taro from "@tarojs/taro"
import { useEffect, useRef, useState } from 'react';
import { View, Map, Text, Image, ScrollView } from '@tarojs/components';
import './index.less';
import SelectMapSiteView from './components/select-map-site-view';
import useSelectMapSite from './hooks/use-select-map-site';
import CustomerServicePopup from '../../components/customer-service';
import TipPopup from './components/tip-popup';
import backIcon from '../../../robotaxi/assets/back-icon.png';
import personalCenter from '../../../robotaxi/assets/personal-center.png';
import coupon from '../../../robotaxi/assets/coupon.png';
import suggest from '../../../robotaxi/assets/suggest.png';
import originIcon from '../../../robotaxi/assets/origin-icon.png';
import pointer from '../../../robotaxi/assets/pointer.png';
import activeStar from '../../../robotaxi/assets/active-star.png';
import noStar from '../../../robotaxi/assets/no-star.png';
import backIcon from '../../../robotaxi/assets/back-icon.png'
import personalCenter from '../../../robotaxi/assets/personal-center.png'
import coupon from '../../../robotaxi/assets/coupon.png'
import suggest from '../../../robotaxi/assets/suggest.png'
import originIcon from '../../../robotaxi/assets/origin-icon.png'
import pointer from '../../../robotaxi/assets/pointer.png'
import activeStar from '../../../robotaxi/assets/active-star.png'
import noStar from '../../../robotaxi/assets/no-star.png'
import {
  findNearestMarker,
  getAutoScaleBySites,
  getDistance,
  formatGeofencePolygons,
  fetchMapData,
  getWalkingRoute,
  extractPointsFromPolygons,
  preloadImageToTemp,
  formatTripDistance,
  formatTripDurationFromSeconds
} from '../../../utils/Robotaxi-public-method';
import { getGeofence } from '../../api/home';
import { getOrderDistance } from '../../api/select-map-site';
import { collectPoint } from '../../api/site-search';
import { checkOtp } from '../../../utils/tools';
import { jumpTo } from '../../../utils/jump';
/**
 * 地图选点页入口:负责 hook 编排与视图组装。
 */
export default function SelectMapSite() {
  const AUTO_SCALE_MAX = 17; // 自动缩放最大等级
  const AUTO_SCALE_MIN = 11; // 自动缩放最小等级
  const [showCustomerService, setShowCustomerService] = useState(false); // 客服弹窗是否显示
  const [longitude, setLongitude] = useState<number | null>(null); // 经度
  const [latitude, setLatitude] = useState<number | null>(null); // 纬度
  const [mapScale, setMapScale] = useState(14); // 地图缩放等级
  const [showToolTip, setShowToolTip] = useState(true); // 提示是否显示
  const [bounce, setBounce] = useState(false); // 是否弹跳
  const [markers, setMarkers] = useState<any[]>([]); // 标记点列表
  const [pageType, setPageType] = useState('end'); // 起点start 终点end
  const [selectedSite, setSelectedSite] = useState<any>({}); // 选择的站点
  const [showTipPopup, setShowTipPopup] = useState(false); // 提示弹窗是否显示
  const [tipType, setTipType] = useState(''); // 提示类型 1:上下车点近 2:距上车点远
  const [startSite, setStartSite] = useState<any>(null); // 上车点
  const [polygons, setPolygons] = useState<any[]>([]); // 地图围栏
  const [isOperatingArea, setIsOperatingArea] = useState(false); // 是否在运营区域内
  const [nearbySites, setNearbySites] = useState<any[]>([]); // 附近站点列表
  const [distanceConfig, setDistanceConfig] = useState({}); // 距离限制配置
  const [polyline, setPolyline] = useState<any[]>([]);
  const [userLongitude, setUserLongitude] = useState<number | null>(null);
  const [userLatitude, setUserLatitude] = useState<number | null>(null);
  const [outOfServiceRange, setOutOfServiceRange] = useState(false);
  const [walkingDistance, setWalkingDistance] = useState<number | null>(null);
  const [walkingDuration, setWalkingDuration] = useState<number | null>(null);
  const tooltipBgUrl = require('../../../robotaxi/assets/tooltip-bg.png');
  const [tooltipBgSrc, setTooltipBgSrc] = useState(tooltipBgUrl);
  const [isAndroid, setIsAndroid] = useState(false);
  const waitingRadarSrc = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/waiting-radar.webp';
  const [nearbyLoading, setNearbyLoading] = useState(false);
  const pendingNearbyRequestCountRef = useRef(0);
  const autoScaleLockedRef = useRef(false); // 自动缩放锁:仅初始化计算一次
  const router = Taro.useRouter();
  const getOutOfServiceRange = (inside: boolean, site: any) => {
    const cachedStartSite = Taro.getStorageSync('startSite');
    const cachedEndSite = Taro.getStorageSync('endSite');
    const isCrossArea = (pageType === 'start'
      ? (cachedEndSite?.electronicAreaId && site?.electronicAreaId && cachedEndSite.electronicAreaId !== site.electronicAreaId)
      : (cachedStartSite?.electronicAreaId && site?.electronicAreaId && cachedStartSite.electronicAreaId !== site.electronicAreaId));
    return !inside || isCrossArea;
  };
  // 页面初始化
  useEffect(() => {
    const systemInfo = Taro.getSystemInfoSync();
    setIsAndroid(systemInfo?.platform === 'android');
    const pType = router.params.pageType || 'end';
    setPageType(pType);
    // 获取对应缓存
    let cachedSite: any = null;
    if (pType === 'start') {
      const s = Taro.getStorageSync('startSite');
      if (s) {
        setStartSite(s);
        cachedSite = s;
      }
    } else if (pType === 'end') {
      cachedSite = Taro.getStorageSync('endSite');
      if (!cachedSite) {
        const s = Taro.getStorageSync('startSite');
        if (s) {
          setStartSite(s);
          cachedSite = s;
        }
      }
    }
    const hasCachedSite = !!(cachedSite && cachedSite.latitude && cachedSite.longitude);
    // 有缓存,优先使用缓存回显
    if (cachedSite && cachedSite.latitude && cachedSite.longitude) {
      setLatitude(cachedSite.latitude);
      setLongitude(cachedSite.longitude);
      setSelectedSite(cachedSite);
      updateMapData(cachedSite.latitude, cachedSite.longitude).then(({ isInside }) => {
        setOutOfServiceRange(getOutOfServiceRange(isInside, cachedSite));
      });
    }
    fetchUserLocation();
    // 获取距离配置
    getOrderDistance().then((res: any) => {
      if (res) {
        setDistanceConfig({
          maxDistance: res.maxDistance,
          minDistance: res.minDistance,
          adsorptionDistance: res.adsorptionDistance
        });
      }
    }).catch(err => {
      console.error('获取距离配置失败', err);
    });
    // 获取电子围栏
    const selectedCity = Taro.getStorageSync('selectedCity');
    if (selectedCity && selectedCity.code) {
      getGeofence({ cityCode: selectedCity.code }).then((res: any) => {
        if (res && Array.isArray(res)) {
          const newPolygons = formatGeofencePolygons(res);
          setPolygons(newPolygons);
          if (!hasCachedSite && res.length > 0 && res[0]?.points?.length > 0) {
            const firstPoint = res[0].points[0];
            const lat = Number(firstPoint.lat);
            const lng = Number(firstPoint.lng);
            setLatitude(lat);
            setLongitude(lng);
            updateMapData(lat, lng).then(({ isInside }) => {
              setOutOfServiceRange(getOutOfServiceRange(isInside, {}));
            });
            const mapCtx = Taro.createMapContext('map');
            // mapCtx.moveToLocation({
            //   latitude: lat,
            //   longitude: lng,
            // });
            const points = extractPointsFromPolygons(newPolygons);
            mapCtx.includePoints({
              points: points,
              padding: [100, 100, 100, 100]
            });
          } else if (!hasCachedSite) {
            getLocationInfo();
          }
        } else {
          setPolygons([]);
          if (!hasCachedSite) {
            getLocationInfo();
          }
        }
      }).catch(err => {
        console.error('获取电子围栏失败', err);
        setPolygons([]);
        if (!hasCachedSite) {
          getLocationInfo();
        }
      });
    } else if (!hasCachedSite) {
      getLocationInfo();
    }
  }, []);
  useEffect(() => {
    let cancelled = false;
    const run = async () => {
      const local = await preloadImageToTemp(tooltipBgUrl);
      if (!cancelled) setTooltipBgSrc(local);
    };
    run();
    return () => { cancelled = true; };
  }, []);
  // 更新地图数据
  const updateMapData = async (lat: number, lng: number) => {
    const selectedCity = Taro.getStorageSync('selectedCity');
    pendingNearbyRequestCountRef.current += 1;
    setNearbyLoading(true);
    try {
      const { isInside, electronicAreaId, newMarkers, sites } = await fetchMapData(lat, lng, selectedCity?.code);
      setIsOperatingArea(isInside);
      setMarkers(newMarkers);
      setNearbySites(sites);
      if (!autoScaleLockedRef.current) {
        const autoScale = getAutoScaleBySites(sites?.length > 0 ? sites : newMarkers);
        if (typeof autoScale === 'number') {
          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;
          });
        }
        autoScaleLockedRef.current = true;
      }
      return { isInside, newMarkers, sites };
    } finally {
      pendingNearbyRequestCountRef.current = Math.max(0, pendingNearbyRequestCountRef.current - 1);
      if (pendingNearbyRequestCountRef.current === 0) {
        setNearbyLoading(false);
      }
    }
  }
  useEffect(() => {
    setOutOfServiceRange(getOutOfServiceRange(isOperatingArea, selectedSite));
  }, [isOperatingArea, pageType, selectedSite?.electronicAreaId]);
  // 获取用户位置信息
  const getLocationInfo = () => {
    Taro.getLocation({ type: 'gcj02' })
      .then(res => {
        setLatitude(res.latitude);
        setLongitude(res.longitude);
        updateMapData(res.latitude, res.longitude).then(({ isInside }) => {
          setOutOfServiceRange(getOutOfServiceRange(isInside, selectedSite));
        });
      })
  }
  const fetchUserLocation = () => {
    Taro.getLocation({ type: 'gcj02' })
      .then(res => {
        setUserLatitude(res.latitude);
        setUserLongitude(res.longitude);
      })
  }
  // 返回当前位置
  const toOrigin = () => {
    const mapCtx = Taro.createMapContext('map');
    mapCtx.moveToLocation();
  }
  // 地图区域变化时触发,判断是否吸附到最近的站点停靠点
  const handleRegionChange = (e: any) => {
    const type = e?.type || e?.detail?.type;
    // 获取触发源: gesture (手势), update (接口调用)
    const causedBy = e?.causedBy || e?.detail?.causedBy;
    if (type === 'begin') {
      setShowToolTip(false);
    } else if (type === 'end') {
      setBounce(true);
      // 只有用户手势拖动触发的移动才进行吸附判断,避免 moveToLocation 触发死循环
      if (causedBy === 'gesture' || causedBy === 'drag') {
        const mapCtx = Taro.createMapContext('map');
        mapCtx.getCenterLocation({
          success: async (res) => {
            const { latitude: lat, longitude: lng } = res;
            const { newMarkers, isInside } = await updateMapData(lat, lng);
            const { targetMarker, minDistance } = findNearestMarker(lat, lng, newMarkers);
            // 如果最近的点在一定范围内(例如 200米),则吸附
            if (targetMarker && minDistance < distanceConfig.adsorptionDistance) {
              // 使用 moveToLocation 实现吸附效果
              mapCtx.moveToLocation({
                latitude: targetMarker.latitude,
                longitude: targetMarker.longitude,
              });
              setSelectedSite(targetMarker);
              setOutOfServiceRange(getOutOfServiceRange(isInside, targetMarker));
              setLatitude(targetMarker.latitude);
              setLongitude(targetMarker.longitude);
              setShowToolTip(true);
            } else {
              setSelectedSite({});
              setOutOfServiceRange(getOutOfServiceRange(isInside, {}));
              setLatitude(lat);
              setLongitude(lng);
              setShowToolTip(true);
            }
          }
        });
      }
      setTimeout(() => {
        setBounce(false);
        setShowToolTip(true);
      }, 600);
    }
  }
  const handleCloseCustomerService = () => {
    setShowCustomerService(false);
  };
  const openService = () => {
    const phoneNumber = Taro.getStorageSync('customerServicePhone');
    Taro.makePhoneCall({
      phoneNumber,
    })
  }
  const toBack = () => {
    Taro.navigateBack({
      delta: 1,
    })
  }
  const confirmSite = async () => {
    if (!selectedSite.name) {
      Taro.showToast({
        title: '请选择站点',
        icon: 'none',
      })
      return
    }
    if (pageType === 'start') {
      const endSiteCache = Taro.getStorageSync('endSite');
      if (endSiteCache?.electronicAreaId && selectedSite?.electronicAreaId && endSiteCache.electronicAreaId !== selectedSite.electronicAreaId) {
        Taro.showToast({
          title: '起终点需在同一运营区域',
          icon: 'none',
        })
        return
      }
      // 校验当前位置与选择的上车点距离
      if (userLatitude && userLongitude) {
        const distance = getDistance(userLatitude, userLongitude, selectedSite.latitude, selectedSite.longitude);
        if (distance > distanceConfig.maxDistance) {
          setTipType('2');
          setShowTipPopup(true);
          return;
        }
      }
      Taro.setStorageSync('startSite', selectedSite)
      if (router.params.skipSource) {
        Taro.navigateBack()
        return
      }
      Taro.navigateBack({
        delta: 2,
      })
    }
    if (pageType === 'end') {
      const otp = await checkOtp(false);
      if (!otp) {
        jumpTo({ url: '/pages/user/login?hideBack=1' });
        return
      }
      const startSiteCache = Taro.getStorageSync('startSite');
      if (startSiteCache?.electronicAreaId && selectedSite?.electronicAreaId && startSiteCache.electronicAreaId != selectedSite.electronicAreaId) {
        Taro.showToast({
          title: '起终点需在同一运营区域',
          icon: 'none',
        })
        return
      }
      // 校验上车点与目的地的距离
      if (startSiteCache && startSiteCache.longitude && startSiteCache.latitude) {
        const distance = getDistance(startSiteCache.latitude, startSiteCache.longitude, selectedSite.latitude, selectedSite.longitude);
        if (distance < distanceConfig.minDistance) {
          setTipType('1');
          setShowTipPopup(true);
          return;
        }
      }
      Taro.setStorageSync('endSite', selectedSite)
      Taro.reLaunch({
        url: Taro.getStorageSync('orderId') ? `/robotaxi/pages/begin-route/index?skipPageFlag=1` : `/robotaxi/pages/begin-route/index`,
      })
    }
  }
  // 关闭提示弹窗
  const handleCloseTipPopup = () => {
    setShowTipPopup(false);
    setTimeout(() => {
      setTipType('');
    }, 600);
  }
  // 切换子站点
  const changeChildrenSite = (site: any) => {
    setSelectedSite(site);
    setLatitude(site.latitude);
    setLongitude(site.longitude);
    const mapCtx = Taro.createMapContext('map');
    mapCtx.moveToLocation({
      latitude: site.latitude,
      longitude: site.longitude,
    });
  }
  const reducePoints = (points: any[], maxCount: number) => {
    if (!Array.isArray(points) || points.length <= maxCount) {
      return points;
    }
    const step = Math.ceil(points.length / maxCount);
    const reduced = [];
    for (let i = 0; i < points.length; i += step) {
      reduced.push(points[i]);
    }
    const last = points[points.length - 1];
    if (reduced[reduced.length - 1] !== last) {
      reduced.push(last);
    }
    return reduced;
  }
  useEffect(() => {
    if (pageType !== 'start') {
      setPolyline([]);
      setWalkingDistance(null);
      setWalkingDuration(null);
      return;
    }
    const originLat = userLatitude ?? latitude;
    const originLng = userLongitude ?? longitude;
    if (!originLat || !originLng || !selectedSite?.latitude || !selectedSite?.longitude) {
      setPolyline([]);
      setWalkingDistance(null);
      setWalkingDuration(null);
      return;
    }
    const origin = `${originLng},${originLat}`;
    const destination = `${selectedSite.longitude},${selectedSite.latitude}`;
    getWalkingRoute(origin, destination).then(({ points, distance, duration }) => {
      setWalkingDistance(distance);
      setWalkingDuration(duration);
      if (!Array.isArray(points) || points.length === 0) {
        setPolyline([]);
        return;
      }
      const safePoints = points.filter((item: any) => Number.isFinite(item?.latitude) && Number.isFinite(item?.longitude));
      const displayPoints = isAndroid ? reducePoints(safePoints, 120) : safePoints;
      if (displayPoints.length === 0) {
        setPolyline([]);
        return;
      }
      const line = [{
        points: displayPoints,
        color: '#6D7FD9',
        width: 6,
        dottedLine: true,
        arrowLine: false,
      }];
      setPolyline(line);
    }).catch(() => {
      setPolyline([]);
    });
  }, [pageType, userLatitude, userLongitude, latitude, longitude, selectedSite?.latitude, selectedSite?.longitude, isAndroid]);
  const toCoupon = async () => {
    const otp = await checkOtp(false);
    if (!otp) {
      jumpTo({ url: '/pages/user/login?hideBack=1' });
      return
    }
    Taro.navigateTo({
      url: '/robotaxi/pages/coupon/index',
    })
  }
  const toOrder = async () => {
    const otp = await checkOtp(false);
    if (!otp) {
      jumpTo({ url: '/pages/user/login?hideBack=1' });
      return
    }
    Taro.navigateTo({
      url: '/robotaxi/pages/order/index',
    })
  }
  // 处理点击地图 marker
  const handleMarkerTap = (e: any) => {
    const markerId = e?.detail?.markerId;
    if (markerId === undefined) return;
    const targetMarker = markers.find(m => m.id === markerId);
    if (targetMarker) {
      // 先隐藏tooltip,并触发指针跳动动画
      setShowToolTip(false);
      setBounce(true);
      // 直接更新经纬度实现跳转
      setLatitude(targetMarker.latitude);
      setLongitude(targetMarker.longitude);
      // 等待指针动画(600ms)结束后,再展示tooltip和吸附状态
      setTimeout(() => {
        setBounce(false);
        setSelectedSite(targetMarker);
        setOutOfServiceRange(getOutOfServiceRange(isOperatingArea, targetMarker));
        setShowToolTip(true);
      }, 600);
    }
  };
    const handleCollect = async (e: any) => {
      e.stopPropagation();
      try {
        const type = selectedSite.isCollect === 1 ? 2 : 1; // 1=收藏 2=取消收藏
        const payload: any = {
          itemType: 1, // 默认为站点处理
          latitude: selectedSite.latitude,
          longitude: selectedSite.longitude,
          type: type
        };
        // 处理可能不同的 id 字段名 (id / pointId / stationId)
        if (selectedSite.stationId) {
          payload.stationId = selectedSite.stationId;
          payload.itemType = 2; // 如果是场站
        } else if (selectedSite.pointId) {
          payload.pointId = selectedSite.pointId;
        } else if (selectedSite.id) {
          payload.pointId = selectedSite.id;
        }
        await collectPoint(payload);
        setSelectedSite({ ...selectedSite, isCollect: type });
        if (pageType === 'start') {
          Taro.setStorageSync('startSite', { ...selectedSite, isCollect: type });
        }
        if (pageType === 'end') {
          Taro.setStorageSync('endSite', { ...selectedSite, isCollect: type });
        }
        Taro.showToast({
          title: type === 2 ? '已取消收藏' : '收藏成功',
          icon: 'none'
        });
      } catch (error) {
        console.log('收藏操作失败', error);
      }
    };
  const selectMapSite = useSelectMapSite();
  return (
    <View className="home-container">
      <Map
        id="map"
        longitude={longitude}
        latitude={latitude}
        scale={mapScale}
        markers={markers.map(m => ({ ...m, title: '', name: '' }))}
        polyline={polyline}
        polygons={polygons}
        showLocation={!!longitude}
        onRegionChange={handleRegionChange}
        onMarkerTap={handleMarkerTap}
        style={{ width: '100%', height: '100%' }}
      >
        {/* 返回 */}
        <View onClick={toBack} className="select-city">
          <Image className="select-city-icon" src={backIcon} />
        </View>
        {/* 地图浮层入口 */}
        <View className="map-entry">
          <Image onClick={openService} className="map-entry-icon" src={personalCenter} />
          <View className="split-line"></View>
          <Image onClick={toCoupon} className="map-entry-icon" src={coupon} />
          <View className="split-line"></View>
          <Image onClick={toOrder} className="map-entry-icon" src={suggest} />
        </View>
        {nearbyLoading && isAndroid && (
          <Image className="waiting-radar-img" src={waitingRadarSrc} />
        )}
        {/* 指针 */}
        <View className="pointer-box">
          {nearbyLoading && !isAndroid && (
            <Image className="waiting-radar" src={waitingRadarSrc} />
          )}
          {((showToolTip && selectedSite.name) || outOfServiceRange) && (
            <View className="toolTip">
              {(outOfServiceRange || selectedSite.time) && (
                <View className="toolTip-bg" style={{ backgroundImage: `url(${tooltipBgSrc})` }}>
                  <Text className="toolTip-bg-text">{outOfServiceRange ? `不在运营区` : `限停${selectedSite.time}分钟`}</Text>
                </View>
              )}
              <View className="toolTip-txt">
                <View className="toolTip-txt-content">{outOfServiceRange ? `请拖动至${pageType == 'end' ? '下车' : '上车'}点` : selectedSite.name}</View>
              </View>
            </View>
          )}
          <Image
            className={`pointer-icon ${bounce ? 'bounce' : ''}`}
            src={pointer}
          />
        </View>
        <View className="container">
          <View className="container-inner">
            {/* 复位按钮 */}
            <View className="origin-box">
              <Image onClick={toOrigin} className="origin-icon" src={originIcon} />
            </View>
            <View catchMove onClick={(e) => e.stopPropagation()}>
              {/* 确认站点 */}<View className="content-box">
                {/* <View className="content-limit-bg"></View>
                  <View className="limit-tip">目的地附近道路有<Text className="emphasis-txt">通行限制</Text>,为您推荐适合的下车位置</View> */}
                {
                  true ?
                    <View className="content-title">{pageType === 'start' ? '确认上车站点' : '确认目的地'}</View>
                    :
                    <View className="content-title">
                      <View>19号大院</View>
                      <Image className="collect-icon" src={activeStar} />
                    </View>
                }
                {
                  pageType === 'start' && walkingDuration !== null && walkingDistance !== null &&
                  <View className="limit-tip">从当前位置出发步行{formatTripDurationFromSeconds(walkingDuration)}({formatTripDistance(walkingDistance)})上车</View>
                }
                {
                  (nearbySites.length > 0 && pageType === 'end') &&
                  <View className="limit-tip">点击其他选项或紫色原点,可更改{pageType === 'start' ? '上车' : '下车'}点</View>
                }
                <ScrollView className="site-list" scrollY>
                  {/* {
                      nearbySites.map(item => (
                        <View onClick={() => changeChildrenSite(item)} key={item.id} className={`site-name ${selectedSite.id == item.id && 'site-active'}`}>{item.name}</View>
                      ))
                    } */}
                  {
                    selectedSite.name ?
                      true ?
                        <View className='site-item site-active'>
                          <View>{selectedSite.name}</View>
                          {/* <Image className="collect-icon" src={activeStar} /> */}
                          <Image onClick={handleCollect} className='collect-icon' src={selectedSite.isCollect === 1 ? activeStar : noStar} />
                        </View>
                        :
                        [1, 2].map((item) => (
                          <View key={item} className={`site-item ${item == 1 && 'site-active'}`}>
                            <View>{selectedSite.name}</View>
                            <Image className="collect-icon" src={activeStar} />
                          </View>
                        ))
                      :
                      <View className="no-service">超出服务范围,请在运营区内选择</View>
                  }
                </ScrollView>
                <View onClick={confirmSite} className={`site-confirm ${selectedSite.name ? '' : 'no-confirm'}`} >{pageType === 'start' ? '确认上车站点' : '确认目的地站点'}</View>
              </View>
            </View>
          </View>
        </View>
      </Map>
      {/* 客服弹窗 */}
      <CustomerServicePopup
        show={showCustomerService}
        onClose={handleCloseCustomerService}
      />
      {/* 提示弹窗 */}
      <TipPopup
        show={showTipPopup}
        onClose={handleCloseTipPopup}
        tipType={tipType}
      />
    </View>
  )
    <SelectMapSiteView
      longitude={selectMapSite.longitude}
      latitude={selectMapSite.latitude}
      mapScale={selectMapSite.mapScale}
      markers={selectMapSite.markers}
      mapPolylines={selectMapSite.mapPolylines}
      polygons={selectMapSite.polygons}
      handleRegionChange={selectMapSite.handleRegionChange}
      handleMarkerTap={selectMapSite.handleMarkerTap}
      toBack={selectMapSite.toBack}
      backIcon={backIcon}
      openService={selectMapSite.openService}
      toCoupon={selectMapSite.toCoupon}
      toOrder={selectMapSite.toOrder}
      personalCenter={personalCenter}
      coupon={coupon}
      suggest={suggest}
      nearbyLoading={selectMapSite.nearbyLoading}
      isAndroid={selectMapSite.isAndroid}
      waitingRadarSrc={selectMapSite.waitingRadarSrc}
      showToolTip={selectMapSite.showToolTip}
      selectedSite={selectMapSite.selectedSite}
      selectionMode={selectMapSite.selectionMode}
      selectedStation={selectMapSite.selectedStation}
      stationSiteList={selectMapSite.stationSiteList}
      outOfServiceRange={selectMapSite.outOfServiceRange}
      tooltipBgSrc={selectMapSite.tooltipBgSrc}
      pageType={selectMapSite.pageType}
      bounce={selectMapSite.bounce}
      pointer={pointer}
      toOrigin={selectMapSite.toOrigin}
      originIcon={originIcon}
      walkingDuration={selectMapSite.walkingDuration}
      walkingDistance={selectMapSite.walkingDistance}
      nearbySites={selectMapSite.nearbySites}
      handleCollect={selectMapSite.handleCollect}
      changeStationSite={selectMapSite.changeStationSite}
      activeStar={activeStar}
      noStar={noStar}
      confirmSite={selectMapSite.confirmSite}
      showCustomerService={selectMapSite.showCustomerService}
      handleCloseCustomerService={selectMapSite.handleCloseCustomerService}
      showTipPopup={selectMapSite.showTipPopup}
      handleCloseTipPopup={selectMapSite.handleCloseTipPopup}
      tipType={selectMapSite.tipType}
    />
  );
}
src/robotaxi/pages/site-search/components/favorite-popup/index.tsx
@@ -69,6 +69,31 @@
    }
  };
  /**
   * 点击收藏项时根据类型分流:
   * itemType=2(场站) 取 children 第一个站点;children 为空则提示无有效站点。
   */
  const handleSelectItem = (item: any) => {
    const itemType = Number(item?.itemType);
    if (itemType === 2) {
      const children = Array.isArray(item?.children) ? item.children : [];
      if (children.length === 0) {
        Taro.showToast({
          title: '此场站无有效站点',
          icon: 'none',
        });
        return;
      }
      const firstSite = children[0] || {};
      onSelect({
        ...firstSite,
        stationId: item?.stationId || firstSite?.stationId,
      });
      return;
    }
    onSelect(item);
  };
  const onScroll = (e) => {
    const { scrollHeight, scrollTop } = e.detail;
    if (containerHeight > 0 && scrollHeight > containerHeight) {
@@ -148,7 +173,7 @@
          >
            <View className='favorite-list-inner'>
              {list.map((item, index) => (
                <View key={item.id || index} className='favorite-item' onClick={() => onSelect(item)}>
                <View key={item.id || index} className='favorite-item' onClick={() => handleSelectItem(item)}>
                  <View className='favorite-info'>
                    <View className='favorite-name'>{item.pointName}</View>
                    <View className='favorite-address'>{item.pointAddress}</View>
src/robotaxi/pages/site-search/components/site-list-item.tsx
@@ -2,6 +2,7 @@
import { View, Image } from '@tarojs/components';
import { useState, useEffect } from 'react';
import { collectPoint, getCollectPoints } from '../../../api/site-search';
import tUpIcon from '../../../assets/T-up.png';
type Props = {
  // 站点数据
@@ -20,6 +21,8 @@
  onSelect: (item: any) => void;
  // 高亮文本渲染器
  renderHighlightedText: (text: string, className: string) => any;
  // 是否展示场站子站点信息
  showStationChildren?: boolean;
};
// 站点列表单项
@@ -31,14 +34,35 @@
  noStarSrc,
  showDistance = true,
  onSelect,
  renderHighlightedText
  renderHighlightedText,
  showStationChildren = false
}: Props) {
  const MAX_COLLECT_COUNT = 50;
  const MAX_VISIBLE_STATION_CHILDREN = 4; // 场站模式默认最多展示子站点数量
  const [isCollected, setIsCollected] = useState(!!item.isCollected);
  const [expandStationChildren, setExpandStationChildren] = useState(false); // 是否展开全部场站子站点
  useEffect(() => {
    setIsCollected(item.isCollect === 1);
  }, [item.isCollect]);
  useEffect(() => {
    setExpandStationChildren(false);
  }, [item?.id, item?.pointId, item?.stationId]);
  const isStationItem = Number(item?.itemType) === 2; // 当前条目是否场站
  const stationChildren = Array.isArray(item?.children) ? item.children : []; // 场站子站点列表
  const shouldShowStationChildren = showStationChildren && isStationItem && stationChildren.length > 0; // 是否渲染场站子站点盒子
  const visibleStationChildren = expandStationChildren
    ? stationChildren
    : stationChildren.slice(0, MAX_VISIBLE_STATION_CHILDREN);
  /**
   * 获取子站点展示名称。
   */
  const getStationChildName = (child: any) => {
    return child?.name || child?.pointName || '--';
  };
  const checkCollectLimit = async () => {
    // 只取总数即可,避免为校验拉完整列表
@@ -116,12 +140,32 @@
          </View>
          : null}
      </View>
      <View className='site-gate'>
        <View>东门</View>
        <View>北门</View>
        <View>西门</View>
        <View>南门</View>
      </View>
      {shouldShowStationChildren && (
        <View className='site-gate'>
          {visibleStationChildren.map((child: any, index: number) => {
            const isMoreSlot = !expandStationChildren
              && stationChildren.length > MAX_VISIBLE_STATION_CHILDREN
              && index === MAX_VISIBLE_STATION_CHILDREN - 1;
            if (isMoreSlot) {
              return (
                <View
                  key='station-more'
                  className='site-gate-item-more'
                  onClick={() => setExpandStationChildren(true)}
                >
                  <View>更多</View>
                  <Image className='site-gate-more-icon' src={tUpIcon} />
                </View>
              );
            }
            return (
              <View key={child?.id || child?.pointId || `${child?.latitude}_${child?.longitude}_${index}`}>
                {getStationChildName(child)}
              </View>
            );
          })}
        </View>
      )}
    </View>
  );
}
src/robotaxi/pages/site-search/index.less
@@ -197,6 +197,18 @@
      letter-spacing: normal;
      color: rgba(0, 0, 0, 0.9);
    }
    .site-gate-item-more {
      display: flex;
      align-items: center;
      justify-content: center;
      column-gap: 8px;
    }
    .site-gate-more-icon {
      width: 16px;
      height: 16px;
    }
  }
  .show-more {
@@ -336,4 +348,4 @@
    width: 480px;
    height: 400px;
  }
}
}
src/robotaxi/pages/site-search/index.tsx
@@ -232,7 +232,12 @@
    );
  }
  const renderSiteItems = (list: any[], onSelect: (item: any) => void, showDistance = true) => { // 复用列表项渲染
  const renderSiteItems = (
    list: any[],
    onSelect: (item: any) => void,
    showDistance = true,
    showStationChildren = false
  ) => { // 复用列表项渲染
    return list.map((item, index) => (
      <SiteListItem
        key={item.id || index}
@@ -244,6 +249,7 @@
        showDistance={showDistance}
        onSelect={onSelect}
        renderHighlightedText={renderHighlightedText}
        showStationChildren={showStationChildren}
      />
    ));
  };
@@ -304,7 +310,7 @@
          <View className='card-header'>历史记录</View>
          <View className='card-content list-content' id='history-card-content'>
            {
              renderSiteItems(visibleHistory, (item) => onChangeSelectSite(item), true)
              renderSiteItems(visibleHistory, (item) => onChangeSelectSite(item), true, true)
            }
          </View>
          {historySite.length > MAX_SHOW_COUNT && (
@@ -322,7 +328,7 @@
          <View className='card-header'>{searchValue ? '推荐站点' : '附近站点'}</View>
          <View className='card-content list-content' id='recommend-card-content'>
            {
              renderSiteItems(visibleRecommend, (item) => onChangeSelectSite(item), true)
              renderSiteItems(visibleRecommend, (item) => onChangeSelectSite(item), true, true)
            }
          </View>
          {recommendList.length > MAX_SHOW_COUNT && (