hejianhao
2026-06-17 320fd377f93e229da3c01f3caf913d166ff7c25d
Merge branch 'feature_v1.0' into feature_v2.0
2个文件已添加
14个文件已修改
462 ■■■■■ 已修改文件
src/pages/robotaxi/home/hooks/use-quick-capsule.ts 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/api/begin-route.ts 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/components/tip-popup/index.tsx 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/constants/order-distance.ts 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car-modules/fare-detail-card.tsx 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/css-folder/calling-car.less 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-order-distance-config.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/pricing-rules/index.tsx 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/select-map-site/hooks/use-select-map-site.ts 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/share-trip/hooks/use-map-markers.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/share-trip/hooks/use-map-viewport.ts 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/share-trip/hooks/use-share-trip-runtime.ts 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/share-trip/hooks/use-view-delegates.tsx 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/share-trip/index.tsx 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/site-search/index.tsx 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/utils/order-distance.ts 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/hooks/use-quick-capsule.ts
@@ -1,11 +1,18 @@
import Taro from '@tarojs/taro';
import { useEffect, useMemo, useRef, useState } from 'react';
import { getUserQuickCapsule } from '../../../../robotaxi/api/home';
import { getOrderDistance } from '../../../../robotaxi/api/select-map-site';
import {
  RECOMMENDED_CONFIRM_RIDE_SOURCE_TYPE,
  completeConfirmRideSelectionSession,
  writeConfirmRideSiteSourceType
} from '../../../../robotaxi/constants/confirm-ride-report';
import { ORDER_DISTANCE_MIN_DISTANCE_TOAST } from '../../../../robotaxi/constants/order-distance';
import {
  ensureOrderDistanceConfig,
  isWithinStartEndMinDistance,
  OrderDistanceConfig
} from '../../../../robotaxi/utils/order-distance';
type CapsuleSite = {
  id: string | number;
@@ -32,6 +39,7 @@
export default function useQuickCapsule(deps: Deps) {
  const [capsuleSite, setCapsuleSite] = useState<CapsuleSite | null>(null); // 快捷胶囊站点
  const requestSeqRef = useRef(0); // 快捷胶囊请求序号,避免旧请求覆盖
  const distanceConfigRef = useRef<OrderDistanceConfig>({}); // 快捷胶囊下单距离配置缓存
  /**
   * 获取用于请求的上车点 ID(仅使用当前选中站点,避免滑动过程误用旧缓存)。
@@ -63,6 +71,17 @@
  const clearQuickCapsule = () => {
    setCapsuleSite(null);
  };
  /**
   * 确保快捷胶囊点击时具备距离配置,避免漏掉起终点最短距离校验。
   */
  const ensureDistanceConfigReady = async () => ensureOrderDistanceConfig({
    currentConfig: distanceConfigRef.current,
    fetchConfig: getOrderDistance,
    onConfigResolved: (config) => {
      distanceConfigRef.current = config;
    }
  });
  /**
   * 归一化快捷胶囊站点结构,兼容不同后端字段。
@@ -130,6 +149,15 @@
      Taro.showToast({ title: '暂无快捷目的地', icon: 'none' });
      return;
    }
    const distanceConfig = await ensureDistanceConfigReady();
    if (!isWithinStartEndMinDistance({
      startSite: targetStartSite,
      endSite: capsuleSite,
      minDistance: distanceConfig?.minDistance
    })) {
      Taro.showToast({ title: ORDER_DISTANCE_MIN_DISTANCE_TOAST, icon: 'none' });
      return;
    }
    Taro.setStorageSync('endSite', capsuleSite);
    Taro.setStorageSync('startSite', targetStartSite);
    writeConfirmRideSiteSourceType('start', RECOMMENDED_CONFIRM_RIDE_SOURCE_TYPE);
src/robotaxi/api/begin-route.ts
@@ -327,7 +327,8 @@
    payload: QueryOrderRoutePayload
): Promise<any> => {
    return request({
        url: _ROBOTAXI_HOST + `/order/api/v1/mini-program/order/queryOrderTrajectory`,
        // url: _ROBOTAXI_HOST + `/order/api/v1/mini-program/order/queryOrderTrajectory`,
        url: _ROBOTAXI_HOST + `/order/base/v1/mini-program/orderActive/share/queryOrderTrajectory`,
        method: 'GET',
        payload,
        showToast: false,
@@ -346,14 +347,6 @@
    }, true);
}
// 分享订单查询实时轨迹路线
export const shareQueryOrderRoute = async (orderId: OrderId): Promise<ShareQueryOrderRouteResponse> => {
    return request({
        url: _ROBOTAXI_HOST + `/order/base/v1/mini-program/orderActive/share/queryOrderRoute/${orderId}`,
        method: 'GET'
    }, true);
}
// 静默查询分享订单实时路线,供分享页自行处理业务异常,避免默认拦截 toast。
export const shareQueryOrderRouteSilently = async (orderId: OrderId): Promise<any> => {
    return request({
@@ -361,17 +354,6 @@
        method: 'GET',
        showToast: false,
        skipException: true
    }, true);
}
// 分享订单查询订单详情
export const shareQueryOrderInfo = async (
    payload: ShareQueryOrderInfoPayload
): Promise<ShareQueryOrderInfoResponse> => {
    return request({
        url: _ROBOTAXI_HOST + '/order/base/v1/mini-program/orderActive/share/orderInfo',
        method: 'GET',
        payload
    }, true);
}
src/robotaxi/components/tip-popup/index.tsx
@@ -1,6 +1,11 @@
import Taro from '@tarojs/taro';
import { View, Text, Image } from '@tarojs/components';
import { useEffect, useState } from 'react';
import {
    ORDER_DISTANCE_MAX_DISTANCE_TITLE,
    ORDER_DISTANCE_MIN_DISTANCE_TITLE,
    ORDER_DISTANCE_TIP_TYPE_MIN_DISTANCE
} from '../../constants/order-distance';
import './index.less'
type Props = {
@@ -53,7 +58,9 @@
            <View className='tip-mask'></View>
            <View className='tip-content' animation={animationData}>
                <Image onClick={toHome} className='close-icon' src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/close-icon.png" />
                <View className='tip-title'>{tipType === '1' ? '上车点与目的地距离较近,建议步行前往' : '当前位置距上车点较远,暂不支持叫车,请重新选择上车点'}</View>
                <View className='tip-title'>
                    {tipType === ORDER_DISTANCE_TIP_TYPE_MIN_DISTANCE ? ORDER_DISTANCE_MIN_DISTANCE_TITLE : ORDER_DISTANCE_MAX_DISTANCE_TITLE}
                </View>
                <View onClick={onClose} className='reselect-btn'>重新选点</View>
            </View>
        </View>
src/robotaxi/constants/order-distance.ts
New file
@@ -0,0 +1,14 @@
/** 起终点距离过近提示类型。 */
export const ORDER_DISTANCE_TIP_TYPE_MIN_DISTANCE = '1';
/** 用户距离起点过远提示类型。 */
export const ORDER_DISTANCE_TIP_TYPE_MAX_DISTANCE = '2';
/** 起终点距离过近弹窗文案。 */
export const ORDER_DISTANCE_MIN_DISTANCE_TITLE = '上车点与目的地距离较近,建议步行前往';
/** 用户距离起点过远弹窗文案。 */
export const ORDER_DISTANCE_MAX_DISTANCE_TITLE = '当前位置距上车点较远,暂不支持叫车,请重新选择上车点';
/** 首页快捷胶囊距离过近提示文案。 */
export const ORDER_DISTANCE_MIN_DISTANCE_TOAST = '上车点与目的地距离较近,请重新选择';
src/robotaxi/pages/begin-route/components/calling-car-modules/fare-detail-card.tsx
@@ -174,11 +174,16 @@
                        </View>
                    }
                </View>
                <View className="car-card-end-cost" onClick={toMoneyDetail}>
                    <View className="car-card-end-cost-money">
                        全程{formatTripDistance(fareBreakdown?.kilometer)}|{formatTripDurationFromSeconds(fareBreakdown?.time)}
                <View className="car-card-end-cost">
                    <View className="car-card-end-cost-label">
                        行程费用
                    </View>
                    <Image src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/ccc-icon-right.png" />
                    <View className="car-card-end-cost-content">
                        <View className="car-card-end-cost-money" onClick={toMoneyDetail}>
                            全程{formatTripDistance(fareBreakdown?.kilometer)}|{formatTripDurationFromSeconds(fareBreakdown?.time)}
                        </View>
                        <Image src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/ccc-icon-right.png" />
                    </View>
                </View>
                {
                    (fareBreakdown?.operateFree != 1 && [TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(tripStatus)) && Number(fareBreakdown?.redPacketMoney + fareBreakdown?.couponMoney) > 0 &&
@@ -200,7 +205,7 @@
                    {
                        fareBreakdown?.operateFree != 1 ?
                            <View className="pay-box-money"><View>{formatMoney(
                               fareBreakdown?.payMoney <= 0
                                fareBreakdown?.payMoney <= 0
                                    ? 0 : fareBreakdown?.payMoney)}</View><View className="unit">元</View></View>
                            :
                            <View className="pay-box-free">载客测试免费</View>
src/robotaxi/pages/begin-route/components/css-folder/calling-car.less
@@ -384,20 +384,31 @@
        margin-top: 16px;
        display: flex;
        align-items: center;
        justify-content: flex-end;
        gap: 4px;
        justify-content: space-between;
        &-money {
        &-label {
          font-size: 24px;
          line-height: 44px;
          letter-spacing: normal;
          color: rgba(0, 0, 0, 0.4);
          color: rgba(0, 0, 0, 0.9);
        }
        image {
          margin-left: 4px;
          width: 32px;
          height: 32px;
        &-content {
          display: flex;
          align-items: center;
          gap: 4px;
          &-money {
            font-size: 24px;
            line-height: 44px;
            letter-spacing: normal;
            color: rgba(0, 0, 0, 0.4);
          }
          image {
            margin-left: 4px;
            width: 32px;
            height: 32px;
          }
        }
      }
@@ -756,4 +767,4 @@
      height: 17.5px;
    }
  }
}
}
src/robotaxi/pages/begin-route/hooks/use-order-distance-config.ts
@@ -3,6 +3,10 @@
import { getOrderDistance } from '../../../api/select-map-site';
import { getDistance } from '../../../../utils/Robotaxi-public-method';
import { ensureSelectedCityByLocation } from '../../../../robotaxi/utils/city';
import {
  ORDER_DISTANCE_TIP_TYPE_MAX_DISTANCE,
  ORDER_DISTANCE_TIP_TYPE_MIN_DISTANCE
} from '../../../constants/order-distance';
type OrderDistanceConfig = {
  maxDistance?: number;
@@ -73,7 +77,7 @@
        const loc = await Taro.getLocation({ type: 'gcj02' });
        const distance = getDistance(loc.latitude, loc.longitude, startSite.latitude, startSite.longitude);
        if (distance > maxDistance) {
          deps.setTipType('2');
          deps.setTipType(ORDER_DISTANCE_TIP_TYPE_MAX_DISTANCE);
          deps.setShowTipPopup(true);
          return false;
        }
@@ -83,7 +87,7 @@
    if (minDistance) {
      const distance = getDistance(startSite.latitude, startSite.longitude, deps.endSite.latitude, deps.endSite.longitude);
      if (distance < minDistance) {
        deps.setTipType('1');
        deps.setTipType(ORDER_DISTANCE_TIP_TYPE_MIN_DISTANCE);
        deps.setShowTipPopup(true);
        return false;
      }
src/robotaxi/pages/pricing-rules/index.tsx
@@ -343,7 +343,7 @@
    waitingFeePerMinute !== null &&
    waitingFeePerMinute !== undefined &&
    waitingFeePerMinute !== ''
      ? `司机到达预约地点${waitingFreeMinutes}分钟后开始收取等待费,为${formatMoney(waitingFeePerMinute)}元/分钟,不足一分钟按一分钟计算`
      ? `司机到达预约地点${waitingFreeMinutes}分钟后开始收取等待费,为${formatMoney(waitingFeePerMinute)}元/分钟,不足1分钟按1分钟计算`
      : '';
  const longDistanceStep1 = basicPricing?.num7;
src/robotaxi/pages/select-map-site/hooks/use-select-map-site.ts
@@ -15,6 +15,12 @@
} from '../../../../utils/Robotaxi-public-method';
import { getGeofence } from '../../../api/home';
import { getOrderDistance } from '../../../api/select-map-site';
import {
  ensureOrderDistanceConfig,
  isWithinPickupMaxDistance,
  isWithinStartEndMinDistance,
  normalizeOrderDistanceConfig
} from '../../../utils/order-distance';
import { collectPoint } from '../../../api/site-search';
import { checkOtp } from '../../../../utils/tools';
import { jumpTo } from '../../../../utils/jump';
@@ -41,6 +47,10 @@
  writeConfirmRideSiteSourceType
} from '../../../constants/confirm-ride-report';
import { GeofenceType } from '../../../constants/geofence';
import {
  ORDER_DISTANCE_TIP_TYPE_MAX_DISTANCE,
  ORDER_DISTANCE_TIP_TYPE_MIN_DISTANCE
} from '../../../constants/order-distance';
const AUTO_SCALE_MAX = 17; // 自动缩放最大等级
const AUTO_SCALE_MIN = 11; // 自动缩放最小等级
@@ -617,6 +627,15 @@
  };
  /**
   * 获取距离配置,避免用户快速确认时使用到未初始化完成的配置。
   */
  const ensureDistanceConfigReady = async () => ensureOrderDistanceConfig({
    currentConfig: state.distanceConfig,
    fetchConfig: getOrderDistance,
    onConfigResolved: state.setDistanceConfig
  });
  /**
   * 确认选择站点(包含起终点距离与电子围栏校验)。
   */
  const confirmSite = async () => {
@@ -629,18 +648,30 @@
      return;
    }
    if (state.pageType === 'start') {
      const distanceConfig = await ensureDistanceConfigReady();
      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;
        }
      if (!isWithinPickupMaxDistance({
        userLatitude: state.userLatitude,
        userLongitude: state.userLongitude,
        pickupSite: state.selectedSite,
        maxDistance: distanceConfig?.maxDistance
      })) {
        state.setTipType(ORDER_DISTANCE_TIP_TYPE_MAX_DISTANCE);
        state.setShowTipPopup(true);
        return;
      }
      if (!isWithinStartEndMinDistance({
        startSite: state.selectedSite,
        endSite: endSiteCache,
        minDistance: distanceConfig?.minDistance
      })) {
        state.setTipType(ORDER_DISTANCE_TIP_TYPE_MIN_DISTANCE);
        state.setShowTipPopup(true);
        return;
      }
      reportConfirmMapSiteEvent();
      Taro.setStorageSync('startSite', state.selectedSite);
