hejianhao
2026-03-12 18c17646a99974f437d3bd50562d7b782d234667
feat(robotaxi): 优化地图显示、优惠券详情及行程时间格式化

- 为Android设备添加车辆位置气泡的自适应定位和样式
- 在CouponDetail接口和优惠券详情页中增加couponDesc字段,支持富文本渲染
- 使用formatTripDuration函数格式化订单详情页的行程时间显示
- 在站点搜索页中统一使用formatDistance函数格式化距离显示
- 优化地图视口的内边距计算,使其适配不同安全区域和路线点
- 将release-route组件中的URL拼接改为模板字符串
- 为自定义气泡添加white-space: nowrap样式防止文本换行
- 新增computeAdaptiveBottomPadding工具函数用于计算自适应地图底部内边距
8个文件已修改
249 ■■■■■ 已修改文件
src/robotaxi/api/coupon.ts 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/release-route.tsx 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/index.less 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/index.tsx 149 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/coupon/index.tsx 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/order/detail/index.tsx 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/site-search/index.tsx 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/Robotaxi-public-method.ts 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/api/coupon.ts
@@ -16,6 +16,7 @@
    validTimeRange: string;
    applicableCities: string[];
    usageInstructions: string;
    couponDesc: string;
}
export interface GetCouponListPayload {
src/robotaxi/pages/begin-route/components/release-route.tsx
@@ -18,7 +18,7 @@
    const toSeleteSitePage = (type: string) => {
        Taro.navigateTo({
            url: '/robotaxi/pages/select-map-site/index?pageType=' + type,
            url: `/robotaxi/pages/select-map-site/index?pageType=${type}`,
        })
    }
    const toPriceDescription = () => {
src/robotaxi/pages/begin-route/index.less
@@ -97,6 +97,7 @@
.custom-callout-content {
    display: flex;
    align-items: center;
    white-space: nowrap;
    border-radius: 16px;
    background: rgba(255, 255, 255, 1);
    font-size: 24px;
@@ -128,6 +129,11 @@
    height: 32px;
    flex-shrink: 0;
    display: block;
}
.car-callout-android {
    z-index: 9;
    pointer-events: none;
}
.waiting-callout {
@@ -201,7 +207,10 @@
    font-weight: 400;
    background: rgba(255, 255, 255, 1);
    padding: 8px 20px;
    padding-right: 6px;
    line-height: 48px;
    display: flex;
}
}
.pr-6 {
    padding-right: 6px;
}
src/robotaxi/pages/begin-route/index.tsx
@@ -19,7 +19,7 @@
import suggest from '../../../robotaxi/assets/suggest.png'
import originIcon from '../../../robotaxi/assets/origin-icon.png'
import wsManager from '../../../utils/websocket';
import { getWalkingRoute, formatDurationToClock, formatDateTime, clearOrderTripStorage, preloadImageToTemp, debounce } from '../../../utils/Robotaxi-public-method';
import { getWalkingRoute, formatDurationToClock, formatDateTime, clearOrderTripStorage, preloadImageToTemp, debounce, computeAdaptiveBottomPadding } from '../../../utils/Robotaxi-public-method';
import {
  saveOrder,
  queryRouteAndFare,
@@ -79,6 +79,9 @@
  const [evaluateSubmitVersion, setEvaluateSubmitVersion] = useState(0);
  const [evaluateCloseVersion, setEvaluateCloseVersion] = useState(0);
  const [originOpacity, setOriginOpacity] = useState(1);
  const [mapBottomPadding, setMapBottomPadding] = useState(280);
  const [carCalloutStyle, setCarCalloutStyle] = useState<any>({});
  const [carCalloutVisible, setCarCalloutVisible] = useState(false);
  const markersRef = useRef<any[]>([]);
  const tripStatusRef = useRef(tripStatus); // 行程状态引用
@@ -242,10 +245,109 @@
      .exec();
  };
  const updateCarCalloutPosition = () => {
    if (!isAndroid) {
      return;
    }
    if (!vehicleLocation || ![4, 6].includes(tripStatus)) {
      setCarCalloutVisible(false);
      return;
    }
    const mapCtx = Taro.createMapContext('map');
    const resolveRect = (rect: any) => {
      if (!rect) {
        return;
      }
      mapCtx.getRegion({
        success: (res) => {
          const sw = res?.southwest;
          const ne = res?.northeast;
          const latRange = ne?.latitude - sw?.latitude;
          const lngRange = ne?.longitude - sw?.longitude;
          if (!latRange || !lngRange) {
            return;
          }
          const x = ((vehicleLocation.longitude - sw.longitude) / lngRange) * rect.width;
          const y = ((ne.latitude - vehicleLocation.latitude) / latRange) * rect.height;
          setCarCalloutStyle({
            position: 'fixed',
            left: `${rect.left + x}px`,
            top: `${rect.top + y}px`,
            transform: 'translate(-50%, -120%)'
          });
          setCarCalloutVisible(true);
        }
      });
    };
    if (mapRectRef.current) {
      resolveRect(mapRectRef.current);
      return;
    }
    Taro.createSelectorQuery()
      .select('#map')
      .boundingClientRect((rect) => {
        if (rect) {
          mapRectRef.current = rect;
        }
        resolveRect(rect);
      })
      .exec();
  };
  // 更新等待雷达位置
  useEffect(() => {
    updateWaitingRadarPosition();
  }, [shouldShowWaitingRadar, startSite?.latitude, startSite?.longitude, longitude, latitude]);
  useEffect(() => {
    updateCarCalloutPosition();
  }, [isAndroid, vehicleLocation?.latitude, vehicleLocation?.longitude, tripStatus]);
  const resolveSafeBottomInset = () => {
    try {
      const info = Taro.getSystemInfoSync();
      const screenHeight = info?.screenHeight;
      const safeBottom = info?.safeArea?.bottom;
      if (Number.isFinite(screenHeight) && Number.isFinite(safeBottom)) {
        return Math.max(0, screenHeight - safeBottom);
      }
    } catch (error) {
      return 0;
    }
    return 0;
  };
  const resolveSafeTopInset = () => {
    try {
      const info = Taro.getSystemInfoSync();
      const safeTop = info?.safeArea?.top;
      if (Number.isFinite(safeTop)) {
        return Math.max(0, safeTop);
      }
    } catch (error) {
      return 0;
    }
    return 0;
  };
  const updateMapViewportPadding = () => {
    Taro.nextTick(() => {
      Taro.createSelectorQuery()
        .select('.container')
        .boundingClientRect((rect) => {
          const safeInset = resolveSafeBottomInset();
          const measured = rect?.height ? rect.height + safeInset + 16 : 0;
          const nextPadding = Math.max(280, Math.round(measured));
          setMapBottomPadding(nextPadding);
        })
        .exec();
    });
  };
  const computeBottomPadding = (points: any[], viewportType: string) => {
    const basePadding = viewportType === 'order_detail' ? Math.max(200, mapBottomPadding) : mapBottomPadding;
    return computeAdaptiveBottomPadding(points, basePadding);
  };
  // 应用路线点
  const applyRoutePoints = (points: any[]) => {
@@ -373,9 +475,11 @@
          const mapCtx = Taro.createMapContext('map');
          const allPoints = [...points, ...walkingPoints];
          if (allPoints.length > 0) {
            const topPadding = Math.max(24, resolveSafeTopInset() + 32);
            const bottomPadding = computeBottomPadding(allPoints, 'route_walking');
            mapCtx.includePoints({
              points: allPoints,
              padding: [100, 100, 280, 100]
              padding: [topPadding, 100, bottomPadding, 100]
            });
          }
        }
@@ -905,7 +1009,12 @@
    if (nextKey === viewportKeyRef.current) return;
    viewportKeyRef.current = nextKey;
    const mapCtx = Taro.createMapContext('map');
    const padding = viewportType === 'order_detail' ? [50, 50, 200, 50] : [100, 100, 280, 100];
    const bottomPadding = computeBottomPadding(points, viewportType);
    const baseTopPadding = viewportType === 'order_detail' ? 60 : 24;
    const topPadding = Math.max(baseTopPadding, resolveSafeTopInset() + 32);
    const padding = viewportType === 'order_detail'
      ? [topPadding, 50, bottomPadding, 50]
      : [topPadding, 100, bottomPadding, 100];
    Taro.nextTick(() => {
      mapCtx.includePoints({
        points,
@@ -915,7 +1024,7 @@
    if (tripStatus === 6 && viewportType === 'route') {
      tripStatus6ViewportLockedRef.current = true;
    }
  }, [tripStatus, statusRule, routePoints, walkingPoints, startSite?.latitude, startSite?.longitude, endSite?.latitude, endSite?.longitude, vehicleLocation, longitude, latitude]);
  }, [tripStatus, statusRule, routePoints, walkingPoints, startSite?.latitude, startSite?.longitude, endSite?.latitude, endSite?.longitude, vehicleLocation, longitude, latitude, mapBottomPadding]);
  useEffect(() => {
    if (!statusRule.translateCar) return;
@@ -1019,6 +1128,7 @@
    }
    ensureSocketListeners();
    updateMapViewportPadding();
  });
  useEffect(() => {
@@ -1033,6 +1143,10 @@
    }
  }, [startSite?.longitude, startSite?.latitude, endSite?.longitude, endSite?.latitude, longitude, latitude]);
  useEffect(() => {
    updateMapViewportPadding();
  }, [tripStatus]);
  // 获取用户位置信息
