xuw5
2026-04-23 ef834a2310f54903e5854676bc3e5c62b248ee0f
Merge branch 'feature_v1.0' into feature/cashier-page
2个文件已添加
9个文件已修改
590 ■■■■■ 已修改文件
src/pages/robotaxi/home/hooks/use-map-state.ts 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/hooks/use-nearby-vehicles.ts 346 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/hooks/use-region-change.ts 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/index.tsx 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/api/home.ts 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/constants/subscribe.ts 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car-modules/fare-detail-card.tsx 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-order-actions.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/select-map-site/index.tsx 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/Robotaxi-public-method.ts 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/subscribe.ts 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/hooks/use-map-state.ts
@@ -1,6 +1,9 @@
import { useRef, useState } from 'react';
import Taro from '@tarojs/taro';
import { fetchMapData } from '../../../../utils/Robotaxi-public-method';
import { fetchMapData, getAutoScaleBySites } from '../../../../utils/Robotaxi-public-method';
const AUTO_SCALE_MAX = 17; // 自动缩放最大等级
const AUTO_SCALE_MIN = 11; // 自动缩放最小等级
/**
 * 地图状态管理:拥有地图坐标、围栏、标记点及地图业务数据刷新能力。
@@ -20,6 +23,7 @@
  const isReturningToOrigin = useRef(false);
  const resetTimer = useRef<any>(null);
  const pendingNearbyRequestCountRef = useRef(0);
  const autoScaleAppliedCityCodeRef = useRef(''); // 已执行自动缩放的城市 code
  /**
   * 根据经纬度和城市编码刷新地图业务数据(围栏内判断、电子围栏 ID、站点标记)。
@@ -36,6 +40,17 @@
      setIsOperatingArea(result.isInside);
      setElectronicAreaId(result.electronicAreaId);
      setMarkers(result.newMarkers);
      if (autoScaleAppliedCityCodeRef.current !== code) {
        const autoScale = getAutoScaleBySites(result?.sites || result?.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;
          });
        }
        autoScaleAppliedCityCodeRef.current = code;
      }
      return result;
    } finally {
      pendingNearbyRequestCountRef.current = Math.max(0, pendingNearbyRequestCountRef.current - 1);
src/pages/robotaxi/home/hooks/use-nearby-vehicles.ts
New file
@@ -0,0 +1,346 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { getNearbyVehicles } from '../../../../robotaxi/api/home';
import { getDistance } from '../../../../utils/Robotaxi-public-method';
type Deps = {
  isOperatingArea: boolean;
};
type NearbyCenter = {
  lat: number;
  lon: number;
};
type NearbyVehicleItem = {
  id: string;
  latitude: number;
  longitude: number;
  direction: number;
};
type VehicleTrackItem = {
  id: string;
  fromLat: number;
  fromLon: number;
  toLat: number;
  toLon: number;
  fromDirection: number;
  toDirection: number;
};
const POLL_INTERVAL_MS = 5000; // 车辆轮询周期(毫秒)
const SETTLE_DELAY_MS = 500; // 地图停止后的静止判定时间(毫秒)
const MOVE_THRESHOLD_METERS = 500; // 触发新搜索的位移阈值(米)
const ANIMATION_DURATION_MS = 4500; // 车辆位移动画时长(毫秒)
const ANIMATION_FRAME_MS = 120; // 动画帧间隔(毫秒)
const VEHICLE_MARKER_ID_BASE = 900000000; // 车辆 marker id 起始段
const VEHICLE_MARKER_ICON = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/marker-car.png'; // 车辆 icon 地址
/**
 * 归一化角度到 [0, 360) 区间。
 */
const normalizeDirection = (value: number) => {
  const safe = Number(value);
  if (!Number.isFinite(safe)) return 0;
  return ((safe % 360) + 360) % 360;
};
/**
 * 插值航向角,选择最短旋转路径避免大角度跳变。
 */
const interpolateDirection = (from: number, to: number, progress: number) => {
  const fromSafe = normalizeDirection(from);
  const toSafe = normalizeDirection(to);
  const delta = ((((toSafe - fromSafe) % 360) + 540) % 360) - 180;
  return normalizeDirection(fromSafe + delta * progress);
};
/**
 * 将任意字符串稳定映射到正整数,避免与站点 marker id 冲突。
 */
