hejianhao
2026-03-06 b8f3899ffaf7ef471899bc06a178ed8f352bc201
feat(robotaxi): 优化站点选择、订单详情及地图交互

- 将站点搜索页重定向至地图选点页,提升用户体验
- 在订单详情页显示格式化价格与行程信息,避免显示"0"
- 优化乘客须知弹窗,使用ScrollView支持滚动并修复遮罩层事件
- 简化定价规则页面逻辑,直接使用API返回的HTML内容
- 实现图片预加载工具函数,提升小程序中图片加载性能
- 添加发布行程超时重新估价机制(10分钟)
- 修复地图选点页距离校验逻辑,使用用户当前位置而非地图中心
- 调整地图路线绘制样式,移除不必要的箭头图标
- 优化等待弹窗图片加载逻辑,统一使用预加载函数
- 修复提示弹窗关闭逻辑,点击关闭按钮时返回首页
9个文件已修改
188 ■■■■■ 已修改文件
src/robotaxi/pages/begin-route/components/css-folder/passenger-notice.less 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/passenger-notice.tsx 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/release-route.tsx 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/index.tsx 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/order/detail/index.tsx 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/pricing-rules/index.tsx 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/select-map-site/components/tip-popup.tsx 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/select-map-site/index.tsx 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/Robotaxi-public-method.ts 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/css-folder/passenger-notice.less
@@ -39,8 +39,8 @@
            line-height: 48px;
            letter-spacing: normal;
            color: rgba(0, 0, 0, 0.9);
            max-height: 342px;
            overflow: auto;
            height: 342px;
            overflow: hidden;
        }
        &-checkbox {
@@ -70,4 +70,4 @@
            color: #FFFFFF;
        }
    }
}
}
src/robotaxi/pages/begin-route/components/passenger-notice.tsx
@@ -1,5 +1,5 @@
import Taro from '@tarojs/taro';
import { View, Text, Image, RichText } from '@tarojs/components';
import { View, Text, Image, RichText, ScrollView } from '@tarojs/components';
import { useEffect, useState } from 'react';
import './css-folder/passenger-notice.less'
import { getPrivacyContent } from '../../../api/home';
@@ -50,13 +50,13 @@
    if (!visible) return null;
    return (
        <View className='passenger-popup' catchMove>
            <View className='passenger-mask'></View>
        <View className='passenger-popup'>
            <View className='passenger-mask' catchMove></View>
            <View className='passenger-content' animation={animationData}>
                <View className='passenger-content-title'>乘车需知</View>
                <View className='passenger-content-desc'>
                <ScrollView className='passenger-content-desc' scrollY enhanced lowerThreshold={30} upperThreshold={30} showScrollbar={false}>
                    <RichText nodes={noticeContent} />
                </View>
                </ScrollView>
                <View onClick={() => setChecked(!checked)} className='passenger-content-checkbox'>
                    {
                        checked ?
src/robotaxi/pages/begin-route/components/release-route.tsx
@@ -18,7 +18,7 @@
    const toSeleteSitePage = (type: string) => {
        Taro.navigateTo({
            url: '/robotaxi/pages/site-search/index?pageType=' + type
            url: '/robotaxi/pages/select-map-site/index?pageType=' + type,
        })
    }
    const toPriceDescription = () => {
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 } from '../../../utils/Robotaxi-public-method';
import { getWalkingRoute, formatDurationToClock, formatDateTime, clearOrderTripStorage, preloadImageToTemp } from '../../../utils/Robotaxi-public-method';
import {
  saveOrder,
  queryRouteAndFare,
@@ -35,6 +35,7 @@
export default function SelectMapSite() {
  const WAITING_TIMER_START_KEY = 'waitingTimerStartAt';
  const WAITING_TIMER_ELAPSED_KEY = 'waitingTimerElapsed';
  const REQUOTE_EXPIRE_SECONDS = 10 * 60; // 10分钟
  const [showCustomerService, setShowCustomerService] = useState(false); // 客服弹窗是否显示
  const [longitude, setLongitude] = useState<number | null>(null); // 经度
@@ -45,6 +46,7 @@
  const [endSite, setEndSite] = useState<any>({}); // 下车点
  const [tripStatus, setTripStatus] = useState<number>(Number(Taro.getStorageSync('tripStatus')) || 1); // 行程状态 1:发布行程 2:发布中 3:呼叫车中 4:车辆即将到达 5:车辆到达上车点 6:车辆行驶中 7:到达目的地 8:行程已结束 9:支付成功
  const [elapsedSeconds, setElapsedSeconds] = useState(0); // 等待计时秒数
  const requoteTriggeredRef = useRef(false);
  const [showCancelPopup, setShowCancelPopup] = useState(false); // 取消弹窗是否显示
  const [showAnnouncementsPopup, setShowAnnouncementsPopup] = useState(false); // 安全注意事项动画弹窗是否显示
  const [showOverRoutePopup, setShowOverRoutePopup] = useState(false); // 结束行程弹窗是否显示
@@ -62,8 +64,10 @@
  const [routeDuration, setRouteDuration] = useState<number | null>(null); // 路线时间
  const [vehicleInfo, setVehicleInfo] = useState<any>({}); // 车辆信息
  const [orderId, setOrderId] = useState<string>(''); // 订单ID
  const customCalloutIconUrl = 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/f9e285fc-4c09-404d-90f8-4bac178565d0.png';
  const waitingCalloutBgUrl = 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/f55ce277-68de-4615-9c7c-807f588afaac.png';
  const waitingRadarUrl = 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/63ff36fc-e84c-4078-a53b-467870645105.webp';
  const [customCalloutIconSrc, setCustomCalloutIconSrc] = useState(customCalloutIconUrl);
  const [waitingCalloutBgSrc, setWaitingCalloutBgSrc] = useState(waitingCalloutBgUrl);
  const [waitingRadarSrc, setWaitingRadarSrc] = useState(waitingRadarUrl);
  const [waitingRadarStyle, setWaitingRadarStyle] = useState<any>({});
@@ -113,34 +117,28 @@
  // 加载等待弹窗图片
  useEffect(() => {
    if (![2, 3].includes(tripStatus)) return;
    if (Taro.getEnv?.() !== Taro.ENV_TYPE.WEAPP) {
      return;
    }
    let cancelled = false;
    const loadImage = async (url: string, setter: (value: string) => void) => {
      try {
        const res = await Taro.downloadFile({ url });
        if (cancelled) {
          return;
        }
        if (res?.statusCode === 200 && res.tempFilePath) {
          setter(res.tempFilePath);
          return;
        }
        const info = await Taro.getImageInfo({ src: url });
        if (!cancelled && info?.path) {
          setter(info.path);
        }
      } catch (error) {
      }
    };
    loadImage(waitingCalloutBgUrl, setWaitingCalloutBgSrc);
    loadImage(waitingRadarUrl, setWaitingRadarSrc);
    const load = async (url: string, setter: (value: string) => void) => {
      const local = await preloadImageToTemp(url);
      if (!cancelled) setter(local);
    }
    load(waitingCalloutBgUrl, setWaitingCalloutBgSrc);
    load(waitingRadarUrl, setWaitingRadarSrc);
    return () => {
      cancelled = true;
    };
  }, [tripStatus]);
  // 页面初始化预下载自定义气泡图标
  useEffect(() => {
    let cancelled = false;
    const run = async () => {
      const local = await preloadImageToTemp(customCalloutIconUrl);
      if (!cancelled) setCustomCalloutIconSrc(local);
    };
    run();
    return () => { cancelled = true; };
  }, []);
  // 更新等待雷达位置
  const updateWaitingRadarPosition = () => {
    if (!shouldShowWaitingRadar) {
@@ -501,6 +499,30 @@
    Taro.setStorageSync('tripStatus', tripStatus);
    tripStatusRef.current = tripStatus;
    let timer: any = null;
    let requoteTimer: any = null;
    // tripStatus=1:开启发布行程停留计时,用于10分钟后提示需重新估价
    if (tripStatus == 1) {
      requoteTriggeredRef.current = false;
      const startAt = Date.now();
      const updateRequoteElapsed = () => {
        const nextElapsed = Math.max(0, Math.floor((Date.now() - startAt) / 1000));
        if (nextElapsed >= REQUOTE_EXPIRE_SECONDS) {
          if (!requoteTriggeredRef.current) {
            requoteTriggeredRef.current = true;
            const s = Taro.getStorageSync('startSite') || {};
            const e = Taro.getStorageSync('endSite') || {};
            if (s?.latitude && s?.longitude && e?.latitude && e?.longitude) {
              requestRouteAndFare(s.latitude, s.longitude, e.latitude, e.longitude, true);
            }
          }
        }
      };
      updateRequoteElapsed();
      requoteTimer = setInterval(updateRequoteElapsed, 1000);
    } else {
      requoteTriggeredRef.current = false;
    }
    // tripStatus=2/3:等待计时
    if (tripStatus == 2 || tripStatus == 3) {
      const s = Taro.getStorageSync('startSite');
      if (s && s.latitude && s.longitude) {
@@ -588,6 +610,7 @@
    }
    return () => {
      if (timer) clearInterval(timer);
      if (requoteTimer) clearInterval(requoteTimer);
    };
  }, [tripStatus]);
@@ -677,6 +700,7 @@
          points: routePoints,
          color: '#7B91FF',
          width: 10,
          dottedLine: false,
          arrowLine: true,
        });
      }
@@ -686,7 +710,7 @@
          color: '#7B91FF',
          width: 6,
          dottedLine: true,
          arrowLine: true,
          arrowLine: false,
        });
      }
      setPolyline(lines);
