hejianhao
2026-06-12 dbe64928d2d48467d35452afc9fcad69d87f05fa
feat(robotaxi): 优化选点校验与优惠券传递逻辑,修复bug与UI态异常展示
14个文件已修改
388 ■■■■■ 已修改文件
src/pages/robotaxi/home/hooks/use-region-change.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/hooks/use-site-state.ts 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/api/types/begin-route.ts 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car-modules/door-trunk-control.tsx 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car-modules/trip-action-bar.tsx 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/css-folder/calling-car.less 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/modify-destination-review.tsx 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-order-payment.ts 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-order-submit.ts 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-vehicle-tracking.ts 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/select-map-site/components/select-map-site-view.tsx 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/select-map-site/hooks/use-select-map-site-state.ts 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/select-map-site/hooks/use-select-map-site.ts 148 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/select-map-site/index.tsx 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/hooks/use-region-change.ts
@@ -59,6 +59,7 @@
    try {
      const { isInside, newMarkers, sites } = await mapState.updateMapData(lat, lng, undefined, { commitMarkers: false });
      if (!isInside) {
        siteState.clearCachedStartSiteSelection();
        commitQueriedMarkers(newMarkers, false, false);
        settleRegionResult({ lat, lon: lng, isInside: false });
        siteState.setBounce(false);
@@ -124,6 +125,7 @@
        try {
          const { isInside, newMarkers, sites } = await mapState.updateMapData(lat, lng, undefined, { commitMarkers: false });
          if (!isInside) {
            siteState.clearCachedStartSiteSelection();
            commitQueriedMarkers(newMarkers, false, false);
            settleRegionResult({ lat, lon: lng, isInside: false });
            siteState.setBounce(false);
src/pages/robotaxi/home/hooks/use-site-state.ts
@@ -5,6 +5,7 @@
  RECOMMENDED_CONFIRM_RIDE_SOURCE_TYPE,
  writeConfirmRideSiteSourceType
} from '../../../../robotaxi/constants/confirm-ride-report';
import { clearHomeStartSiteDisplayState } from '../../../../robotaxi/hooks/use-home-start-site-display';
/**
 * 站点与吸附状态管理:统一吸附判断与逆地理地址回填。
@@ -16,6 +17,14 @@
  const [showToolTip, setShowToolTip] = useState(true);
  const [bounce, setBounce] = useState(false);
  const [adsorptionDistance, setAdsorptionDistance] = useState<number | null>(null);
  /**
   * 清理首页已缓存的上车点选择,避免拖图未吸附时继续复用旧站点。
   */
  const clearCachedStartSiteSelection = () => {
    Taro.removeStorageSync('startSite');
    clearHomeStartSiteDisplayState();
  };
  /**
   * 统一吸附判断,命中阈值则更新吸附站点并显示提示。
@@ -39,6 +48,7 @@
   */
  const resolveAddress = async (lat: number, lng: number) => {
    setIsAdsorbed(false);
    clearCachedStartSiteSelection();
    setCurrentSite({ name: '正在获取位置...' });
    try {
      const data = await getAddressByLocation(lng, lat);
@@ -68,6 +78,7 @@
    setShowToolTip,
    setBounce,
    setAdsorptionDistance,
    clearCachedStartSiteSelection,
    tryAdsorb,
    resolveAddress,
  };
src/robotaxi/api/types/begin-route.ts
@@ -60,6 +60,10 @@
    finalFare?: NumericLike;
    /** 原价金额。 */
    originalFare?: NumericLike;
    /** 线路报价返回的优惠券 ID。 */
    couponId?: OrderId | '';
    /** 兼容后端返回的用户优惠券 ID。 */
    userCouponId?: OrderId | '';
}
/** 单条路线选项。 */
src/robotaxi/pages/begin-route/components/calling-car-modules/door-trunk-control.tsx
@@ -222,7 +222,7 @@
            <View className="car-card-arrive">
                <View className={`car-card-arrive-item ${trunkStatus && 'close-btn-item'}`} onClick={() => handleUnlock('trunk', () => setTrunkStatus(!trunkStatus))}>
                    <Image className="car-card-arrive-item-img" src={trunkStatus ? 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/trunk-open.png' : 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/trunk-close.png'} />
                    <View className="car-card-arrive-item-text">{trunkStatus ? '关闭后备箱' : '打开后备箱'}</View>
                    <View className="car-card-arrive-item-text">{trunkStatus ? '后备箱' : '后备箱'}</View>
                </View>
                <View className={`car-card-arrive-item ${lockStatus && 'close-btn-item'}`} onClick={() => handleUnlock('door')}>
                    <Image className="car-card-arrive-item-img" src={lockStatus ? LOCK_OPEN_ICON_URL : LOCK_CLOSE_ICON_URL} />
@@ -240,7 +240,7 @@
                <View className="car-card-reach-btn">
                    <View className={`car-card-reach-btn-item ${trunkStatus && 'close-btn-item'}`} onClick={() => handleUnlock('trunk', () => setTrunkStatus(!trunkStatus))}>
                        <Image className="car-card-reach-btn-item-img" src={trunkStatus ? 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/trunk-open.png' : 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/trunk-close.png'} />
                        <View className="car-card-reach-btn-item-text">{trunkStatus ? '关闭后备箱' : '打开后备箱'}</View>
                        <View className="car-card-reach-btn-item-text">{trunkStatus ? '后备箱' : '后备箱'}</View>
                    </View>
                    <View className="car-card-reach-btn-item" onClick={handleOpenOverRoutePopupWithDebounce}>
                        <Image className="car-card-reach-btn-item-img" src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/over-gps-icon.png" />
src/robotaxi/pages/begin-route/components/calling-car-modules/trip-action-bar.tsx
@@ -16,6 +16,7 @@
    handleShareTrip,
    showModifyDestination = true
}: Props) {
    const canShowTripAction = [TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup, TripStatus.InService].includes(tripStatus);
    // 按行程状态控制按钮集合,避免在 JSX 中堆叠过多条件判断
    const actionList = [
        {
@@ -29,14 +30,15 @@
            key: "destination",
            label: "修改目的地",
            icon: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/change-destination.png',
            visible: [TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup, TripStatus.InService].includes(tripStatus) && showModifyDestination,
            visible: canShowTripAction,
            disabled: !showModifyDestination,
            onClick: handleModifyDestination
        },
        {
            key: "share",
            label: "分享行程",
            icon: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/share-trip.png',
            visible: [TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup, TripStatus.InService].includes(tripStatus),
            visible: canShowTripAction,
            onClick: handleShareTrip
        }
    ].filter(item => item.visible);
@@ -62,9 +64,12 @@
                ) : (
                    <View
                        key={item.key}
                        className="trip-action-bar-item"
                        hoverClass="trip-action-bar-item-hover"
                        onClick={() => item.onClick?.()}
                        className={`trip-action-bar-item${item.disabled ? ' trip-action-bar-item-disabled' : ''}`}
                        hoverClass={item.disabled ? 'none' : 'trip-action-bar-item-hover'}
                        onClick={() => {
                            if (item.disabled) return;
                            item.onClick?.();
                        }}
                    >
                        <Image className="trip-action-bar-item-icon" src={item.icon} />
                        <Text className="trip-action-bar-item-text">{item.label}</Text>
src/robotaxi/pages/begin-route/components/css-folder/calling-car.less
@@ -222,6 +222,10 @@
        justify-content: center;
        box-sizing: border-box;
        &-disabled {
          opacity: 0.4;
        }
        &-icon {
          flex-shrink: 0;
          width: 48px;
src/robotaxi/pages/begin-route/components/modify-destination-review.tsx
@@ -25,26 +25,6 @@
const ROUTE_PANEL_ESTIMATE_HEIGHT_PX = 230; // 首次展开未拿到真实高度时的路线面板预估高度
const MODIFY_DESTINATION_UNLIMITED_COUNT = -1; // 后端返回 -1 代表修改次数不限
const MODIFY_DESTINATION_DISABLED_COUNT = 0; // 后端返回 0 代表没有修改机会
const CHINESE_NUMBER_DIGITS = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九']; // 中文数字映射
/**
 * 将正整数转为中文数字展示。
 */
const formatChineseCount = (count: number) => {
  if (!Number.isInteger(count) || count < 0) {
    return String(count);
  }
  if (count < 10) {
    return CHINESE_NUMBER_DIGITS[count];
  }
  if (count < 100) {
    const tensDigit = Math.floor(count / 10);
    const unitsDigit = count % 10;
    const tensText = tensDigit === 1 ? '十' : `${CHINESE_NUMBER_DIGITS[tensDigit]}十`;
    return unitsDigit === 0 ? tensText : `${tensText}${CHINESE_NUMBER_DIGITS[unitsDigit]}`;
  }
  return String(count);
};
/**
 * 生成修改目的地次数提示文案。
@@ -57,10 +37,10 @@
    return '';
  }
  if (remainingCount === MODIFY_DESTINATION_DISABLED_COUNT) {
    return '暂无修改机会';
    return '剩余修改次数:0次';
  }
  if (typeof remainingCount === 'number' && remainingCount > MODIFY_DESTINATION_DISABLED_COUNT) {
    return `仅可修改${formatChineseCount(remainingCount)}次`;
    return `剩余修改次数:${remainingCount}次`;
  }
  return '修改次数获取失败';
};
@@ -343,10 +323,8 @@
                      ? '次数加载中...'
                      : !isModifyCountReady
                        ? '次数获取失败'
                      : isRouteDataIncomplete
                        ? '路线加载中...'
                        : isModifyCountDisabled
                          ? '暂无修改机会'
                        : isRouteDataIncomplete
                          ? '路线加载中...'
                          : '确认修改'}
                </View>
                {isConfirmDisabled && <View className='modify-destination-review-confirm-mask' />}
src/robotaxi/pages/begin-route/hooks/use-order-payment.ts
@@ -185,34 +185,34 @@
    }
    paySubmittingRef.current = true;
    try {
      await getOrderPayment({ orderId: targetOrderId });
      syncPaymentSuccessStatus();
      // await getOrderPayment({ orderId: targetOrderId });
      // syncPaymentSuccessStatus();
      // const paymentResult = await getOrderPayment({ orderId: targetOrderId });
      // const paymentStatus = Number(paymentResult?.status);
      const paymentResult = await getOrderPayment({ orderId: targetOrderId });
      const paymentStatus = Number(paymentResult?.status);
      // if (paymentStatus === RobotaxiPaymentEntryStatus.Unsigned) {
      //   const cashierToken = paymentResult?.value?.token;
      //   if (!cashierToken) {
      //     Taro.showToast({ title: '支付凭证缺失,请稍后重试', icon: 'none' });
      //     return;
      //   }
      //   await deps.handleUnsignedPayment(cashierToken);
      //   return;
      // }
      if (paymentStatus === RobotaxiPaymentEntryStatus.Unsigned) {
        const cashierToken = paymentResult?.value?.token;
        if (!cashierToken) {
          Taro.showToast({ title: '支付凭证缺失,请稍后重试', icon: 'none' });
          return;
        }
        await deps.handleUnsignedPayment(cashierToken);
        return;
      }
      // if (paymentStatus === RobotaxiPaymentEntryStatus.Signed) {
      //   const signedPaymentValue = paymentResult?.value as SignedOrderPaymentValue;
      //   if (!signedPaymentValue?.packageValue) {
      //     Taro.showToast({ title: '支付参数缺失,请稍后重试', icon: 'none' });
      //     return;
      //   }
      //   await deps.handleSignedCreditPayment(signedPaymentValue);
      //   return;
      // }
      if (paymentStatus === RobotaxiPaymentEntryStatus.Signed) {
        const signedPaymentValue = paymentResult?.value as SignedOrderPaymentValue;
        if (!signedPaymentValue?.packageValue) {
          Taro.showToast({ title: '支付参数缺失,请稍后重试', icon: 'none' });
          return;
        }
        await deps.handleSignedCreditPayment(signedPaymentValue);
        return;
      }
      // Taro.showToast({ title: '暂不支持当前支付方式', icon: 'none' });
      Taro.showToast({ title: '暂不支持当前支付方式', icon: 'none' });
    } catch (error) {
      Taro.showToast({ title: '支付失败,请重试', icon: 'none' });
src/robotaxi/pages/begin-route/hooks/use-order-submit.ts
@@ -148,13 +148,31 @@
  };
  /**
   * 解析下单时应透传的有效优惠券 ID。
   */
  const resolveSubmitCouponId = (selectedRoute: any) => {
    const routeFareEstimate = selectedRoute?.fareEstimate;
    return String(
      routeFareEstimate?.couponId
      || routeFareEstimate?.userCouponId
      || deps.fareEstimate?.couponId
      || deps.fareEstimate?.userCouponId
      || Taro.getStorageSync('selectedCouponId')
      || ''
    ).trim();
  };
  /**
   * 提交订单。
   */
  const submitOrder = async (startSite: any, openCityId: number): Promise<SubmitOrderResult | null> => {
    const waitingTimeMinutes = Number(deps.fareEstimate?.waitingTime || 0);
    const estimatedTimestamp = Date.now() + waitingTimeMinutes * MILLISECONDS_PER_MINUTE;
    const estimatedTime = formatDateTime(new Date(estimatedTimestamp));
    const cachedCouponId = Taro.getStorageSync('selectedCouponId') || '';
    const selectedRoute = Array.isArray(deps.routeOptions) && typeof deps.selectedRouteIndex === 'number'
      ? deps.routeOptions[deps.selectedRouteIndex]
      : null;
    const resolvedCouponId = resolveSubmitCouponId(selectedRoute);
    const payload: any = {
      endPointId: deps.endSite.id,
      openCityId,
@@ -166,16 +184,13 @@
      estimatedTime,
    };
    if (deps.routeOptions && typeof deps.selectedRouteIndex === 'number') {
      const selectedRoute = deps.routeOptions[deps.selectedRouteIndex];
      if (selectedRoute?.route?.routeID) {
        payload.selectedRouteId = selectedRoute.route.routeID;
        payload.navigationSnapshotId = selectedRoute.snapshotId;
      }
    if (selectedRoute?.route?.routeID) {
      payload.selectedRouteId = selectedRoute.route.routeID;
      payload.navigationSnapshotId = selectedRoute.snapshotId;
    }
    if (cachedCouponId) {
      payload.userCouponId = cachedCouponId;
    if (resolvedCouponId) {
      payload.userCouponId = resolvedCouponId;
    }
    const response = await saveOrder(payload);
@@ -183,7 +198,7 @@
    reportEvent(ORDER_SUBMIT_EVENT_ID, {
      total_amount: deps.fareEstimate.originalFare,
      payable_amount: deps.fareEstimate.finalFare,
      coupon_id: cachedCouponId,
      coupon_id: resolvedCouponId,
      status_code: statusCode
    });
src/robotaxi/pages/begin-route/hooks/use-vehicle-tracking.ts
@@ -68,6 +68,8 @@
  } | null>(null);
  const vehicleMarkerReadyRef = useRef(false);
  const vehicleMarkerReadyTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const vehicleMarkerIdRef = useRef<number>(vehicleMarkerId);
  const translateSessionIdRef = useRef(0);
  /**
   * 清理车辆 marker ready 延时器,避免旧定时器污染新一轮平移态。
@@ -77,6 +79,18 @@
      clearTimeout(vehicleMarkerReadyTimerRef.current);
      vehicleMarkerReadyTimerRef.current = null;
    }
  };
  /**
   * 重置当前车辆 marker 对应的平移上下文,避免沿用旧 marker 的角度与 ready 状态。
   */
  const resetTranslateRuntimeState = () => {
    isTranslatingRef.current = false;
    pendingTranslateRef.current = null;
    lastAppliedTranslateTaskRef.current = null;
    vehicleMarkerReadyRef.current = false;
    clearVehicleMarkerReadyTimer();
    translateSessionIdRef.current += 1;
  };
  /**
@@ -100,12 +114,14 @@
    if (isTranslatingRef.current) return;
    if (!pendingTranslateRef.current) return;
    const task = pendingTranslateRef.current;
    const translateSessionId = translateSessionIdRef.current;
    pendingTranslateRef.current = null;
    isTranslatingRef.current = true;
    let finished = false;
    let fallbackTimer: any = null;
    const finishTranslate = () => {
      if (finished) return;
      if (translateSessionId !== translateSessionIdRef.current) return;
      finished = true;
      if (fallbackTimer) clearTimeout(fallbackTimer);
      isTranslatingRef.current = false;
@@ -132,6 +148,9 @@
        rotate: task.rotate,
        duration: task.duration,
        animationEnd: () => {
          if (translateSessionId !== translateSessionIdRef.current) {
            return;
          }
          lastAppliedTranslateTaskRef.current = {
            latitude: task.latitude,
            longitude: task.longitude,
@@ -140,6 +159,9 @@
          finishTranslate();
        },
        fail: () => {
          if (translateSessionId !== translateSessionIdRef.current) {
            return;
          }
          lastAppliedTranslateTaskRef.current = {
            latitude: task.latitude,
            longitude: task.longitude,
@@ -187,12 +209,20 @@
  }, [statusRule.translateCar, vehicleLocation, vehicleDirection, vehicleMarkerId]);
  useEffect(() => {
    if (!statusRule.translateCar) {
      vehicleMarkerIdRef.current = vehicleMarkerId;
      return;
    }
    if (vehicleMarkerIdRef.current === vehicleMarkerId) {
      return;
    }
    vehicleMarkerIdRef.current = vehicleMarkerId;
    resetTranslateRuntimeState();
  }, [statusRule.translateCar, vehicleMarkerId]);
  useEffect(() => {
    if (statusRule.translateCar) return;
    isTranslatingRef.current = false;
    pendingTranslateRef.current = null;
    lastAppliedTranslateTaskRef.current = null;
    vehicleMarkerReadyRef.current = false;
    clearVehicleMarkerReadyTimer();
    resetTranslateRuntimeState();
  }, [statusRule.translateCar]);
  useEffect(() => () => {
src/robotaxi/pages/select-map-site/components/select-map-site-view.tsx
@@ -2,6 +2,7 @@
import CustomerServicePopup from '../../../components/customer-service';
import TipPopup from '../../../components/tip-popup';
import { formatTripDistance, formatTripDurationFromSeconds } from '../../../../utils/Robotaxi-public-method';
import { SELECT_SITE_RANGE_STATUS, type SelectSiteRangeStatus } from '../hooks/use-select-map-site-state';
type Deps = {
  longitude: number | null;
@@ -27,7 +28,7 @@
  selectionMode: 'point' | 'station';
  selectedStation: any;
  stationSiteList: any[];
  outOfServiceRange: boolean;
  rangeStatus: SelectSiteRangeStatus;
  tooltipBgSrc: string;
  pageType: string;
  bounce: boolean;
@@ -54,6 +55,12 @@
 * 选点页视图组件:仅负责渲染地图与页面 UI,不包含业务状态编排。
 */
export default function SelectMapSiteView(deps: Deps) {
  const isOutOfOperatingArea = deps.rangeStatus === SELECT_SITE_RANGE_STATUS.outOfOperatingArea;
  const isCrossOperatingArea = deps.rangeStatus === SELECT_SITE_RANGE_STATUS.crossOperatingArea;
  const showRangeWarning = isOutOfOperatingArea || isCrossOperatingArea;
  const shouldShowSelectedSiteEcho = !showRangeWarning && Boolean(deps.selectedSite.name);
  const shouldShowStationSelection = deps.selectionMode === 'station' && !showRangeWarning;
  return (
    <View className="home-container">
      <Map
@@ -89,19 +96,23 @@
          {deps.nearbyLoading && !deps.isAndroid && (
            <Image className="waiting-radar" src={deps.waitingRadarSrc} />
          )}
          {((deps.showToolTip && deps.selectedSite.name) || deps.outOfServiceRange) && (
          {((deps.showToolTip && shouldShowSelectedSiteEcho) || showRangeWarning) && (
            <View className="toolTip">
              {(deps.outOfServiceRange || deps.selectedSite.time) && (
              {(showRangeWarning || deps.selectedSite.time) && (
                <View className="toolTip-bg">
                  <Image className="toolTip-bg-img" src={deps.tooltipBgSrc} />
                  <Text className="toolTip-bg-text">
                    {deps.outOfServiceRange ? '不在运营区' : `限停${deps.selectedSite.time}分钟`}
                    {isOutOfOperatingArea ? '不在运营区' : isCrossOperatingArea ? '不在同一运营区' : `限停${deps.selectedSite.time}分钟`}
                  </Text>
                </View>
              )}
              <View className="toolTip-txt">
                <View className="toolTip-txt-content">
                  {deps.outOfServiceRange ? `请拖动至${deps.pageType == 'end' ? '目的地' : '上车点'}` : deps.selectedSite.name}
                  {isOutOfOperatingArea
                    ? `请拖动至${deps.pageType == 'end' ? '目的地' : '上车点'}`
                    : isCrossOperatingArea
                      ? '请在同一运营区内选择'
                      : deps.selectedSite.name}
                </View>
              </View>
            </View>
@@ -122,7 +133,7 @@
              <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>
                    <View className="content-title">{shouldShowStationSelection ? 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)})上车
@@ -132,7 +143,7 @@
                      <View className="limit-tip">点击紫色原点或拖动大头针,可修改{deps.pageType === 'start' ? '上车点' : '目的地'}</View>
                    )}
                  </View>
                  {deps.selectionMode === 'station' && (
                  {shouldShowStationSelection && (
                    <Image
                      onClick={deps.handleCollect}
                      className='collect-icon'
@@ -148,7 +159,7 @@
                    showScrollbar={true}
                  >
                    <View className='site-list-inner'>
                      {deps.selectionMode === 'station' ? (
                      {shouldShowStationSelection ? (
                        deps.stationSiteList.length > 0 ? deps.stationSiteList.map((item) => (
                          <View
                            key={item.id || item.pointId || `${item.latitude}_${item.longitude}`}
@@ -158,15 +169,15 @@
                            <View>{item.name}</View>
                          </View>
                        )) : <View className="no-service">当前场站暂无可选站点</View>
                      ) : deps.selectedSite.name ? (
                      ) : shouldShowSelectedSiteEcho ? (
                        <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>
                      ) : deps.outOfServiceRange ? (
                        <View className="no-service">超出服务范围,请在运营区内选择</View>
                      ) : showRangeWarning ? (
                        <View className="no-service">{isCrossOperatingArea ? '请在同一运营区内选择' : '超出服务范围,请在运营区内选择'}</View>
                      ) : <View className="no-service">附近无可选站点</View>
                      }
                    </View>
src/robotaxi/pages/select-map-site/hooks/use-select-map-site-state.ts
@@ -1,6 +1,14 @@
import Taro from "@tarojs/taro";
import { useMemo, useRef, useState } from 'react';
export const SELECT_SITE_RANGE_STATUS = {
  normal: 'normal',
  outOfOperatingArea: 'out_of_operating_area',
  crossOperatingArea: 'cross_operating_area'
} as const;
export type SelectSiteRangeStatus = typeof SELECT_SITE_RANGE_STATUS[keyof typeof SELECT_SITE_RANGE_STATUS];
/**
 * 选点页状态容器:仅维护页面状态、refs 与轻量派生数据,不承载业务副作用。
 */
@@ -29,7 +37,7 @@
  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 [rangeStatus, setRangeStatus] = useState<SelectSiteRangeStatus>(SELECT_SITE_RANGE_STATUS.normal); // 当前选点范围状态
  const [walkingDistance, setWalkingDistance] = useState<number | null>(null); // 步行距离
  const [walkingDuration, setWalkingDuration] = useState<number | null>(null); // 步行时长
  const tooltipBgUrl = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/tooltip-bg.png'; // 提示背景图资源
@@ -94,8 +102,8 @@
    setUserLongitude,
    userLatitude,
    setUserLatitude,
    outOfServiceRange,
    setOutOfServiceRange,
    rangeStatus,
    setRangeStatus,
    walkingDistance,
    setWalkingDistance,
    walkingDuration,
src/robotaxi/pages/select-map-site/hooks/use-select-map-site.ts
@@ -18,6 +18,7 @@
import { checkOtp } from '../../../../utils/tools';
import { jumpTo } from '../../../../utils/jump';
import useSelectMapSiteState from './use-select-map-site-state';
import { SELECT_SITE_RANGE_STATUS } from './use-select-map-site-state';
import useSelectMapSiteWalkingRoute from './use-select-map-site-walking-route';
import {
  MODIFY_DESTINATION_CANDIDATE_KEY,
@@ -137,15 +138,110 @@
  };
  /**
   * 获取当前页面用于约束同运营区选择的对端站点电子围栏 ID。
   */
  const getReferenceElectronicAreaId = () => {
    const cachedSite = state.pageType === 'start'
      ? Taro.getStorageSync('endSite')
      : Taro.getStorageSync('startSite');
    return String(cachedSite?.electronicAreaId || '');
  };
  /**
   * 判断目标站点是否属于当前允许的同运营区范围。
   */
  const isSelectableInSameOperatingArea = (site: any) => {
    const referenceElectronicAreaId = getReferenceElectronicAreaId();
    if (!referenceElectronicAreaId) {
      return true;
    }
    const siteElectronicAreaId = String(site?.electronicAreaId || '');
    if (!siteElectronicAreaId) {
      return false;
    }
    return referenceElectronicAreaId === siteElectronicAreaId;
  };
  /**
   * 过滤当前允许参与吸附和选择的同运营区候选站点。
   */
  const filterSelectableSites = (sites: any[]) => {
    if (!Array.isArray(sites)) {
      return [];
    }
    return sites.filter((site) => isSelectableInSameOperatingArea(site));
  };
  /**
   * 从 marker 中解析对应的业务站点对象,优先使用完整的站点数据。
   */
  const findSiteByMarker = (marker: any, candidateSites?: any[]) => {
    const sourceSites = Array.isArray(candidateSites) ? candidateSites : state.nearbySites;
    return sourceSites.find((item: any) => {
      const markerPointId = String(marker?.pointId || marker?.id || '');
      const sitePointId = String(item?.pointId || item?.id || '');
      if (markerPointId && sitePointId && markerPointId === sitePointId) {
        return true;
      }
      return Number(item?.latitude) === Number(marker?.latitude) && Number(item?.longitude) === Number(marker?.longitude);
    });
  };
  /**
   * 根据允许选择的站点列表,构造可参与吸附的 marker 集合。
   */
  const getSelectableMarkers = (markers: any[], candidateSites: any[]) => {
    const selectableSites = filterSelectableSites(candidateSites);
    if (selectableSites.length === 0) {
      return [];
    }
    const selectablePointIds = new Set(
      selectableSites.map((item: any) => String(item?.pointId || item?.id || '')).filter(Boolean)
    );
    return markers.filter((marker: any) => {
      const markerPointId = String(marker?.pointId || marker?.id || '');
      if (markerPointId && selectablePointIds.has(markerPointId)) {
        return true;
      }
      return selectableSites.some((site: any) => (
        Number(site?.latitude) === Number(marker?.latitude)
        && Number(site?.longitude) === Number(marker?.longitude)
      ));
    });
  };
  /**
   * 判断当前中心点附近是否存在跨运营区但距离满足吸附阈值的候选站点。
   */
  const hasCrossOperatingAreaCandidateNearby = (lat: number, lng: number, markers: any[], candidateSites: any[]) => {
    if (!getReferenceElectronicAreaId()) {
      return false;
    }
    return markers.some((marker: any) => {
      const site = findSiteByMarker(marker, candidateSites) || marker;
      if (isSelectableInSameOperatingArea(site)) {
        return false;
      }
      return getDistance(lat, lng, marker.latitude, marker.longitude) < state.distanceConfig.adsorptionDistance;
    });
  };
  /**
   * 计算是否超出可选范围(运营区外或起终点电子围栏不一致)。
   */
  const getOutOfServiceRange = (inside: boolean, site: any) => {
  const getRangeStatus = (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;
    if (!inside) {
      return SELECT_SITE_RANGE_STATUS.outOfOperatingArea;
    }
    if (isCrossArea) {
      return SELECT_SITE_RANGE_STATUS.crossOperatingArea;
    }
    return SELECT_SITE_RANGE_STATUS.normal;
  };
  /**
@@ -164,7 +260,7 @@
      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));
        const nextStationSites = filterSelectableSites(sites).filter((item: any) => String(item?.stationId || '') === String(state.selectedStation.stationId));
        state.setStationSiteList(nextStationSites);
      }
      
@@ -199,7 +295,7 @@
      state.setLongitude(res.longitude);
      syncPinLocation(res.latitude, res.longitude);
      updateMapData(res.latitude, res.longitude).then(({ isInside }) => {
        state.setOutOfServiceRange(getOutOfServiceRange(isInside, state.selectedSite));
        state.setRangeStatus(getRangeStatus(isInside, state.selectedSite));
      });
    });
  };
@@ -255,14 +351,15 @@
        success: async (res) => {
          const { latitude: lat, longitude: lng } = res;
          const { newMarkers, sites, isInside } = await updateMapData(lat, lng);
          const { targetMarker, minDistance } = findNearestMarker(lat, lng, newMarkers);
          const selectableMarkers = getSelectableMarkers(newMarkers, sites);
          const { targetMarker, minDistance } = findNearestMarker(lat, lng, selectableMarkers);
          if (targetMarker && minDistance < state.distanceConfig.adsorptionDistance) {
            mapCtx.moveToLocation({
              latitude: targetMarker.latitude,
              longitude: targetMarker.longitude,
            });
            const activeSite = applySelectionBySite(targetMarker, sites);
            state.setOutOfServiceRange(getOutOfServiceRange(isInside, activeSite));
            const activeSite = applySelectionBySite(findSiteByMarker(targetMarker, sites) || targetMarker, filterSelectableSites(sites));
            state.setRangeStatus(getRangeStatus(isInside, activeSite));
            state.setLatitude(activeSite?.latitude || targetMarker.latitude);
            state.setLongitude(activeSite?.longitude || targetMarker.longitude);
            syncPinLocation(activeSite?.latitude || targetMarker.latitude, activeSite?.longitude || targetMarker.longitude);
@@ -270,7 +367,11 @@
          } else {
            resetStationSelection();
            state.setSelectedSite({});
            state.setOutOfServiceRange(getOutOfServiceRange(isInside, {}));
            state.setRangeStatus(
              isInside && hasCrossOperatingAreaCandidateNearby(lat, lng, newMarkers, sites)
                ? SELECT_SITE_RANGE_STATUS.crossOperatingArea
                : getRangeStatus(isInside, {})
            );
            // 只有当坐标发生实质性变化才去更新 React 状态中的经纬度,避免频繁反推给原生组件导致抖动
            if (Math.abs(state.latitude - lat) > 0.0001 || Math.abs(state.longitude - lng) > 0.0001) {
              state.setLatitude(lat);
@@ -296,6 +397,7 @@
    if (markerId === undefined) return;
    const targetMarker = state.markers.find((m) => m.id === markerId);
    if (!targetMarker) return;
    const targetSite = findSiteByMarker(targetMarker, state.nearbySites) || targetMarker;
    state.pickupConfirmTypeRef.current = 'click';
    state.setShowToolTip(false);
    state.setBounce(true);
@@ -303,8 +405,16 @@
    state.setLongitude(targetMarker.longitude);
    setTimeout(() => {
      state.setBounce(false);
      const activeSite = applySelectionBySite(targetMarker, state.nearbySites);
      state.setOutOfServiceRange(getOutOfServiceRange(state.isOperatingArea, activeSite));
      if (!isSelectableInSameOperatingArea(targetSite)) {
        resetStationSelection();
        state.setSelectedSite({});
        state.setRangeStatus(SELECT_SITE_RANGE_STATUS.crossOperatingArea);
        syncPinLocation(targetMarker.latitude, targetMarker.longitude);
        state.setShowToolTip(true);
        return;
      }
      const activeSite = applySelectionBySite(targetSite, filterSelectableSites(state.nearbySites));
      state.setRangeStatus(getRangeStatus(state.isOperatingArea, activeSite));
      state.setLatitude(activeSite?.latitude || targetMarker.latitude);
      state.setLongitude(activeSite?.longitude || targetMarker.longitude);
      syncPinLocation(activeSite?.latitude || targetMarker.latitude, activeSite?.longitude || targetMarker.longitude);
@@ -370,10 +480,14 @@
      Taro.showToast({ title: '请选择站点', icon: 'none' });
      return;
    }
    if (state.rangeStatus === SELECT_SITE_RANGE_STATUS.crossOperatingArea) {
      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' });
        Taro.showToast({ title: '请在同一运营区内选择', icon: 'none' });
        return;
      }
      if (state.userLatitude && state.userLongitude) {
@@ -403,7 +517,7 @@
    }
    const startSiteCache = Taro.getStorageSync('startSite');
    if (startSiteCache?.electronicAreaId && state.selectedSite?.electronicAreaId && startSiteCache.electronicAreaId !== state.selectedSite.electronicAreaId) {
      Taro.showToast({ title: '起终点需在同一运营区域', icon: 'none' });
      Taro.showToast({ title: '请在同一运营区内选择', icon: 'none' });
      return;
    }
    if (startSiteCache && startSiteCache.longitude && startSiteCache.latitude) {
@@ -505,7 +619,7 @@
  };
  useEffect(() => {
    state.setOutOfServiceRange(getOutOfServiceRange(state.isOperatingArea, state.selectedSite));
    state.setRangeStatus(getRangeStatus(state.isOperatingArea, state.selectedSite));
  }, [state.isOperatingArea, state.pageType, state.selectedSite?.electronicAreaId]);
  useEffect(() => {
@@ -530,7 +644,7 @@
      state.setLongitude(cachedSite.longitude);
      applySelectionBySite(cachedSite);
      updateMapData(cachedSite.latitude, cachedSite.longitude).then(({ isInside }) => {
        state.setOutOfServiceRange(getOutOfServiceRange(isInside, cachedSite));
        state.setRangeStatus(getRangeStatus(isInside, cachedSite));
      });
    }
    fetchUserLocation();
@@ -562,7 +676,7 @@
            state.setLatitude(lat);
            state.setLongitude(lng);
            updateMapData(lat, lng).then(({ isInside }) => {
              state.setOutOfServiceRange(getOutOfServiceRange(isInside, {}));
              state.setRangeStatus(getRangeStatus(isInside, {}));
            });
            const mapCtx = Taro.createMapContext('map');
            const points = extractPointsFromPolygons(newPolygons);
@@ -622,7 +736,7 @@
    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);
    const nextStationSites = filterSelectableSites(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) {
@@ -671,7 +785,7 @@
    selectionMode: state.selectionMode,
    selectedStation: state.selectedStation,
    stationSiteList: state.stationSiteList,
    outOfServiceRange: state.outOfServiceRange,
    rangeStatus: state.rangeStatus,
    tooltipBgSrc: state.tooltipBgSrc,
    pageType: state.pageType,
    bounce: state.bounce,
src/robotaxi/pages/select-map-site/index.tsx
@@ -31,7 +31,7 @@
  }, [handleScaleChange]);
  const hasReportedPickupPageViewRef = useRef(false);
  const hasReportedDropoffPageViewRef = useRef(false);
  const hasReportedDropoffPageViewRef = useRef(false);
  useEffect(() => {
    if (selectMapSite.pageType === 'start' && !hasReportedPickupPageViewRef.current) {