const hashToMarkerId = (rawId: string) => {
  let hash = 0;
  for (let i = 0; i < rawId.length; i += 1) {
    hash = (hash << 5) - hash + rawId.charCodeAt(i);
    hash |= 0;
  }
  return VEHICLE_MARKER_ID_BASE + (Math.abs(hash) % 9999999);
};
/**
 * 附近车辆编排:处理停稳判定、500 米阈值、5 秒轮询与平滑动画。
 */
export default function useNearbyVehicles({ isOperatingArea }: Deps) {
  const [vehicleMarkers, setVehicleMarkers] = useState<any[]>([]); // 车辆 marker 列表
  const pollTimerRef = useRef<any>(null); // 轮询定时器句柄
  const settleTimerRef = useRef<any>(null); // 停稳判定定时器句柄
  const animationTimerRef = useRef<any>(null); // 动画帧定时器句柄
  const requestLockRef = useRef(false); // 请求并发锁,防止重入
  const latestSeqRef = useRef(0); // 请求序号,防止旧响应覆盖新状态
  const activeSearchCenterRef = useRef<NearbyCenter | null>(null); // 当前轮询中心点
  const lastSearchCenterRef = useRef<NearbyCenter | null>(null); // 上一次发请求的中心点
  const renderedVehicleMapRef = useRef<Map<string, NearbyVehicleItem>>(new Map()); // 当前渲染中的车辆
  const animationStateRef = useRef<{ startAt: number; endAt: number; tracks: VehicleTrackItem[] } | null>(null); // 当前动画轨迹状态
  /**
   * 清理动画定时器。
   */
  const clearAnimationTimer = useCallback(() => {
    if (animationTimerRef.current) {
      clearTimeout(animationTimerRef.current);
      animationTimerRef.current = null;
    }
    animationStateRef.current = null;
  }, []);
  /**
   * 清理轮询定时器。
   */
  const clearPollTimer = useCallback(() => {
    if (pollTimerRef.current) {
      clearInterval(pollTimerRef.current);
      pollTimerRef.current = null;
    }
  }, []);
  /**
   * 清理停稳判定定时器。
   */
  const clearSettleTimer = useCallback(() => {
    if (settleTimerRef.current) {
      clearTimeout(settleTimerRef.current);
      settleTimerRef.current = null;
    }
  }, []);
  /**
   * 清空车辆渲染状态。
   */
  const clearVehicles = useCallback(() => {
    renderedVehicleMapRef.current = new Map();
    setVehicleMarkers([]);
    clearAnimationTimer();
  }, [clearAnimationTimer]);
  /**
   * 停止车辆相关任务并清空车辆。
   */
  const disposeVehicles = useCallback(() => {
    clearSettleTimer();
    clearPollTimer();
    clearVehicles();
    activeSearchCenterRef.current = null;
    lastSearchCenterRef.current = null;
  }, [clearPollTimer, clearSettleTimer, clearVehicles]);
  /**
   * 将车辆位置数据映射为地图 marker。
   */
  const buildVehicleMarkers = useCallback((vehicles: NearbyVehicleItem[]) => {
    return vehicles.map((vehicle) => ({
      id: hashToMarkerId(vehicle.id),
      markerType: 'vehicle',
      vehicleId: vehicle.id,
      latitude: vehicle.latitude,
      longitude: vehicle.longitude,
      rotate: vehicle.direction,
      width: 20,
      height: 20,
      zIndex: 6,
      anchor: { x: 0.5, y: 0.5 },
      iconPath: VEHICLE_MARKER_ICON
    }));
  }, []);
  /**
   * 基于插值轨迹推进车辆动画帧。
   */
  const playAnimationFrame = useCallback(() => {
    const animationState = animationStateRef.current;
    if (!animationState) return;
    const now = Date.now();
    const progress = Math.max(0, Math.min(1, (now - animationState.startAt) / Math.max(1, animationState.endAt - animationState.startAt)));
    const nextVehicleMap = new Map<string, NearbyVehicleItem>();
    animationState.tracks.forEach((track) => {
      const lat = track.fromLat + (track.toLat - track.fromLat) * progress;
      const lon = track.fromLon + (track.toLon - track.fromLon) * progress;
      const direction = interpolateDirection(track.fromDirection, track.toDirection, progress);
      nextVehicleMap.set(track.id, {
        id: track.id,
        latitude: lat,
        longitude: lon,
        direction
      });
    });
    renderedVehicleMapRef.current = nextVehicleMap;
    setVehicleMarkers(buildVehicleMarkers(Array.from(nextVehicleMap.values())));
    if (progress >= 1) {
      animationStateRef.current = null;
      animationTimerRef.current = null;
      return;
    }
    animationTimerRef.current = setTimeout(() => {
      playAnimationFrame();
    }, ANIMATION_FRAME_MS);
  }, [buildVehicleMarkers]);
  /**
   * 规范化接口返回,过滤非法经纬度。
   */
  const normalizeVehicleList = useCallback((response: any): NearbyVehicleItem[] => {
    const rawList = Array.isArray(response)
      ? response
      : Array.isArray(response?.list)
        ? response.list
        : Array.isArray(response?.vehicles)
          ? response.vehicles
          : [];
    return rawList
      .map((item: any, index: number) => {
        const latitude = Number(item?.latitude);
        const longitude = Number(item?.longitude);
        if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) return null;
        const rawId = item?.id ?? `vehicle-${index}-${latitude}-${longitude}`;
        return {
          id: String(rawId),
          latitude,
          longitude,
          direction: normalizeDirection(Number(item?.direction))
        };
      })
      .filter(Boolean) as NearbyVehicleItem[];
  }, []);
  /**
   * 请求车辆并衔接平滑位移动画。
   */
  const fetchAndAnimateVehicles = useCallback(async (center: NearbyCenter) => {
    if (requestLockRef.current) return;
    requestLockRef.current = true;
    const seq = latestSeqRef.current + 1;
    latestSeqRef.current = seq;
    try {
      const response = await getNearbyVehicles({ lat: center.lat, lon: center.lon });
      if (seq !== latestSeqRef.current) return;
      const normalizedVehicles = normalizeVehicleList(response);
      if (normalizedVehicles.length === 0) {
        clearVehicles();
        return;
      }
      const now = Date.now();
      const currentVehicleMap = renderedVehicleMapRef.current;
      const tracks: VehicleTrackItem[] = normalizedVehicles.map((vehicle) => {
        const currentVehicle = currentVehicleMap.get(vehicle.id);
        return {
          id: vehicle.id,
          fromLat: currentVehicle?.latitude ?? vehicle.latitude,
          fromLon: currentVehicle?.longitude ?? vehicle.longitude,
          toLat: vehicle.latitude,
          toLon: vehicle.longitude,
          fromDirection: currentVehicle?.direction ?? vehicle.direction,
          toDirection: vehicle.direction
        };
      });
      clearAnimationTimer();
      animationStateRef.current = {
        startAt: now,
        endAt: now + ANIMATION_DURATION_MS,
        tracks
      };
      playAnimationFrame();
    } catch (error) {
      // 请求异常时保持当前车辆状态,避免闪断。
    } finally {
      requestLockRef.current = false;
    }
  }, [clearAnimationTimer, clearVehicles, normalizeVehicleList, playAnimationFrame]);
  /**
   * 启动 5 秒轮询,轮询中心遵循 activeSearchCenterRef。
   */
  const ensurePolling = useCallback(() => {
    if (pollTimerRef.current) return;
    pollTimerRef.current = setInterval(() => {
      const center = activeSearchCenterRef.current;
      if (!center || !isOperatingArea) {
        disposeVehicles();
        return;
      }
      fetchAndAnimateVehicles(center);
    }, POLL_INTERVAL_MS);
  }, [disposeVehicles, fetchAndAnimateVehicles, isOperatingArea]);
  /**
   * 激活指定中心点并立即拉起一次车辆查询。
   */
  const activateCenterAndFetch = useCallback((center: NearbyCenter) => {
    activeSearchCenterRef.current = center;
    lastSearchCenterRef.current = center;
    fetchAndAnimateVehicles(center);
    ensurePolling();
  }, [ensurePolling, fetchAndAnimateVehicles]);
  /**
   * 首页初始化触发:围栏加载完成且中心点在 ODD 内时立即开始轮询。
   */
  const tryStartPollingOnInit = useCallback((payload: { lat: number; lon: number; isInside: boolean }) => {
    if (!payload?.isInside || !isOperatingArea) return;
    const lat = Number(payload.lat);
    const lon = Number(payload.lon);
    if (!Number.isFinite(lat) || !Number.isFinite(lon)) return;
    if (lastSearchCenterRef.current) return;
    activateCenterAndFetch({ lat, lon });
  }, [activateCenterAndFetch, isOperatingArea]);
  /**
   * 地图拖动开始:仅清理停稳判定,不发起请求。
   */
  const handleRegionBegin = useCallback(() => {
    clearSettleTimer();
  }, [clearSettleTimer]);
  /**
   * 地图拖动结束:静止 500ms 后按 500 米阈值判断是否更新搜索中心。
   */
  const handleRegionSettled = useCallback((payload: { lat: number; lon: number; isInside: boolean }) => {
    clearSettleTimer();
    settleTimerRef.current = setTimeout(() => {
      if (!payload?.isInside || !isOperatingArea) {
        disposeVehicles();
        return;
      }
      const currentCenter = { lat: payload.lat, lon: payload.lon };
      const lastSearchCenter = lastSearchCenterRef.current;
      if (!lastSearchCenter) {
        activateCenterAndFetch(currentCenter);
        return;
      }
      const movedDistance = getDistance(lastSearchCenter.lat, lastSearchCenter.lon, currentCenter.lat, currentCenter.lon);
      if (movedDistance > MOVE_THRESHOLD_METERS) {
        activateCenterAndFetch(currentCenter);
      } else {
        ensurePolling();
      }
    }, SETTLE_DELAY_MS);
  }, [activateCenterAndFetch, clearSettleTimer, disposeVehicles, ensurePolling, isOperatingArea]);
  useEffect(() => {
    if (!isOperatingArea) {
      disposeVehicles();
    }
  }, [disposeVehicles, isOperatingArea]);
  useEffect(() => {
    return () => {
      disposeVehicles();
    };
  }, [disposeVehicles]);
  return {
    vehicleMarkers,
    handleRegionBegin,
    handleRegionSettled,
    tryStartPollingOnInit,
    disposeVehicles
  };
}
src/pages/robotaxi/home/hooks/use-region-change.ts
@@ -5,12 +5,14 @@
  mapState: any;
  siteState: any;
  toOrigin: () => void;
  onRegionBegin?: () => void;
  onRegionSettled?: (payload: { lat: number; lon: number; isInside: boolean }) => void;
};
/**
 * 地图 region-change 编排:处理地图拖拽/缩放后的吸附、回填和自动归位。
 */