@@ -1092,7 +1116,7 @@
                  <CoverView className="custom-callout-content">
                    {item.name}
                    {
                      tripStatus == 1 && <CoverImage className="custom-callout-icon" src='https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/f9e285fc-4c09-404d-90f8-4bac178565d0.png' />
                      tripStatus == 1 && <CoverImage className="custom-callout-icon" src={customCalloutIconSrc} />
                    }
                  </CoverView>
                }
src/robotaxi/pages/order/detail/index.tsx
@@ -155,10 +155,10 @@
      <View className='trip-info'>
        <View className='trip-top'>
          <View className='price-card'>
            <Text className='price'>{orderData.payMoney || 0}</Text>
            <Text className='price'>{orderData.payMoney || '-'}元</Text>
            <Text className='unit'>元</Text>
            {orderData.orderMoney > orderData.payMoney && (
              <Text className='old-price'>{orderData.orderMoney || 0}元</Text>
              <Text className='old-price'>{orderData.orderMoney || '-'}元</Text>
            )}
          </View>
          <View onClick={todescription} className='price-remark'>
@@ -181,7 +181,7 @@
        </View>
        {/* 行程信息 */}
        <View className='trip-line'>
          <View className='line-name'>全程{orderData.totalMileage || 0}公里|{orderData.orderTime || 0}分钟</View>
          <View className='line-name'>全程{orderData.totalMileage || '-'}公里|{orderData.orderTime || '-'}分钟</View>
          <View className='line-address'>
            <View className='line-icon first-icon'></View>
            <Text className='line-text'>{orderData.startAddress}</Text>