@@ -659,18 +690,20 @@
      jumpTo({ url: '/pages/user/login?hideBack=1' });
      return;
    }
    const distanceConfig = await ensureDistanceConfigReady();
    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;
      }
    if (!isWithinStartEndMinDistance({
      startSite: startSiteCache,
      endSite: state.selectedSite,
      minDistance: distanceConfig?.minDistance
    })) {
      state.setTipType(ORDER_DISTANCE_TIP_TYPE_MIN_DISTANCE);
      state.setShowTipPopup(true);
      return;
    }
    reportConfirmMapSiteEvent();
    const modifySession = Taro.getStorageSync(MODIFY_DESTINATION_SESSION_KEY); // 修改目的地会话中将终点写入候选缓存,返回行程页进入预览态
@@ -795,11 +828,7 @@
    getOrderDistance().then((res: any) => {
      if (res) {
        state.setDistanceConfig({
          maxDistance: res.maxDistance,
          minDistance: res.minDistance,
          adsorptionDistance: res.adsorptionDistance
        });
        state.setDistanceConfig(normalizeOrderDistanceConfig(res));
      }
    }).catch((err) => {
      console.error('获取距离配置失败', err);
src/robotaxi/pages/share-trip/hooks/use-map-markers.ts
@@ -85,7 +85,7 @@
        useAndroidNativeTrip1Callout: boolean
    ) => {
        if (!(pickupSite?.latitude && pickupSite?.longitude)) return null;
        if (deps.isAndroid && [TripStatus.VehicleArrivedPickup, TripStatus.InService].includes(deps.tripStatus)) {
        if (deps.isAndroid && deps.tripStatus === TripStatus.InService) {
            return null;
        }
        return {
@@ -129,7 +129,7 @@
    ) => {
        if (!(dropoffSite?.latitude && dropoffSite?.longitude)) return null;
        // 分享行程只有从 5/6 及之后的状态才渲染下车点,避免 4 态提前入列引发地图多次缩放。
        if (deps.tripStatus < TripStatus.VehicleArrivedPickup) return null;
        if (deps.tripStatus <= TripStatus.VehicleArrivedPickup) return null;
        const dropoffName =
            [TripStatus.ArrivedDestination].includes(deps.tripStatus) ? '已到达' : dropoffSite.name || '目的地';
        return {
@@ -293,7 +293,7 @@
        const shouldWaitArrivedDirection = deps.tripStatus === TripStatus.ArrivedDestination && !deps.arrivedMarkerReady;
        const effectiveShowCar = deps.statusRule.showCar && !shouldWaitArrivedDirection;
        const shouldUseAndroidDropoffOnly =
            deps.isAndroid && [TripStatus.VehicleArrivedPickup, TripStatus.InService].includes(deps.tripStatus);
            deps.isAndroid && deps.tripStatus === TripStatus.InService;
        const carMarker = createCarMarker();
        const nextMarkers: any[] = [];
        if (!shouldUseAndroidDropoffOnly && deps.statusRule.showPickup && pickupMarker) nextMarkers.push(pickupMarker);
src/robotaxi/pages/share-trip/hooks/use-map-viewport.ts
@@ -256,6 +256,12 @@
            keyParts.push(pickupSite?.latitude, pickupSite?.longitude);
            return { points, keyParts };
        }
        if (viewportType === "pickup_center") {
            const pickupPoint = buildSiteViewportPoint(pickupSite);
            if (pickupPoint) points.push(pickupPoint);
            keyParts.push(pickupSite?.latitude, pickupSite?.longitude);
            return { points, keyParts };
        }
        if (viewportType === "route") {
            if (deps.routePoints.length > 0) points.push(...deps.routePoints);
            keyParts.push(buildPointsSignature(deps.routePoints));
@@ -321,6 +327,26 @@
                : [topPadding, 50, bottomPadding, 50];
        holdDynamicScale();
        Taro.nextTick(() => {
            if (viewportType === "pickup_center") {
                if (activeTripViewportSyncTimerRef.current) {
                    clearTimeout(activeTripViewportSyncTimerRef.current);
                    activeTripViewportSyncTimerRef.current = null;
                }
                mapCtx.moveToLocation({
                    latitude: points[0].latitude,
                    longitude: points[0].longitude
                });
                activeTripViewportSyncTimerRef.current = setTimeout(() => {
                    if (typeof deps.setLatitude === "function") {
                        deps.setLatitude(points[0].latitude);
                    }
                    if (typeof deps.setLongitude === "function") {
                        deps.setLongitude(points[0].longitude);
                    }
                    activeTripViewportSyncTimerRef.current = null;
                }, ACTIVE_TRIP_VIEWPORT_SYNC_ANIMATION_MS);
                return;
            }
            if (viewportType === "dropoff") {
                mapCtx.moveToLocation({
                    latitude: points[0].latitude,
@@ -460,6 +486,10 @@
        if (![TripStatus.VehicleApproaching, TripStatus.InService].includes(deps.tripStatus)) {
            lastActiveTripViewportSyncAtRef.current = 0;
            activeTripViewportSyncingRef.current = false;
            if (activeTripViewportSyncTimerRef.current) {
                clearTimeout(activeTripViewportSyncTimerRef.current);
                activeTripViewportSyncTimerRef.current = null;
            }
            return;
        }
        syncActiveTripViewportToVehicle();
src/robotaxi/pages/share-trip/hooks/use-share-trip-runtime.ts
@@ -350,7 +350,7 @@
    ]);
    // 路线态统一交给 includePoints 计算视口,避免先写一次几何中心,再被视口纠正,出现地图“先偏后回”的抖动。
    // 这里只保留无路线视口的轻量兜底。
    if (center && [TripStatus.Publishing, TripStatus.CallingCar, TripStatus.VehicleArrivedPickup].includes(mappedStatus)) {
    if (center && [TripStatus.Publishing, TripStatus.CallingCar].includes(mappedStatus)) {
      deps.setLongitude(center.longitude);
      deps.setLatitude(center.latitude);
    }
@@ -593,6 +593,16 @@
          stopPolling();
          return;
        }
        if ([TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(infoState.mappedStatus)) {
          const routeResponse = unwrapSilentResponse(
            await requestShareRouteByStatus(infoState.mappedStatus)
          );
          if (cancelled) return;
          syncShareRoute(routeResponse, infoState.mappedStatus, infoState.nextStartSite, infoState.nextEndSite);
          stopPolling();
          errorToastShownRef.current = false;
          return;
        }
        const shouldContinuePolling = await fetchShareTripData();
        if (cancelled) return;
        if (shouldContinuePolling) {
src/robotaxi/pages/share-trip/hooks/use-view-delegates.tsx
@@ -1,7 +1,7 @@
import { CoverImage, CoverView, Image, View } from '@tarojs/components';
import { memo, useCallback, useMemo } from 'react';
import { formatDurationDisplay } from '../../../../utils/Robotaxi-public-method';
import { TripStatus } from '../../../constants/trip-status';
import { formatRemainingDurationDisplay } from '../../../utils/format';
type Deps = {
  isAndroid: boolean;
@@ -59,21 +59,27 @@
    if (!hasRouteDuration) {
      return { value: '--', unit: '' };
    }
    return formatDurationDisplay(deps.routeDuration as number);
    return formatRemainingDurationDisplay(deps.routeDuration as number);
  }, [deps.routeDuration, hasRouteDuration]);
  // 与 begin-route 保持一致:车辆气泡与路线摘要不足 1 分钟时统一展示 1 分钟。
  const vehicleMetricDurationDisplay = useMemo<DurationDisplay>(() => (
    durationDisplay.unit === '秒'
      ? { value: 1, unit: '分钟' }
      : durationDisplay
  ), [durationDisplay]);
  const shouldHideAndroidMarkerCallout = deps.isAndroid
    && [TripStatus.PublishRoute, TripStatus.VehicleArrivedPickup, TripStatus.TripFinished, TripStatus.PaymentSuccess, TripStatus.CancelOnlyPickup].includes(deps.tripStatus);
  const androidCarCalloutContent = useMemo(() => (
    <AndroidCarCalloutContent
      routeDistance={deps.routeDistance}
      durationDisplay={durationDisplay}
      durationDisplay={vehicleMetricDurationDisplay}
    />
  ), [deps.routeDistance, durationDisplay]);
  ), [deps.routeDistance, vehicleMetricDurationDisplay]);
  const routeSummaryText = useMemo(() => (
    deps.routeDistance && hasRouteDuration
      ? `${durationDisplay.value}${durationDisplay.unit} ${deps.routeDistance.value}${deps.routeDistance.unit === 'm' ? '米' : '公里'}`
      ? `${vehicleMetricDurationDisplay.value}${vehicleMetricDurationDisplay.unit} ${deps.routeDistance.value}${deps.routeDistance.unit === 'm' ? '米' : '公里'}`
      : ''
  ), [deps.routeDistance, durationDisplay, hasRouteDuration]);
  ), [deps.routeDistance, hasRouteDuration, vehicleMetricDurationDisplay]);
  const renderWaitingCallout = useCallback((item: any, calloutKey: string) => (
    <CoverView slot='callout' marker-id={item.id} key={calloutKey} className='waiting-callout'>
@@ -121,8 +127,8 @@
            <CoverView className='car-time'>
              <CoverView className='car-time-num'>{deps.routeDistance?.value ?? 0}</CoverView>
              <CoverView>{deps.routeDistance?.unit === 'm' ? '米' : '公里'}</CoverView>
              <CoverView style={{ marginLeft: '2.5px' }} className='car-time-num'>{durationDisplay.value}</CoverView>
              <CoverView>{durationDisplay.unit}</CoverView>
              <CoverView style={{ marginLeft: '2.5px' }} className='car-time-num'>{vehicleMetricDurationDisplay.value}</CoverView>
              <CoverView>{vehicleMetricDurationDisplay.unit}</CoverView>
            </CoverView>
          </CoverView>
        )}
@@ -141,8 +147,8 @@
    deps.isAndroid,
    deps.routeDistance,
    deps.tripStatus,
    durationDisplay,
    routeSummaryText,
    vehicleMetricDurationDisplay,
  ]);
  const renderMarkerCallout = useCallback((item: any, index: number) => {
src/robotaxi/pages/share-trip/index.tsx
@@ -69,7 +69,7 @@
    [TripStatus.Publishing]: { showPickup: true, showDropoff: false, showCar: false, showRoute: false, showWalking: false, viewport: 'pickup', translateCar: false },
    [TripStatus.CallingCar]: { showPickup: true, showDropoff: false, showCar: false, showRoute: false, showWalking: false, viewport: 'pickup', translateCar: false },
    [TripStatus.VehicleApproaching]: { showPickup: true, showDropoff: false, showCar: true, showRoute: true, showWalking: false, viewport: 'route', translateCar: true },
    [TripStatus.VehicleArrivedPickup]: { showPickup: true, showDropoff: false, showCar: true, showRoute: false, showWalking: false, viewport: 'car_user', translateCar: true },
    [TripStatus.VehicleArrivedPickup]: { showPickup: true, showDropoff: false, showCar: true, showRoute: false, showWalking: false, viewport: 'pickup_center', translateCar: true },
    [TripStatus.InService]: { showPickup: false, showDropoff: true, showCar: true, showRoute: true, showWalking: false, viewport: 'route', translateCar: true },
    [TripStatus.ArrivedDestination]: { showPickup: false, showDropoff: true, showCar: false, showRoute: true, showWalking: false, viewport: 'order_detail', translateCar: false },
    [TripStatus.TripFinished]: { showPickup: true, showDropoff: true, showCar: false, showRoute: true, showWalking: false, viewport: 'order_detail', translateCar: false },
src/robotaxi/pages/site-search/index.tsx
@@ -2,8 +2,14 @@
import { useState, useEffect, useRef } from 'react';
import { View, Text, Image, Input } from '@tarojs/components';
import './index.less';
import { getDistance, reportEvent, toUnixSeconds } from '../../../utils/Robotaxi-public-method';
import { reportEvent, toUnixSeconds } from '../../../utils/Robotaxi-public-method';
import { getOrderDistance } from '../../api/select-map-site';
import {
  ensureOrderDistanceConfig,
  isWithinPickupMaxDistance,
  isWithinStartEndMinDistance,
  normalizeOrderDistanceConfig
} from '../../utils/order-distance';
import { checkOtp } from '../../../utils/tools';
import TipPopup from '../../components/tip-popup';
import SiteListItem from './components/site-list-item';
@@ -42,6 +48,10 @@
  HOME_START_SITE_SOURCE_MANUAL_SELECTED,
  writeHomeStartSiteDisplayState
} from '../../hooks/use-home-start-site-display';
import {
  ORDER_DISTANCE_TIP_TYPE_MAX_DISTANCE,
  ORDER_DISTANCE_TIP_TYPE_MIN_DISTANCE
} from '../../constants/order-distance';
import {
  HISTORY_CONFIRM_RIDE_SOURCE_TYPE,
  RECOMMENDED_CONFIRM_RIDE_SOURCE_TYPE,
@@ -94,20 +104,26 @@
  useEffect(() => {
    getOrderDistance().then((res: any) => {
      if (res) {
        setDistanceConfig({
          maxDistance: res.maxDistance,
          minDistance: res.minDistance,
          adsorptionDistance: res.adsorptionDistance
        });
        setDistanceConfig(normalizeOrderDistanceConfig(res));
      }
    }).catch(() => { });
  }, []);
  /**
   * 确保搜索页距离配置可用,避免快速确认时漏掉校验。
   */
  const ensureDistanceConfigReady = async () => ensureOrderDistanceConfig({
    currentConfig: distanceConfig,
    fetchConfig: getOrderDistance,
    onConfigResolved: setDistanceConfig
  });
  // 处理终点选择后的业务校验与跳转
  const handleEndSiteSelection = async (
    item: any,
    sourceType = UNKNOWN_CONFIRM_RIDE_SOURCE_TYPE
  ) => {
    const resolvedDistanceConfig = await ensureDistanceConfigReady();
    const startSiteCache = Taro.getStorageSync('startSite');
    if (startSiteCache?.electronicAreaId && item?.electronicAreaId && startSiteCache.electronicAreaId !== item.electronicAreaId) {
      Taro.showToast({
@@ -117,29 +133,14 @@
      return
    }
    try {
      let minDistance = distanceConfig?.minDistance;
      if (!distanceConfig?.minDistance) {
        const res = await getOrderDistance();
        if (res) {
          minDistance = res.minDistance;
          setDistanceConfig({
            maxDistance: res.maxDistance,
            minDistance: res.minDistance,
            adsorptionDistance: res.adsorptionDistance,
            geo_lat: item.latitude,
            geo_lng: item.longitude,
            odd_grid_id: item.electronicAreaId,
            timestamp: Date.now(),
          });
        }
      }
      if (startSiteCache && startSiteCache.longitude && startSiteCache.latitude) {
        const distance = getDistance(startSiteCache.latitude, startSiteCache.longitude, item.latitude, item.longitude);
        if (minDistance && distance < minDistance) {
          setTipType('1');
          setShowTipPopup(true);
          return;
        }
      if (!isWithinStartEndMinDistance({
        startSite: startSiteCache,
        endSite: item,
        minDistance: resolvedDistanceConfig?.minDistance
      })) {
        setTipType(ORDER_DISTANCE_TIP_TYPE_MIN_DISTANCE);
        setShowTipPopup(true);
        return;
      }
    } catch { }
    const modifySession = Taro.getStorageSync(MODIFY_DESTINATION_SESSION_KEY);
@@ -345,6 +346,7 @@
      return
    }
    if (pageType == 'start') {
      const resolvedDistanceConfig = await ensureDistanceConfigReady();
      const endSiteCache = Taro.getStorageSync('endSite');
      if (endSiteCache?.electronicAreaId && item?.electronicAreaId && endSiteCache.electronicAreaId !== item.electronicAreaId) {
        Taro.showToast({
@@ -354,20 +356,23 @@
        return
      }
      try {
        if (!distanceConfig?.maxDistance) {
          const res = await getOrderDistance();
          if (res) {
            setDistanceConfig({
              maxDistance: res.maxDistance,
              minDistance: res.minDistance,
              adsorptionDistance: res.adsorptionDistance
            });
          }
        }
        const loc = await Taro.getLocation({ type: 'gcj02' });
        const distance = getDistance(loc.latitude, loc.longitude, item.latitude, item.longitude);
        if (distanceConfig?.maxDistance && distance > distanceConfig.maxDistance) {
          setTipType('2');
        if (!isWithinPickupMaxDistance({
          userLatitude: loc.latitude,
          userLongitude: loc.longitude,
          pickupSite: item,
          maxDistance: resolvedDistanceConfig?.maxDistance
        })) {
          setTipType(ORDER_DISTANCE_TIP_TYPE_MAX_DISTANCE);
          setShowTipPopup(true);
          return;
        }
        if (!isWithinStartEndMinDistance({
          startSite: item,
          endSite: endSiteCache,
          minDistance: resolvedDistanceConfig?.minDistance
        })) {
          setTipType(ORDER_DISTANCE_TIP_TYPE_MIN_DISTANCE);
          setShowTipPopup(true);
          return;
        }
src/robotaxi/utils/order-distance.ts
New file
@@ -0,0 +1,103 @@
import { getDistance } from '../../utils/Robotaxi-public-method';
/** 下单距离配置。 */
export type OrderDistanceConfig = {
  /** 用户到起点的最大距离。 */
  maxDistance?: number;
  /** 起终点之间的最小距离。 */
  minDistance?: number;
  /** 点位吸附距离。 */
  adsorptionDistance?: number;
};
type CoordinateSite = {
  latitude?: number | string;
  longitude?: number | string;
};
/**
 * 统一将下单距离配置归一化为数字,避免字符串配置影响校验。
 */
export const normalizeOrderDistanceConfig = (config: any): OrderDistanceConfig => ({
  maxDistance: Number.isFinite(Number(config?.maxDistance)) ? Number(config.maxDistance) : undefined,
  minDistance: Number.isFinite(Number(config?.minDistance)) ? Number(config.minDistance) : undefined,
  adsorptionDistance: Number.isFinite(Number(config?.adsorptionDistance)) ? Number(config.adsorptionDistance) : undefined
});
/**
 * 确保下单距离配置可用,必要时拉取最新配置并回写页面状态。
 */
export const ensureOrderDistanceConfig = async (params: {
  currentConfig: any;
  fetchConfig: () => Promise<any>;
  onConfigResolved?: (config: OrderDistanceConfig) => void;
}) => {
  const normalizedCurrentConfig = normalizeOrderDistanceConfig(params.currentConfig);
  if (
    Number.isFinite(normalizedCurrentConfig.maxDistance)
    && Number.isFinite(normalizedCurrentConfig.minDistance)
  ) {
    return normalizedCurrentConfig;
  }
  const response = await params.fetchConfig();
  const normalizedNextConfig = normalizeOrderDistanceConfig(response);
  params.onConfigResolved?.(normalizedNextConfig);
  return normalizedNextConfig;
};
/**
 * 判断站点是否具备有效经纬度。
 */
export const hasValidSiteCoordinate = (site: CoordinateSite | null | undefined) => (
  Number.isFinite(Number(site?.latitude)) && Number.isFinite(Number(site?.longitude))
);
/**
 * 校验用户当前位置到起点的最大允许距离。
 */
export const isWithinPickupMaxDistance = (params: {
  userLatitude?: number | null;
  userLongitude?: number | null;
  pickupSite?: CoordinateSite | null;
  maxDistance?: number;
}) => {
  if (
    !Number.isFinite(Number(params.userLatitude))
    || !Number.isFinite(Number(params.userLongitude))
    || !hasValidSiteCoordinate(params.pickupSite)
    || !Number.isFinite(Number(params.maxDistance))
  ) {
    return true;
  }
  const distance = getDistance(
    Number(params.userLatitude),
    Number(params.userLongitude),
    Number(params.pickupSite?.latitude),
    Number(params.pickupSite?.longitude)
  );
  return distance <= Number(params.maxDistance);
};
/**
 * 校验起终点是否满足最短下单距离。
 */
export const isWithinStartEndMinDistance = (params: {
  startSite?: CoordinateSite | null;
  endSite?: CoordinateSite | null;
  minDistance?: number;
}) => {
  if (
    !hasValidSiteCoordinate(params.startSite)
    || !hasValidSiteCoordinate(params.endSite)
    || !Number.isFinite(Number(params.minDistance))
  ) {
    return true;
  }
  const distance = getDistance(
    Number(params.startSite?.latitude),
    Number(params.startSite?.longitude),
    Number(params.endSite?.latitude),
    Number(params.endSite?.longitude)
  );
  return distance >= Number(params.minDistance);
};