hejianhao
2026-02-06 f7d0660291650251a702e4f1b33e9a8dbfcf1b93
feat(robotaxi): 完善行程发布与评价功能,优化地图交互

- 新增上传图标资源用于评价弹窗
- 修复城市选择页面的标题错别字
- 重构地图选点页面,合并初始化逻辑并移除冗余图标
- 在行程发布组件中动态显示起终点站点名称
- 扩展车辆呼叫组件,支持打开评价弹窗并默认选中“很满意”
- 实现完整的评价弹窗,包含问题类型选择、描述输入和图片上传
- 优化城市选择页面的数据过滤和选中状态管理
- 重构行程开始页面,改进地图标记、路线规划和弹窗管理逻辑
- 增强主页地图交互,实现站点吸附、位置反编码和行程状态跳转
1个文件已添加
9个文件已修改
709 ■■■■■ 已修改文件
src/pages/robotaxi/home/index.tsx 237 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/upload-icon.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car.tsx 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/css-folder/evaluate-popup.less 187 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/evaluate-popup.tsx 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/release-route.tsx 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/index.tsx 129 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/select-city/index.config.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/select-city/index.tsx 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/select-map-site/index.tsx 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/index.tsx
@@ -28,6 +28,9 @@
  const [isMaintain, setIsMaintain] = useState(false); // 是否维护中
  const [isOperatingArea, setIsOperatingArea] = useState(false); // 是否在运营区域内
  const [tripStatus, setTripStatus] = useState<number>(0); // 行程状态
  const [currentSite, setCurrentSite] = useState<any>({}); // 当前站点
  const [currentAddress, setCurrentAddress] = useState(''); // 当前位置地址
  const [isAdsorbed, setIsAdsorbed] = useState(true); // 是否吸附到站点
  const [boxHeight, setBoxHeight] = useState<number | string>('auto'); // 活动区域高度
  const startY = useRef(0); // 拖动开始时的Y坐标
@@ -35,6 +38,7 @@
  const fullHeight = useRef(0); // 活动区域最大高度
  const minHeight = useRef(0); // 活动区域最小高度
  const isDragging = useRef(false); // 是否正在拖动
  const shouldIgnoreRegionChange = useRef(false); // 是否忽略地图区域变化
  const [selectedCity, setSelectedCity] = useState(''); // 选中的城市
@@ -56,6 +60,30 @@
    if (tripStatus) {
      setTripStatus(Number(tripStatus));
    }
    Taro.getSetting({
      success: (res) => {
        const auth = res.authSetting['scope.userLocation'];
        if (auth === false) {
          setIsLocationAuthDenied(true);
        } else {
          // 检查缓存中是否有 startSite
          const startSite = Taro.getStorageSync('startSite');
          if (startSite && startSite.latitude && startSite.longitude) {
            setLatitude(startSite.latitude);
            setLongitude(startSite.longitude);
            setCurrentSite(startSite);
            setIsAdsorbed(true);
            shouldIgnoreRegionChange.current = true;
          } else {
            getLocationInfo(true);
          }
        }
      },
      fail: (err) => {
        console.error('获取位置授权信息失败', err);
      }
    });
  });
  useEffect(() => {
@@ -68,7 +96,8 @@
        iconPath: 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/a2b43790-9317-4b3b-b29f-d82cc10830e9.png',
        width: 15,
        height: 15,
        anchor: { x: 0.5, y: 0.5 }
        anchor: { x: 0.5, y: 0.5 },
        name: '中国南方电网·2号门'
      },
      {
        id: 2,
@@ -77,7 +106,8 @@
        iconPath: 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/a2b43790-9317-4b3b-b29f-d82cc10830e9.png',
        width: 15,
        height: 15,
        anchor: { x: 0.5, y: 0.5 }
        anchor: { x: 0.5, y: 0.5 },
        name: '天河体育中心'
      },
      {
        id: 3,
@@ -86,7 +116,8 @@
        iconPath: 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/a2b43790-9317-4b3b-b29f-d82cc10830e9.png',
        width: 15,
        height: 15,
        anchor: { x: 0.5, y: 0.5 }
        anchor: { x: 0.5, y: 0.5 },
        name: '太古汇'
      },
      {
        id: 4,
@@ -95,7 +126,8 @@
        iconPath: 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/a2b43790-9317-4b3b-b29f-d82cc10830e9.png',
        width: 15,
        height: 15,
        anchor: { x: 0.5, y: 0.5 }
        anchor: { x: 0.5, y: 0.5 },
        name: '广州东站'
      },
      {
        id: 5,
@@ -104,7 +136,8 @@
        iconPath: 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/a2b43790-9317-4b3b-b29f-d82cc10830e9.png',
        width: 15,
        height: 15,
        anchor: { x: 0.5, y: 0.5 }
        anchor: { x: 0.5, y: 0.5 },
        name: '广州南站'
      }
    ];
    setMarkers(defaultMarkers);