src/robotaxi/pages/pricing-rules/index.tsx
@@ -5,10 +5,8 @@
import './index.less';
export default function PricingRulesPage() {
  const [pricingData, setPricingData] = useState<any>(null);
  const [loading, setLoading] = useState(true);
  const [pricingHtml, setPricingHtml] = useState<string>('');
  const [pricingNodes, setPricingNodes] = useState<any[]>([]);
  useEffect(() => {
    const cachedCity = Taro.getStorageSync('selectedCity');
@@ -20,55 +18,11 @@
    }
  }, []);
  const extractHtmlFromData = (data: any): string => {
    if (!data) return '';
    if (typeof data === 'string') return data;
    const candidates = ['html', 'content', 'richText', 'rich_text', 'data'];
    for (const key of candidates) {
      const val = (data as any)?.[key];
      if (typeof val === 'string') return val;
    }
    return '';
  };
  const parseHtmlToNodes = async (html: string) => {
    if (!html) {
      setPricingNodes([]);
      return;
    }
    try {
      const { parse } = await import('mini-html-parser2');
      parse(html, (_err: any, nodes: any[]) => {
        const enhanceNodes = (ns: any[]): any[] =>
          ns.map((n: any) => {
            const node = { ...n };
            if (node.name === 'img') {
              node.attrs = {
                ...(node.attrs || {}),
                style: `${node.attrs?.style || ''};max-width:100%;height:auto;display:block;`
              };
            }
            if (Array.isArray(node.children) && node.children.length > 0) {
              node.children = enhanceNodes(node.children);
            }
            return node;
          });
        setPricingNodes(enhanceNodes(nodes || []));
      });
    } catch {
      setPricingNodes([]);
    }
  };
  const fetchPricingRules = async (cityId: string) => {
    try {
      setLoading(true);
      const res: any = await getPricingRules(cityId);
      const data = res?.data ?? res;
      setPricingData(data);
      const html = extractHtmlFromData(data);
      setPricingHtml(html);
      await parseHtmlToNodes(html);
      setPricingHtml(res);
    } catch (error) {
      console.error('获取定价规则失败', error);
      Taro.showToast({
@@ -90,7 +44,7 @@
    );
  }
  if ((!pricingHtml || pricingHtml.trim().length === 0) && (!pricingNodes || pricingNodes.length === 0)) {
  if (!pricingHtml || pricingHtml.trim().length === 0) {
    return (
      <View className='pricing-rules-page'>
        <View className='no-data'>
@@ -103,7 +57,7 @@
  return (
    <View className='pricing-rules-page'>
      <View className='rich-text-wrapper'>
        <RichText nodes={pricingNodes && pricingNodes.length > 0 ? pricingNodes : pricingHtml} />
        <RichText nodes={pricingHtml} />
      </View>
    </View>
  );
src/robotaxi/pages/select-map-site/components/tip-popup.tsx
@@ -36,12 +36,18 @@
        }
    }, [show]);
    const toHome = () => {
        Taro.reLaunch({
            url: '/pages/robotaxi/home/index'
        })
    }
    if (!visible) return null;
    return (
        <View className='tip-popup' catchMove>
            <View className='tip-mask'></View>
            <View className='tip-content' animation={animationData}>
                <Image onClick={onClose} className='close-icon' src={require('../../../assets/close-icon.png')} />
                <Image onClick={toHome} className='close-icon' src={require('../../../assets/close-icon.png')} />
                <View className='tip-title'>{tipType === '1' ? '上车点与目的地距离较近,建议步行前往' : '当前位置距上车点较远,暂不支持叫车,请重新选择上车点'}</View>
                <View onClick={onClose} className='reselect-btn'>重新选点</View>
            </View>
src/robotaxi/pages/select-map-site/index.tsx
@@ -270,9 +270,10 @@
        })
        return
      }
      // 校验当前位置与选择的上车点距离
      if (longitude && latitude) {
        const distance = getDistance(latitude, longitude, selectedSite.latitude, selectedSite.longitude);
      if (userLatitude && userLongitude) {
        const distance = getDistance(userLatitude, userLongitude, selectedSite.latitude, selectedSite.longitude);
        if (distance > distanceConfig.maxDistance) {
          setTipType('2');
          setShowTipPopup(true);
@@ -313,8 +314,8 @@
  // 关闭提示弹窗
  const handleCloseTipPopup = () => {
    setTipType('');
    setShowTipPopup(false);
    setTipType('');
  }
  // 切换子站点
@@ -352,9 +353,7 @@
        color: '#6D7FD9',
        width: 6,
        dottedLine: true,
        arrowLine: true,
        arrowIconPath: lineRight,
        borderColor: '#FFFFFF',
        arrowLine: false,
      }];
      setPolyline(line);
    }).catch(() => {
src/utils/Robotaxi-public-method.ts
@@ -198,6 +198,25 @@
  }
}
// 预下载图片,返回本地临时路径(WeApp),失败或非WeApp环境返回原始URL
export const preloadImageToTemp = async (url: string): Promise<string> => {
  try {
    if (typeof Taro.getEnv !== 'function' || Taro.getEnv() !== Taro.ENV_TYPE.WEAPP) {
      return url;
    }
    const res = await Taro.downloadFile({ url });
    if (res?.statusCode === 200 && res.tempFilePath) {
      return res.tempFilePath;
    }
    const info = await Taro.getImageInfo({ src: url });
    if (info?.path) {
      return info.path;
    }
  } catch (e) {
  }
  return url;
}
// 获取地图数据(围栏检查+附近站点)
export const fetchMapData = async (lat: number, lng: number) => {
  try {