@@ -1274,7 +1388,7 @@
  const toSelectSite = (pageType: 'start' | 'end') => {
    Taro.navigateTo({
      url: `/robotaxi/pages/site-search/index?pageType=${pageType}`
      url: `/robotaxi/pages/site-search/index?pageType=${pageType}&longitude=${startSite.longitude}&latitude=${startSite.latitude}`
    })
  }
@@ -1326,11 +1440,13 @@
  const handleRegionChange = (e) => {
    if (e?.type === 'begin') {
      setWaitingRadarVisible(false);
      setCarCalloutVisible(false);
      return;
    }
    if (e?.type === 'end') {
      setWaitingRadarVisible(true);
      updateWaitingRadarPosition();
      updateCarCalloutPosition();
    }
  };
@@ -1340,6 +1456,7 @@
    } else {
      setOriginOpacity(1);
    }
    updateMapViewportPadding();
  };
  const durationDisplay = formatDurationDisplay(routeDuration);
@@ -1373,9 +1490,12 @@
                      <CoverImage className="waiting-callout-bg-img" src={waitingCalloutBgSrc} />
                      <CoverView className="waiting-callout-bg-text">{formatDurationToClock(elapsedSeconds)}</CoverView>
                    </CoverView> */}
                    <CoverView className="waiting-callout-txt">
                    <CoverView className={`waiting-callout-txt ${!isAndroid && 'pr-6'}`}>
                      {item.name}
                      <CoverView style={{ color: 'transparent' }}>占</CoverView>
                      {
                        !isAndroid &&
                        <CoverView style={{ color: 'transparent' }}>占</CoverView>
                      }
                    </CoverView>
                  </CoverView>
                  <CoverImage className="waiting-radar" src={waitingRadarSrc} />