@@ -134,31 +167,42 @@
        }
      }).exec();
    }, 500);
    Taro.getSetting({
      success: (res) => {
        const auth = res.authSetting['scope.userLocation'];
        if (auth === false) {
          setIsLocationAuthDenied(true);
        } else {
          getLocationInfo();
        }
      },
      fail: (err) => {
        console.error('获取位置授权信息失败', err);
      }
    });
  }, []);
  // 获取用户位置信息
  const getLocationInfo = () => {
  const getLocationInfo = (updateCenter = true) => {
    Taro.getLocation({ type: 'gcj02' })
      .then(res => {
        setLatitude(res.latitude);
        setLongitude(res.longitude);
        if (updateCenter) {
          setLatitude(res.latitude);
          setLongitude(res.longitude);
        }
        const myAmapFun = new amapFile.AMapWX({ key: AMAP_KEY });
        // 获取当前位置的逆地理编码
        myAmapFun.getRegeo({
          location: `${res.longitude},${res.latitude}`,
          success: (data) => {
            if (data && data[0]) {
              const address = data[0].name || data[0].desc;
              setCurrentAddress(address);
              setCurrentSite({
                name: address,
                latitude: res.latitude,
                longitude: res.longitude
              });
              setIsAdsorbed(false);
            }
          },
          fail: (info) => {
            console.error('getRegeo fail', info);
            setCurrentAddress('未知位置');
            setCurrentSite({ name: '未知位置' });
          }
        });
        myAmapFun.getPoiAround({
          location: `${res.latitude},${res.longitude}`,
          location: `${res.longitude},${res.latitude}`,
          success: () => { },
          fail: () => { }
        });
@@ -179,6 +223,7 @@
  // 返回当前位置
  const toOrigin = () => {
    shouldIgnoreRegionChange.current = false;
    const mapCtx = Taro.createMapContext('map');
    mapCtx.moveToLocation();
  }
@@ -189,36 +234,75 @@
    // 获取触发源: gesture (手势), update (接口调用)
    const causedBy = e?.causedBy || e?.detail?.causedBy;
    // 如果是用户手势拖动,则必须触发,且重置忽略标记
    if (causedBy === 'gesture' || causedBy === 'drag' || causedBy === 'scale') {
      shouldIgnoreRegionChange.current = false;
    } else {
      shouldIgnoreRegionChange.current = true;
      return
    }
    if (type === 'begin') {
      setShowToolTip(false);
    } else if (type === 'end') {
      setBounce(true);
      // 只有用户手势拖动触发的移动才进行吸附判断,避免 moveToLocation 触发死循环
      if (causedBy === 'gesture' || causedBy === 'drag') {
        const mapCtx = Taro.createMapContext('map');
        mapCtx.getCenterLocation({
          success: (res) => {
            const { latitude: lat, longitude: lng } = res;
            const { targetMarker, minDistance } = findNearestMarker(lat, lng, markers);
            // 如果最近的点在一定范围内(例如 200米),则吸附
            if (targetMarker && minDistance < 200) {
              // 使用 moveToLocation 实现吸附效果
              mapCtx.moveToLocation({
                latitude: targetMarker.latitude,
                longitude: targetMarker.longitude,
              });
            }
          }
        });
      // 如果标记为忽略且非手势触发,则直接返回
      if (shouldIgnoreRegionChange.current) {
        shouldIgnoreRegionChange.current = false;
        return;
      }
      setTimeout(() => {
        setBounce(false);
        setShowToolTip(true);
      }, 600);
      setBounce(true);
      const mapCtx = Taro.createMapContext('map');
      mapCtx.getCenterLocation({
        success: (res) => {
          const { latitude: lat, longitude: lng } = res;
          const { targetMarker, minDistance } = findNearestMarker(lat, lng, markers);
          // 只有用户手势拖动触发的移动才进行吸附判断,避免 moveToLocation 触发死循环
          if ((causedBy === 'gesture' || causedBy === 'drag' || causedBy === 'scale') && targetMarker && minDistance < 200) {
            // 使用 moveToLocation 实现吸附效果
            mapCtx.moveToLocation({
              latitude: targetMarker.latitude,
              longitude: targetMarker.longitude,
            });
          }
          // 根据距离决定是否显示 ToolTip
          setTimeout(() => {
            setBounce(false);
            if (minDistance < 200) {
              setIsAdsorbed(true);
              if (targetMarker) {
                setCurrentSite(targetMarker);
              }
              setShowToolTip(true);
            } else {
              setIsAdsorbed(false);
              setCurrentSite({ name: '正在获取位置...' });
              // 获取当前位置的逆地理编码
              const myAmapFun = new amapFile.AMapWX({ key: AMAP_KEY });
              myAmapFun.getRegeo({
                location: `${lng},${lat}`,
                success: (data) => {
                  if (data && data[0]) {
                    const address = data[0].name || data[0].desc;
                    setCurrentAddress(address);
                    setCurrentSite({ name: address, latitude: lat, longitude: lng });
                  }
                },
                fail: (info) => {
                  console.error('getRegeo fail', info);
                  setCurrentAddress('未知位置');
                  setCurrentSite({ name: '未知位置' });
                }
              });
              setShowToolTip(true);
            }
          }, 600);
        }
      });
    }
  }
@@ -286,6 +370,20 @@
    })
  }
  const toBeginRoute = () => {
    if (currentSite && currentSite.name) {
      Taro.setStorageSync('startSite', currentSite);
    }
    Taro.setStorageSync('endSite', {
      latitude: 30.571100,
      longitude: 104.065200,
      name: '中国南方电网·3号门'
    });
    Taro.navigateTo({
      url: '/robotaxi/pages/begin-route/index',
    })
  }
  return (
    <View className="home-container">
      <Map
@@ -313,17 +411,23 @@
        </View>
        {/* 指针 */}
        <View className="pointer-box">
          {showToolTip && (
            <View className="toolTip">
              <View className="toolTip-bg">
                <Text className="toolTip-bg-text">限停3分钟</Text>
          {
            showToolTip && (
              <View className="toolTip">
                {
                  isAdsorbed && (
                    <View className="toolTip-bg">
                      <Text className="toolTip-bg-text">限停3分钟</Text>
                    </View>
                  )
                }
                <View className="toolTip-txt">
                  <View className="toolTip-txt-content">{isAdsorbed ? currentSite.name : currentAddress}</View>
                  {isAdsorbed && <Image className="toolTip-icon" src={rightIcon} />}
                </View>
              </View>
              <View className="toolTip-txt">
                <View className="toolTip-txt-content">中国南方电网·2号门</View>
                <Image className="toolTip-icon" src={rightIcon} />
              </View>
            </View>
          )}
            )
          }
          <Image className={`pointer-icon ${bounce ? 'bounce' : ''}`} src={pointer} />
        </View>
        {/* 复位按钮 */}
@@ -370,9 +474,16 @@
            <View onClick={() => toSeleteSitePage('start')} className="now-site">
              <View className="now-site-dot"></View>
              <View className="now-site-address">
                <View className="now-site-address-content">
                  从<Text className="now-site-address-text">中国南方电网·2号门</Text>上车
                </View>
                {
                  currentSite.name ?
                    <View className="now-site-address-content">
                      从<Text className="now-site-address-text">{currentSite.name}</Text>出发
                    </View>
                    :
                    <View className="now-site-address-content">
                      <Text className="now-site-address-text">请选择上车点</Text>
                    </View>
                }
                <Image className="now-site-icon" src={rightIcon} />
              </View>
            </View>
@@ -381,7 +492,7 @@
              <View className="to-site-dot"></View>
              <View className="to-site-address">
                <View className="to-site-text">你想去哪?</View>
                <View className="to-site-fast">去:体育西·珠江新城</View>
                <View onClick={toBeginRoute} className="to-site-fast">去:中国南方电网·3号门</View>
              </View>
            </View>
          </View>
@@ -419,7 +530,7 @@
            {
              [1, 2].map(item => (
                <View key={item} className="activity-item">
                  <Image className="activity-img" src={'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/436c0306-d22c-4b60-bc1c-4cf01fe20f4b.png'} />
                  <Image mode="aspectFill" className="activity-img" src={'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/436c0306-d22c-4b60-bc1c-4cf01fe20f4b.png'} />
                </View>
              ))
            }
src/robotaxi/assets/upload-icon.png
src/robotaxi/pages/begin-route/components/calling-car.tsx
@@ -14,15 +14,16 @@
    handleOpenOverRoutePopup?: () => void;
    handlePay?: () => void;
    updateStatus?: () => void;
    handleOpenEvaluatePopup?: () => void;
}
export default function CallingCar({ tripStatus = 3, handleOpenCancelPopup, handleOpenOverRoutePopup, handlePay, updateStatus }: Props) {
export default function CallingCar({ tripStatus = 3, handleOpenCancelPopup, handleOpenOverRoutePopup, handlePay, updateStatus, handleOpenEvaluatePopup }: Props) {
    const [trunkStatus, setTrunkStatus] = useState(false);
    const [lockStatus, setLockStatus] = useState(false);
    const [tempStatus, setTempStatus] = useState(19);
    const [showDetail, setShowDetail] = useState(false);
    const [activeIndex, setActiveIndex] = useState(3) // 默认选中"很满意"
    const [activeIndex, setActiveIndex] = useState(0) // 默认选中"很满意"
    // 使用 useRef 保存最新的 updateStatus 回调,避免因父组件重绘导致定时器重置
    const updateStatusRef = useRef(updateStatus);
@@ -62,6 +63,14 @@
        6: { text: '行程已开始,请全程系好安全带', className: 'calling-car-content-desc' },
        7: { text: '倒计时结束时车辆将自动驶离,请带好物品安全下车', className: 'calling-car-content-desc' },
        8: { text: '订单待支付,请核对后完成支付', className: 'calling-car-content-desc' }
    }
    // 切换评价选项
    const changeEvaluate = (index: number) => {
        setActiveIndex(index);
        if (index == 1) {
            handleOpenEvaluatePopup?.()
        }
    }
    return (
@@ -240,15 +249,15 @@
                <View className='car-card-evaluate'>
                    <View className='car-card-evaluate-title'>您对本次行程满意吗?</View>
                    <View className='car-card-evaluate-content'>
                        <View className='car-card-evaluate-content-item' onClick={() => { setActiveIndex(1) }}>
                        <View className='car-card-evaluate-content-item' onClick={() => changeEvaluate(1)}>
                            <Image src={activeIndex == 1 ? activeBad : bad} className='car-card-evaluate-content-item-img' />
                            <Text className={`car-card-evaluate-content-item-text ${activeIndex == 1 ? 'active-text' : ''}`}>很不满意</Text>
                        </View>
                        <View className='car-card-evaluate-content-item' onClick={() => { setActiveIndex(2) }}>
                        <View className='car-card-evaluate-content-item' onClick={() => changeEvaluate(2)}>
                            <Image src={activeIndex == 2 ? activeNormal : normal} className='car-card-evaluate-content-item-img' />
                            <Text className={`car-card-evaluate-content-item-text ${activeIndex == 2 ? 'active-text' : ''}`}>一般般</Text>
                        </View>
                        <View className='car-card-evaluate-content-item' onClick={() => { setActiveIndex(3) }}>
                        <View className='car-card-evaluate-content-item' onClick={() => changeEvaluate(3)}>
                            <Image src={activeIndex == 3 ? activeGood : good} className='car-card-evaluate-content-item-img' />
                            <Text className={`car-card-evaluate-content-item-text ${activeIndex == 3 ? 'active-text' : ''}`}>很满意</Text>
                        </View>
src/robotaxi/pages/begin-route/components/css-folder/evaluate-popup.less
@@ -0,0 +1,187 @@
.evaluate-popup {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 99999;
    .evaluate-mask {
        position: absolute;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        background: rgba(0, 0, 0, 0.5);
    }
    .evaluate-content {
        position: absolute;
        bottom: 0;
        left: 0;
        right: 0;
        background: #ffffff;
        border-radius: 16px 16px 0 0;
        padding: 42px 0 88px 0;
        &-close {
            display: flex;
            justify-content: flex-end;
            padding-right: 34px;
            image {
                width: 36px;
                height: 36px;
            }
        }
        &-title {
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 24px;
            font-size: 32px;
            font-weight: 600;
            letter-spacing: normal;
            color: rgba(0, 0, 0, 0.9);
            image {
                width: 82px;
                height: 80px;
                flex-shrink: 0;
            }
        }
        .content-box {
            margin-top: 54px;
            padding: 0 49px;
            .evaluate-problem-type {
                &-title {
                    font-size: 24px;
                    line-height: 40px;
                    letter-spacing: normal;
                    color: rgba(0, 0, 0, 0.4);
                }
                &-content {
                    margin-top: 24px;
                    display: flex;
                    flex-wrap: wrap;
                    gap: 24px;
                    &-item {
                        padding: 0 40px;
                        height: 60px;
                        display: flex;
                        justify-content: center;
                        align-items: center;
                        border-radius: 8px;
                        background: #FFFFFF;
                        box-sizing: border-box;
                        border: 2px solid rgba(0, 0, 0, 0.2);
                        font-size: 28px;
                        line-height: 44px;
                        letter-spacing: normal;
                        color: rgba(0, 0, 0, 0.6);
                    }
                    .active-tag {
                        border-radius: 12px;
                        box-sizing: border-box;
                        border: 2px solid #000000;
                        color: rgba(0, 0, 0, 0.9);
                    }
                }
            }
            .evaluate-problem-desc-box {
                position: relative;
                .evaluate-problem-desc {
                    width: calc(100% - 48px);
                    margin-top: 48px;
                    border-radius: 16px;
                    background: rgba(0, 0, 0, 0.03);
                    padding: 24px;
                    padding-bottom: 16px;
                    font-size: 28px;
                    color: rgba(0, 0, 0, 0.9);
                    height: 196px;
                }
                .placeholder-class {
                    font-size: 28px;
                    line-height: 44px;
                    color: rgba(0, 0, 0, 0.25);
                }
                .evaluate-problem-desc-count {
                    position: absolute;
                    right: 24px;
                    bottom: 16px;
                    font-size: 20px;
                    line-height: 20px;
                    letter-spacing: normal;
                    color: rgba(0, 0, 0, 0.25);
                }
            }
            .evaluate-problem-pic-box {
                margin-top: 24px;
                display: flex;
                flex-wrap: wrap;
                gap: 10px;
                &-img {
                    flex-shrink: 0;
                    width: 208px;
                    height: 208px;
                }
                &-upload {
                    flex-shrink: 0;
                    width: 208px;
                    height: 208px;
                    display: flex;
                    flex-direction: column;
                    align-items: center;
                    justify-content: center;
                    gap: 8px;
                    border-radius: 16px;
                    background: rgba(0, 0, 0, 0.03);
                    &-img {
                        width: 48px;
                        height: 48px;
                    }
                    &-count {
                        font-size: 24px;
                        line-height: 40px;
                        letter-spacing: normal;
                        color: rgba(0, 0, 0, 0.25);
                    }
                }
            }
        }
        &-confirm {
            margin: 0 94px;
            margin-top: 40px;
            font-size: 30px;
            letter-spacing: normal;
            color: #FFFFFF;
            text-align: center;
            line-height: 96px;
            border-radius: 16px;
            background: rgba(0, 0, 0, 0.4);
        }
        .ok-btn {
            border-radius: 16px;
            background: linear-gradient(97deg, #8475FF 0%, #7DD6FF 103%);
        }
    }
}
src/robotaxi/pages/begin-route/components/evaluate-popup.tsx
@@ -1,5 +1,5 @@
import Taro from '@tarojs/taro';
import { View, Text, Image } from '@tarojs/components';
import { View, Text, Image, Textarea } from '@tarojs/components';
import { useEffect, useState } from 'react';
import './css-folder/evaluate-popup.less'
@@ -12,6 +12,8 @@
export default function EvaluatePopup({ show = false, onClose, onConfirm }: Props) {
    const [visible, setVisible] = useState(false);
    const [animationData, setAnimationData] = useState<any>();
    const [selectedTag, setSelectedTag] = useState('');
    const tags = ['乘车体验', '服务态度', '司机行为', '其他']
    useEffect(() => {
        if (show && !visible) {
@@ -36,12 +38,48 @@
        }
    }, [show]);
    const submit = () => {
        if (!selectedTag) return;
        onConfirm?.();
    }
    if (!visible) return null;
    return (
        <View className='cancel-popup' catchMove>
            <View className='cancel-mask'></View>
            <View className='cancel-content' animation={animationData}>
        <View className='evaluate-popup' catchMove onClick={(e) => e.stopPropagation()}>
            <View className='evaluate-mask'></View>
            <View className='evaluate-content' animation={animationData}>
                <View className='evaluate-content-close'>
                    <Image onClick={onClose} src={require('../../../assets/close-icon.png')} />
                </View>
                <View className='evaluate-content-title'>
                    <Image src={require('../../../assets/active-bad.png')} />
                    <View>“不满意,我要吐槽”</View>
                </View>
                <View className='content-box'>
                    <View className='evaluate-problem-type'>
                        <View className='evaluate-problem-type-title'>遇到的问题类型:</View>
                        <View className='evaluate-problem-type-content'>
                            {tags.map((item) => (
                                <View key={item} className={`evaluate-problem-type-content-item ${selectedTag === item && 'active-tag'}`} onClick={() => setSelectedTag(item)}>{item}</View>
                            ))}
                        </View>
                    </View>
                    <View className='evaluate-problem-desc-box'>
                        <Textarea className='evaluate-problem-desc' maxlength={500} placeholderClass='placeholder-class' placeholder='请描述您的建议或问题(可添加相关截图)' />
                        <View className='evaluate-problem-desc-count'>0/500</View>
                    </View>
                    <View className='evaluate-problem-pic-box'>
                        {/* <Image className='evaluate-problem-pic-box-img  /> */}
                        <View className='evaluate-problem-pic-box-upload'>
                            <Image className='evaluate-problem-pic-box-upload-img' src={require('../../../assets/upload-icon.png')} />
                            <View className='evaluate-problem-pic-box-upload-count'>0/9</View>
                        </View>
                    </View>
                </View>
                <View className='evaluate-content-confirm'>
                    <View className={`evaluate-content-confirm-btn ${selectedTag && 'ok-btn'}`} onClick={submit}>提交反馈</View>
                </View>
            </View>
        </View>
    )
src/robotaxi/pages/begin-route/components/release-route.tsx
@@ -6,7 +6,13 @@
import tipBlueIcon from '../../../assets/tip-blue-icon.png'
import tipDefaultIcon from '../../../assets/tip-default-icon.png'
export default function ReleaseRoute({ callImmediately }: { callImmediately: () => void }) {
type Props = {
    startSite: Site;
    endSite: Site;
    callImmediately: () => void;
}
export default function ReleaseRoute({ startSite, endSite, callImmediately }: Props) {
    const toSeleteSitePage = (type: string) => {
        Taro.navigateTo({
@@ -31,7 +37,7 @@
                            <View className="now-site-dot"></View>
                            <View className="now-site-address">
                                <View className="now-site-address-content">
                                    从<Text className="now-site-address-text">中国南方电网·2号门</Text>上车
                                    从<Text className="now-site-address-text">{startSite.name}</Text>上车
                                </View>
                                <Image className="now-site-icon" src={rightIcon} />
                            </View>
@@ -42,7 +48,7 @@
                    <View onClick={() => toSeleteSitePage('end')} className="to-site">
                        <View className="to-site-dot"></View>
                        <View className="to-site-address">
                            <View className="to-site-text">你想去哪?</View>
                            <View className="to-site-text">{endSite.name}</View>
                        </View>
                    </View>
                </View>
src/robotaxi/pages/begin-route/index.tsx
@@ -1,4 +1,4 @@
import Taro from "@tarojs/taro"
import Taro, { useDidShow } from "@tarojs/taro"
import { useEffect, useState } from 'react';
import { View, Map, Text, Image, CoverView, CoverImage } from '@tarojs/components';
import './index.less';
@@ -11,6 +11,7 @@
import AnnouncementsPopup from './components/announcements-popup';
import OverRoute from './components/over-route';
import PassengerNotice from './components/passenger-notice';
import EvaluatePopup from './components/evaluate-popup';
import backIcon from '../../../robotaxi/assets/back-icon.png'
import contactService from '../../../robotaxi/assets/contact-service.png'
@@ -32,11 +33,12 @@
  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 [showCancelPopup, setShowCancelPopup] = useState(true); // 取消弹窗是否显示
  const [showCancelPopup, setShowCancelPopup] = useState(false); // 取消弹窗是否显示
  const [showAnnouncementsPopup, setShowAnnouncementsPopup] = useState(false); // 安全注意事项动画弹窗是否显示
  const [showOverRoutePopup, setShowOverRoutePopup] = useState(false); // 结束行程弹窗是否显示
  const [showPassengerNotice, setShowPassengerNotice] = useState(false); // 乘车需知弹窗是否显示
  const [cancelType, setCancelType] = useState(1); // 取消类型
  const [showEvaluatePopup, setShowEvaluatePopup] = useState(false); // 评价弹窗是否显示
  const router = Taro.useRouter();
@@ -87,7 +89,7 @@
  }, [tripStatus]);
  // 页面初始化
  useEffect(() => {
  useDidShow(() => {
    const s = Taro.getStorageSync('startSite');
    const e = Taro.getStorageSync('endSite');
    if (s) {
@@ -97,69 +99,24 @@
      setEndSite(e);
    }
    // 初始化默认标记点 (成都坐标)
    const defaultMarkers = [
      {
        id: 1,
        latitude: 30.572269,
        longitude: 104.066541,
        iconPath: 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/a2b43790-9317-4b3b-b29f-d82cc10830e9.png',
        width: 15,
        height: 15,
        anchor: { x: 0.5, y: 0.5 },
        name: '中国南方电网·1号门'
      },
      {
        id: 2,
        latitude: 30.573200,
        longitude: 104.068100,
        iconPath: 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/a2b43790-9317-4b3b-b29f-d82cc10830e9.png',
        width: 15,
        height: 15,
        anchor: { x: 0.5, y: 0.5 },
        name: '中国南方电网·2号门'
      },
      {
        id: 3,
        latitude: 30.571100,
        longitude: 104.065200,
        iconPath: 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/a2b43790-9317-4b3b-b29f-d82cc10830e9.png',
        width: 15,
        height: 15,
        anchor: { x: 0.5, y: 0.5 },
        name: '中国南方电网·3号门'
      },
      {
        id: 4,
        latitude: 30.574100,
        longitude: 104.066100,
        iconPath: 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/a2b43790-9317-4b3b-b29f-d82cc10830e9.png',
        width: 15,
        height: 15,
        anchor: { x: 0.5, y: 0.5 },
        name: '中国南方电网·4号门'
      },
      {
        id: 5,
        latitude: 30.570100,
        longitude: 104.067100,
        iconPath: 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/a2b43790-9317-4b3b-b29f-d82cc10830e9.png',
        width: 15,
        height: 15,
        anchor: { x: 0.5, y: 0.5 },
        name: '中国南方电网·5号门'
      }
    ];
    setMarkers(defaultMarkers);
    getLocationInfo();
  }, []);
    if (s && s.latitude && s.longitude) {
      setLatitude(s.latitude);
      setLongitude(s.longitude);
    } else {
      getLocationInfo();
    }
  });
  // 规划路线
  useEffect(() => {
    const originLng = startSite?.longitude || longitude;
    const originLat = startSite?.latitude || latitude;
    if (!originLng || !originLat || !endSite.longitude || !endSite.latitude) return;
    const myAmapFun = new amapFile.AMapWX({ key: AMAP_KEY });
    myAmapFun.getDrivingRoute({
      origin: `${longitude},${latitude}`,
      origin: `${originLng},${originLat}`,
      destination: `${endSite.longitude},${endSite.latitude}`,
      success: function (data) {
        const points = parseRouteToPoints(data);
@@ -237,7 +194,7 @@
        console.log('规划失败', info)
      }
    })
  }, [tripStatus, endSite, longitude, latitude])
  }, [tripStatus, endSite, startSite, longitude, latitude])
  const formatTime = (sec: number) => {
    const m = Math.floor(sec / 60);
@@ -344,6 +301,39 @@
    setShowPassengerNotice(false);
  }
  // 打开评价弹窗
  const handleOpenEvaluatePopup = () => {
    setShowEvaluatePopup(true);
  }
  // 关闭评价弹窗
  const handleCloseEvaluatePopup = () => {
    setShowEvaluatePopup(false);
  }
  // 确认评价
  const handleConfirmEvaluate = () => {
    setShowEvaluatePopup(false);
  }
  const toSelectSite = (id: number) => {
    if (id === 1) {
      Taro.navigateTo({
        url: '/robotaxi/pages/site-search/index?pageType=start'
      })
    } else {
      Taro.navigateTo({
        url: '/robotaxi/pages/site-search/index?pageType=end'
      })
    }
  }
  // 点击气泡
  const handleCalloutTap = (e) => {
    const markerId = e.detail.markerId;
    toSelectSite(markerId);
  }
  return (
    <View className="home-container">
      <Map
@@ -354,6 +344,7 @@
        markers={markers}
        polyline={polyline}
        showLocation={!!longitude}
        onCalloutTap={handleCalloutTap}
        style={{ width: '100%', height: '100%' }}
      >
        {/* 自定义callout */}
@@ -428,7 +419,12 @@
            <View catchMove onClick={(e) => e.stopPropagation()}>
              {/* 发布路线 */}
              {
                tripStatus == 1 && <ReleaseRoute callImmediately={callImmediately} />
                (tripStatus == 1 && startSite && endSite) &&
                <ReleaseRoute
                  startSite={startSite}
                  endSite={endSite}
                  callImmediately={callImmediately}
                />
              }
              {/* 发布中弹窗 */}
              {
@@ -444,6 +440,7 @@
                  handleOpenOverRoutePopup={handleOpenOverRoutePopup}
                  handlePay={handlePay}
                  updateStatus={() => setTripStatus(prev => prev + 1)}
                  handleOpenEvaluatePopup={handleOpenEvaluatePopup}
                />
              }
            </View>
@@ -478,6 +475,12 @@
        show={showPassengerNotice}
        onClose={handleClosePassengerNotice}
      />
      {/* 评价弹窗 */}
      <EvaluatePopup
        show={showEvaluatePopup}
        onClose={handleCloseEvaluatePopup}
        onConfirm={handleConfirmEvaluate}
      />
    </View>
  )
}
src/robotaxi/pages/select-city/index.config.ts
@@ -1,3 +1,3 @@
export default {
  navigationBarTitleText: '国内(含广澳门台)',
  navigationBarTitleText: '国内(含港澳台)',
};
src/robotaxi/pages/select-city/index.tsx
@@ -1,32 +1,52 @@
import Taro from '@tarojs/taro';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { View, Text, Input, ScrollView } from '@tarojs/components';
import './index.less';
const pendingCityList = [
  { title: 'A', items: ['安庆市', '安顺市', '安阳市', '鞍山市'] },
  { title: 'B', items: ['北京市', '保定市', '包头市', '北海市'] },
  { title: 'C', items: ['重庆市', '成都市', '长沙市', '长春市'] },
  { title: 'D', items: ['大连市', '东莞市', '大同市', '达州市'] },
  { title: 'F', items: ['福州市', '佛山市', '抚顺市', '阜阳市'] },
  { title: 'G', items: ['广州市', '贵阳市', '桂林市', '赣州市'] },
  { title: 'H', items: ['杭州市', '哈尔滨市', '合肥市', '海口市'] },
  { title: 'J', items: ['济南市', '嘉兴市', '金华市', '吉林市'] },
  { title: 'K', items: ['昆明市', '开封市'] },
  { title: 'L', items: ['兰州市', '拉萨市', '洛阳市', '临沂市'] },
  { title: 'N', items: ['南京市', '南宁市', '宁波市', '南昌市'] },
  { title: 'Q', items: ['青岛市', '秦皇岛市', '泉州市', '曲靖市'] },
  { title: 'S', items: ['上海市', '深圳市', '石家庄市', '沈阳市', '苏州市', '三亚市'] },
  { title: 'T', items: ['天津市', '太原市', '唐山市', '台州市'] },
  { title: 'W', items: ['武汉市', '无锡市', '温州市', '乌鲁木齐市'] },
  { title: 'X', items: ['西安市', '厦门市', '徐州市', '西宁市'] },
  { title: 'Y', items: ['银川市', '扬州市', '烟台市', '宜昌市'] },
  { title: 'Z', items: ['郑州市', '珠海市', '中山市', '张家口市'] }
  { title: 'A', items: ['安庆', '安顺', '安阳', '鞍山'] },
  { title: 'B', items: ['北京', '保定', '包头', '北海'] },
  { title: 'C', items: ['重庆', '成都', '长沙', '长春'] },
  { title: 'D', items: ['大连', '东莞', '大同', '达州'] },
  { title: 'F', items: ['福州', '佛山', '抚顺', '阜阳'] },
  { title: 'G', items: ['广州', '贵阳', '桂林', '赣州'] },
  { title: 'H', items: ['杭州', '哈尔滨', '合肥', '海口'] },
  { title: 'J', items: ['济南', '嘉兴', '金华', '吉林'] },
  { title: 'K', items: ['昆明', '开封'] },
  { title: 'L', items: ['兰州', '拉萨', '洛阳', '临沂'] },
  { title: 'N', items: ['南京', '南宁', '宁波', '南昌'] },
  { title: 'Q', items: ['青岛', '秦皇岛', '泉州', '曲靖'] },
  { title: 'S', items: ['上海', '深圳', '石家庄', '沈阳', '苏州', '三亚'] },
  { title: 'T', items: ['天津', '太原', '唐山', '台州'] },
  { title: 'W', items: ['武汉', '无锡', '温州', '乌鲁木齐'] },
  { title: 'X', items: ['西安', '厦门', '徐州', '西宁'] },
  { title: 'Y', items: ['银川', '扬州', '烟台', '宜昌'] },
  { title: 'Z', items: ['郑州', '珠海', '中山', '张家口'] }
];
export default function SelectCityPage() {
  const [scrollIntoView, setScrollIntoView] = useState('');
  const [searchValue, setSearchValue] = useState('');
  const [selectedCity, setSelectedCity] = useState('北京');
  const [filteredPendingList, setFilteredPendingList] = useState<any[]>([]);
  // 开放城市
  const openCityList = ['北京', '广州', '深圳', '珠海', '佛山'];
  useEffect(() => {
    const city = Taro.getStorageSync('selectedCity');
    if (city) {
      setSelectedCity(city);
    }
    // 初始化待开放城市,过滤掉已开放的城市
    const list = pendingCityList.map(group => ({
      ...group,
      items: group.items.filter(item => !openCityList.includes(item))
    })).filter(group => group.items.length > 0);
    setFilteredPendingList(list);
  }, [])
  const handleScrollTo = (letter) => {
    setScrollIntoView(`city-${letter}`);
@@ -65,10 +85,10 @@
            <View className='service-city-title'>运营城市</View>
            <View className='service-city-list'>
              {
                ['广州市', '深圳市', '珠海市', '佛山市'].map((item, index) => (
                openCityList.map((item, index) => (
                  <View
                    key={index}
                    className={`service-city-item ${index === 0 ? 'active-city' : ''}`}
                    className={`service-city-item ${selectedCity === item && 'active-city'}`}
                    onClick={() => handleSelectCity(item)}
                  >
                    {item}
@@ -86,7 +106,7 @@
              scrollIntoView={scrollIntoView}
            >
              {
                pendingCityList.map((group) => (
                filteredPendingList.map((group) => (
                  <View key={group.title} id={`city-${group.title}`} className='city-group'>
                    <View className='city-group-title'>{group.title}</View>
                    {
@@ -107,7 +127,7 @@
          </View>
          <View className='shortcut'>
            {
              pendingCityList.map((group) => (
              filteredPendingList.map((group) => (
                <View
                  key={group.title}
                  className='shortcut-item'
@@ -129,7 +149,7 @@
        <View>
          <View className='search-result-box'>
            {
              ['成都市', '广州市', '深圳市', '珠海市', '佛山市'].map((item, index) => (
              ['成都', '广州', '深圳', '珠海', '佛山'].map((item, index) => (
                <View key={index} className='search-result-item'>
                  <View onClick={() => handleSelectCity(item)} className='search-result-city'>{item}</View>
                  <View className={`search-result-zone ${index != 0 ? 'search-result-zone-disabled' : ''}`}>运营区</View>
src/robotaxi/pages/select-map-site/index.tsx
@@ -31,16 +31,12 @@
  const router = Taro.useRouter();
  // 校验上车点是否存在
  // 页面初始化
  useEffect(() => {
    const s = Taro.getStorageSync('startSite');
    if (s) {
      setStartSite(s);
    }
  }, []);
  // 页面初始化
  useEffect(() => {
    if (router.params.pageType) {
      setPageType(router.params.pageType);
    }
@@ -275,11 +271,10 @@
          {(showToolTip && selectedSite.name) && (
            <View className="toolTip">
              <View className="toolTip-bg">
                <Text className="toolTip-bg-text">'限停3分钟'</Text>
                <Text className="toolTip-bg-text">限停3分钟</Text>
              </View>
              <View className="toolTip-txt">
                <View className="toolTip-txt-content">{selectedSite.name}</View>
                <Image className="toolTip-icon" src={rightIcon} />
              </View>
            </View>
          )}