export default function useRegionChange({ mapState, siteState, toOrigin }: Deps) {
export default function useRegionChange({ mapState, siteState, toOrigin, onRegionBegin, onRegionSettled }: Deps) {
  /**
   * 通过中心点主动触发吸附判断。
   */
@@ -63,7 +65,10 @@
          return;
        }
        try {
          const { newMarkers, sites } = await mapState.updateMapData(lat, lng);
          const { isInside, newMarkers, sites } = await mapState.updateMapData(lat, lng);
          if (typeof onRegionSettled === 'function') {
            onRegionSettled({ lat, lon: lng, isInside: !!isInside });
          }
          const nearestCandidates = sites.length > 0 ? sites : newMarkers;
          const { adsorbed, marker } = siteState.tryAdsorb(lat, lng, nearestCandidates);
          if ((causedBy === 'gesture' || causedBy === 'drag' || wasReturningToOrigin) && marker) {
@@ -106,6 +111,9 @@
    }
    if (type === 'begin') {
      siteState.setShowToolTip(false);
      if (typeof onRegionBegin === 'function') {
        onRegionBegin();
      }
      return;
    }
    if (type !== 'end') return;
src/pages/robotaxi/home/index.tsx
@@ -1,4 +1,4 @@
import Taro, { useDidShow } from "@tarojs/taro"
import Taro, { useDidHide, useDidShow } from "@tarojs/taro"
import { useEffect, useState, useRef, useMemo } from 'react';
import { View, Map, Text, Image, Button } from '@tarojs/components';
import './index.less';
@@ -46,6 +46,8 @@
import useHomeBootstrap from './hooks/use-home-bootstrap';
// 快捷胶囊编排
import useQuickCapsule from './hooks/use-quick-capsule';
// 附近车辆编排
import useNearbyVehicles from './hooks/use-nearby-vehicles';
import { DEFAULT_LATITUDE, DEFAULT_LONGITUDE, FIRST_ENTER_KEY } from '../../../robotaxi/constants/map';
import { TripStatus, toTripStatus } from '../../../robotaxi/pages/begin-route/constants/trip-status';
import {
@@ -86,7 +88,7 @@
  const {
    longitude,
    latitude,
    markers,
    markers: siteMarkers,
    polygons,
    mapScale,
    isOperatingArea,
@@ -380,12 +382,49 @@
  useDidShow(() => {
    bootstrap.onDidShow();
    refreshQuickCapsule();
    if (typeof latitude === 'number' && typeof longitude === 'number') {
      tryStartPollingOnInit({
        lat: latitude,
        lon: longitude,
        isInside: isOperatingArea
      });
    }
  });
  useDidHide(() => {
    disposeVehicles();
  });
  const {
    vehicleMarkers,
    handleRegionBegin: handleVehicleRegionBegin,
    handleRegionSettled: handleVehicleRegionSettled,
    tryStartPollingOnInit,
    disposeVehicles
  } = useNearbyVehicles({
    isOperatingArea
  }); // 附近车辆编排
  useEffect(() => {
    if (nearbyLoading || isLocationAuthDenied) return;
    if (typeof latitude !== 'number' || typeof longitude !== 'number') return;
    tryStartPollingOnInit({
      lat: latitude,
      lon: longitude,
      isInside: isOperatingArea
    });
  }, [nearbyLoading, isLocationAuthDenied, latitude, longitude, isOperatingArea, tryStartPollingOnInit]);
  const mergedMapMarkers = useMemo(() => {
    return [...siteMarkers, ...vehicleMarkers];
  }, [siteMarkers, vehicleMarkers]); // 地图合并 marker 列表
  const { handleRegionChange, queryPointerByCenter } = useRegionChange({
    mapState,
    siteState,
    toOrigin
    toOrigin,
    onRegionBegin: handleVehicleRegionBegin,
    onRegionSettled: handleVehicleRegionSettled
  }); // 地图 region-change 编排
  const {
    ensureNetwork,
@@ -426,8 +465,11 @@
    const markerId = e?.detail?.markerId;
    if (markerId === undefined) return;
    const targetMarker = markers.find(m => m.id === markerId);
    const targetMarker = mergedMapMarkers.find(m => m.id === markerId);
    if (targetMarker) {
      if (targetMarker.markerType === 'vehicle') {
        return;
      }
      // 忽略因调用 moveToLocation 引起的 region change,避免重复吸附计算
      shouldIgnoreRegionChange.current = true;
@@ -457,7 +499,7 @@
        longitude={longitude ?? DEFAULT_LONGITUDE}
        latitude={latitude ?? DEFAULT_LATITUDE}
        scale={mapScale}
        markers={isLocationAuthDenied ? [] : markers.map(m => ({ ...m, title: '', name: '' }))}
        markers={isLocationAuthDenied ? [] : mergedMapMarkers.map(m => ({ ...m, title: '', name: '' }))}
        polygons={polygons}
        showLocation={!!longitude}
        onRegionChange={handleRegionChange}
src/robotaxi/api/home.ts
@@ -101,3 +101,12 @@
        payload
    }, true);
}
// 获取附近车辆
export const getNearbyVehicles = async (payload: { lat: number; lon: number }) => {
    return request({
        url: _ROBOTAXI_HOST + '/vehicleConvoy/api/v1/mini-program/vehicle/nearby',
        method: 'GET',
        payload
    }, true);
}
src/robotaxi/constants/subscribe.ts
New file
@@ -0,0 +1,36 @@
// 订阅场景枚举
export enum SubscribeSceneKey {
  OrderSuccess = 'ORDER_CREATE_SUCCESS',
  OperationCouponGroup = 'OPERATION_COUPON_GROUP',
}
// 订阅场景配置类型
export type SubscribeSceneConfig = {
  sceneKey: SubscribeSceneKey;
  tmplIds: string[];
  limit: number;
  needLogin: boolean;
};
// 下单成功订阅模板配置
export const ORDER_SUCCESS_SUBSCRIBE_CONFIG: SubscribeSceneConfig = {
  sceneKey: SubscribeSceneKey.OrderSuccess,
  tmplIds: [
    'NOi0ff4rpuTxcRnnZl0SJxg9SMbvrc8xB8VJgrwu3oo',
    'elyV30kO0BZAD2OtihJ4Wdu7fwR3FdvCZYCM1BEbqWs'
  ],
  limit: 1,
  needLogin: true
};
// 运营活动 + 券效期 + 券到账订阅模板配置
export const OPERATION_COUPON_SUBSCRIBE_CONFIG: SubscribeSceneConfig = {
  sceneKey: SubscribeSceneKey.OperationCouponGroup,
  tmplIds: [
    'S6gGP8jyuDTkW03h8LhOm2KVDdUtHr0OhL-Mt_PyRPU',
    'ANtToR63JPCV_ujV69gmO15xCa8O3elnLWB52HLdcHM',
    'P7z_ksdyzpiiOBO07EJ1AZ6WeHlWs4XveQYjmt9JRE8'
  ],
  limit: 1,
  needLogin: true
};
src/robotaxi/pages/begin-route/components/calling-car-modules/fare-detail-card.tsx
@@ -68,7 +68,7 @@
                        行程费用
                    </View>
                    {
                        fareBreakdown.operateFree == 1 ?
                        fareBreakdown?.operateFree == 1 ?
                            <View className="car-card-end-particulars-free">
                                载客测试免费
                            </View>
@@ -163,15 +163,15 @@
                        </View>
                    }
                </View>
                <View className="car-card-end-cost">
                <View className="car-card-end-cost" onClick={toMoneyDetail}>
                    <View className="car-card-end-cost-money">
                        全程{formatTripDistance(fareBreakdown?.kilometer)}|{formatTripDurationFromSeconds(fareBreakdown?.time)}
                    </View>
                    <Image src={require('../../../../assets/ccc-icon-right.png')} />
                </View>
                {
                    (fareBreakdown?.redPacketMoney + fareBreakdown?.couponMoney > 0 && fareBreakdown.operateFree != 1) &&
                    <View className="car-card-end-through" onClick={toMoneyDetail}>
                    (fareBreakdown?.redPacketMoney + fareBreakdown?.couponMoney > 0 && fareBreakdown?.operateFree != 1) &&
                    <View className="car-card-end-through">
                        <View className="car-card-end-through-label">优惠活动</View>
                        <View className="car-card-end-through-content">
                            <View className="car-card-end-through-content-title">最佳优惠</View>
@@ -185,7 +185,7 @@
                tripStatus === TripStatus.TripFinished &&
                <View className="pay-box">
                    {
                        fareBreakdown.operateFree != 1 ?
                        fareBreakdown?.operateFree != 1 ?
                            <View className="pay-box-money"><View>0.00</View><View className="unit">元</View></View>
                            :
                            <View className="pay-box-free">载客测试免费</View>
src/robotaxi/pages/begin-route/hooks/use-order-actions.ts
@@ -3,6 +3,7 @@
import { saveOrder, getOrderPayment, cancelOrder, endOrder, routeStaleCheck } from '../../../api/begin-route';
import { checkOtp } from '../../../../utils/tools';
import { jumpTo } from '../../../../utils/jump';
import { subscribeAfterOrderSuccess } from '../../../../utils/subscribe';
import { TripStatus } from '../constants/trip-status';
type Deps = {
@@ -211,6 +212,7 @@
      deps.setOrderId(nextOrderId);
      deps.setTripStatus(TripStatus.Publishing);
      deps.bindSocketListeners();
      void subscribeAfterOrderSuccess(nextOrderId);
    } catch (error) {
      stopCallSubmitting();
    }
src/robotaxi/pages/select-map-site/index.tsx
@@ -16,6 +16,7 @@
import noStar from '../../../robotaxi/assets/no-star.png'
import {
  findNearestMarker,
  getAutoScaleBySites,
  getDistance,
  formatGeofencePolygons,
  fetchMapData,
@@ -32,10 +33,13 @@
import { jumpTo } from '../../../utils/jump';
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[]>([]); // 标记点列表
@@ -60,6 +64,7 @@
  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();
@@ -191,6 +196,17 @@
      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 {
@@ -540,7 +556,7 @@
        id="map"
        longitude={longitude}
        latitude={latitude}
        scale={14}
        scale={mapScale}
        markers={markers.map(m => ({ ...m, title: '', name: '' }))}
        polyline={polyline}
        polygons={polygons}
src/utils/Robotaxi-public-method.ts
@@ -36,6 +36,38 @@
};
/**
 * 根据站点空间分布估算推荐缩放等级(站点越分散,缩放越小)。
 */
export const getAutoScaleBySites = (sites: any[]) => {
  if (!Array.isArray(sites) || sites.length === 0) return null;
  if (sites.length === 1) return 16;
  let minLat = Number.POSITIVE_INFINITY;
  let maxLat = Number.NEGATIVE_INFINITY;
  let minLng = Number.POSITIVE_INFINITY;
  let maxLng = Number.NEGATIVE_INFINITY;
  sites.forEach((site) => {
    const lat = Number(site?.latitude);
    const lng = Number(site?.longitude);
    if (!Number.isFinite(lat) || !Number.isFinite(lng)) return;
    if (lat < minLat) minLat = lat;
    if (lat > maxLat) maxLat = lat;
    if (lng < minLng) minLng = lng;
    if (lng > maxLng) maxLng = lng;
  });
  if (!Number.isFinite(minLat) || !Number.isFinite(maxLat) || !Number.isFinite(minLng) || !Number.isFinite(maxLng)) {
    return null;
  }
  const diagonalMeters = getDistance(minLat, minLng, maxLat, maxLng);
  if (diagonalMeters > 12000) return 11;
  if (diagonalMeters > 8000) return 12;
  if (diagonalMeters > 5000) return 13;
  if (diagonalMeters > 3000) return 14;
  if (diagonalMeters > 1500) return 15;
  if (diagonalMeters > 800) return 16;
  return 17;
};
/**
 * 计算点 P 在线段 AB 上的投影点(吸附算法)
 * @param p 真实点 {latitude, longitude}
 * @param a 线段起点 {latitude, longitude}
src/utils/subscribe.ts
@@ -6,6 +6,11 @@
import Taro from "@tarojs/taro";
import { checkOtp } from "./tools";
import {
  OPERATION_COUPON_SUBSCRIBE_CONFIG,
  ORDER_SUCCESS_SUBSCRIBE_CONFIG,
  SubscribeSceneConfig
} from "../robotaxi/constants/subscribe";
// 需要频控的订阅字段,用于清空订阅次数
const subscribeFields = [
@@ -26,9 +31,8 @@
export async function addSubscribeCount(type, tmplIds, limit = 3, needlogin = true) {
  const isLogin = await checkOtp();
  if (!isLogin && needlogin) return false;
  const userInfo = Taro.getStorageSync(type);
  console.log(userInfo, "userInfo");
  Taro.setStorageSync(type, userInfo + 1 || 1);
  const userInfo = Number(Taro.getStorageSync(type) || 0);
  Taro.setStorageSync(type, userInfo + 1);
  return new Promise((resolve, reject) => {
    Taro.getSetting({
      withSubscriptions: true,
@@ -83,4 +87,48 @@
      return true;
    }
  })
}
}
// 生成订阅频控 key
export function buildSubscribeType(sceneKey: string, bizId?: string | number) {
  if (bizId === undefined || bizId === null || bizId === '') {
    return sceneKey;
  }
  return `${sceneKey}_${bizId}`;
}
// 执行通用订阅流程(失败/拒绝不影响主流程)
export async function runSubscribeScene(config: SubscribeSceneConfig, bizId?: string | number) {
  try {
    const type = buildSubscribeType(config.sceneKey, bizId);
    const canSubscribe = await addSubscribeCount(type, config.tmplIds, config.limit, config.needLogin);
    if (!canSubscribe) return;
    await new Promise<void>((resolve) => {
      Taro.requestSubscribeMessage({
        tmplIds: config.tmplIds,
        success: (result) => {
          checkSubscribeStatus(type, config.tmplIds, result, config.limit);
          resolve();
        },
        fail: () => {
          resolve();
        },
        complete: () => {
          resolve();
        }
      });
    });
  } catch (error) {
  }
}
// 下单成功后订阅消息
export async function subscribeAfterOrderSuccess(orderId: string) {
  if (!orderId) return;
  await runSubscribeScene(ORDER_SUCCESS_SUBSCRIBE_CONFIG, orderId);
}
// 运营活动 + 券效期 + 券到账三合一模板订阅
export async function subscribeOperationCouponGroup(bizId?: string | number) {
  await runSubscribeScene(OPERATION_COUPON_SUBSCRIBE_CONFIG, bizId);
}