@@ -1383,6 +1503,9 @@
              )
            }
            if (item.name === '车辆位置' && isAndroid && [4, 6].includes(tripStatus)) {
              return null;
            }
            return (
              <CoverView slot="callout" marker-id={item.id} key={`${tripStatus}-${item.id}`} className="custom-callout">
                {
@@ -1418,6 +1541,18 @@
        {shouldShowWaitingRadar && waitingRadarVisible && (
          <Image className="waiting-radar-img" src={waitingRadarSrc} webp={true} style={waitingRadarStyle} />
        )}
        {isAndroid && carCalloutVisible && (
          <View className="car-callout-android" style={carCalloutStyle}>
            <View className="custom-callout-content" style={{ padding: '6px 10px' }}>
              <View className="car-time" style={{ alignItems: 'flex-end' }}>
                <View className="car-time-num">{routeDistance?.value ?? 0}</View>
                <View style={{ flexShrink: 0 }}>{routeDistance?.unit === 'm' ? '米' : '公里'}</View>
                <View style={{ marginLeft: '2.5px' }} className="car-time-num">{durationDisplay.value}</View>
                <View style={{ flexShrink: 0 }}>{durationDisplay.unit}</View>
              </View>
            </View>
          </View>
        )}
        {/* 返回按钮 */}
        <View onClick={toBack} className="select-city">
          <Image className="select-city-icon" src={backIcon} />
src/robotaxi/pages/coupon/index.tsx
@@ -1,6 +1,6 @@
import Taro, { useReachBottom } from '@tarojs/taro';
import { useState, useEffect } from 'react';
import { View, Text, Image } from '@tarojs/components';
import { View, Text, Image, RichText } from '@tarojs/components';
import './index.less';
import icon from '../../assets/Subtract.png'
import close from '../../assets/close-icon.png'
@@ -172,7 +172,7 @@
        <View className='privacy-content' animation={animationData}>
          {/* 头部:标题 + 关闭*/}
          <View className='privacy-header'>
            <Text className='title'>优惠券说明</Text>
            <Text className='title'>{currentCoupon.title}</Text>
            <Image className='close' src={close} onClick={() => closePopup()}></Image>
          </View>
          {/* 内容文案 */}
@@ -190,7 +190,7 @@
          </View>
          <View className='remark-card'>
            <Text className='remark-label'>使用说明</Text>
            <Text className='remark-value'>{currentCoupon.couponDesc}</Text>
            <Text className='remark-value'><RichText nodes={currentCoupon.couponDesc} /></Text>
          </View>
        </View>
src/robotaxi/pages/order/detail/index.tsx
@@ -45,6 +45,24 @@
    return { longitude: (minLng + maxLng) / 2, latitude: (minLat + maxLat) / 2 };
  };
  const formatTripDuration = (value: any) => {
    const num = Number(value);
    if (!Number.isFinite(num)) {
      return '-';
    }
    const safe = Math.max(0, Math.floor(num));
    const hours = Math.floor(safe / 3600);
    const minutes = Math.floor((safe % 3600) / 60);
    const seconds = safe % 60;
    if (hours > 0) {
      return `${String(hours).padStart(2, '0')}小时${String(minutes).padStart(2, '0')}分钟`;
    }
    if (safe < 60) {
      return `${String(seconds).padStart(2, '0')}秒`;
    }
    return `${String(minutes).padStart(2, '0')}分钟${String(seconds).padStart(2, '0')}秒`;
  };
  useEffect(() => {
    if (orderId) {
      fetchOrderDetail(orderId);
@@ -293,7 +311,7 @@
        <View className='trip-line'>
          {
            orderData.status != 0 &&
            <View className='line-name'>全程{orderData.totalMileage}公里|{orderData.orderTime}分钟</View>
            <View className='line-name'>全程{orderData.totalMileage}公里|{formatTripDuration(orderData.orderTime)}</View>
          }
          <View className='line-address'>
            <View className='line-icon first-icon'></View>
src/robotaxi/pages/site-search/index.tsx
@@ -47,7 +47,7 @@
      if (points.length > 0) {
        const list = points.map(item => ({
          ...item,
          distance: item.distance
          distance: formatDistance(item.distance)
        }));
        setNearbySiteList(list);
      } else {
@@ -149,7 +149,7 @@
                ...item,
                longitude: Number(locationArr[0]),
                latitude: Number(locationArr[1]),
                // distance: formatDistance(item.distance)
                distance: formatDistance(item.distance)
              }
            });
            setNonOperationalSite(list.slice(0, MAX_SHOW_COUNT));
@@ -374,7 +374,7 @@
                    <View className='history-name'>{item.name || '_'}</View>
                    <View className='history-address-box'>
                      <View className='history-address'>{item.address || '-'}</View>
                      <View className='history-distance'>{item.distance}km</View>
                      <View className='history-distance'>{item.distance}</View>
                    </View>
                  </View>
                </View>
@@ -403,7 +403,7 @@
                    {renderHighlightedName(item.name || '_')}
                    <View className='history-address-box'>
                      <View className='history-address'>{item.address || '-'}</View>
                      <View className='history-distance'>{item.distance}km</View>
                      <View className='history-distance'>{item.distance}</View>
                    </View>
                  </View>
                </View>
src/utils/Robotaxi-public-method.ts
@@ -23,11 +23,57 @@
  const d = Number(val);
  if (Number.isNaN(d)) return val;
  if (d >= 1000) {
    return (d / 1000).toFixed(2) + 'km';
    return (d / 1000).toFixed(1) + 'km';
  }
  return d.toFixed(2) + 'm';
  return d.toFixed(0) + 'm';
}
export const computeAdaptiveBottomPadding = (
  points: any[],
  basePadding: number,
  minPadding = 200,
  minAspect = 0.8,
  minRatio = 0.7
) => {
  if (!Array.isArray(points) || points.length < 2) {
    return basePadding;
  }
  let minLat = Number.POSITIVE_INFINITY;
  let maxLat = Number.NEGATIVE_INFINITY;
  let minLng = Number.POSITIVE_INFINITY;
  let maxLng = Number.NEGATIVE_INFINITY;
  for (const p of points) {
    const lat = Number(p?.latitude);
    const lng = Number(p?.longitude);
    if (!Number.isFinite(lat) || !Number.isFinite(lng)) {
      continue;
    }
    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 basePadding;
  }
  const latRange = maxLat - minLat;
  const lngRange = maxLng - minLng;
  if (latRange <= 0 || lngRange <= 0) {
    return basePadding;
  }
  const avgLat = (minLat + maxLat) / 2;
  const adjustedLngRange = lngRange * Math.cos((avgLat * Math.PI) / 180);
  if (!Number.isFinite(adjustedLngRange) || adjustedLngRange <= 0) {
    return basePadding;
  }
  const aspect = latRange / adjustedLngRange;
  if (aspect >= minAspect) {
    return basePadding;
  }
  const ratio = Math.max(minRatio, Math.min(1, minRatio + (aspect / minAspect) * (1 - minRatio)));
  return Math.max(minPadding, Math.round(basePadding * ratio));
};
// 查找最近的标记点
export const findNearestMarker = (lat: number, lng: number, markers: any[]) => {
  let minDistance = Infinity;