hejianhao
2026-02-06 3e0c1b211a8c8dc6fafb9f76cee2a6f45fab5d40
feat(robotaxi): 优化行程发布与地图交互体验

- 修复行程状态初始值,避免从storage读取旧状态
- 优化地图标记点生成逻辑,分离路线计算与标记渲染
- 调整地图视角控制策略,根据不同行程状态展示不同区域
- 增加高德API动态获取城市列表,替换静态数据
- 修复按钮点击事件冒泡问题,优化用户交互
- 调整CSS样式,修复布局和显示问题
8个文件已修改
362 ■■■■■ 已修改文件
src/pages/robotaxi/home/index.tsx 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car.tsx 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/css-folder/calling-car.less 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/css-folder/release-be-being.less 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/release-be-being.tsx 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/index.less 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/index.tsx 216 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/select-city/index.tsx 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/index.tsx
@@ -504,7 +504,13 @@
              <View className="to-site-dot"></View>
              <View className="to-site-address">
                <View className="to-site-text">你想去哪?</View>
                <View onClick={toBeginRoute} className="to-site-fast">去:中国南方电网·3号门</View>
                <View
                  onClick={(e) => {
                    e.stopPropagation();
                    toBeginRoute()
                  }}
                  className="to-site-fast"
                >去:中国南方电网·3号门</View>
              </View>
            </View>
          </View>
src/robotaxi/pages/begin-route/components/calling-car.tsx
@@ -40,12 +40,12 @@
                if (updateStatusRef.current) {
                    updateStatusRef.current();
                }
            }, 5000);
            }, 1000 * 10);
        }
        return () => {
            if (timer) clearInterval(timer);
        };
    }, [tripStatus]); // 移除 updateStatus 依赖,只在 tripStatus 变化时重置定时器
    }, [tripStatus]);
    const titleMap = {
        3: <>预计<View className="time">6</View>分钟内有车应答</>,
src/robotaxi/pages/begin-route/components/css-folder/calling-car.less
@@ -55,6 +55,7 @@
            padding: 0 24px;
            border-radius: 3112px;
            border: 1px solid #181916;
            box-sizing: border-box;
            font-size: 24px;
            height: 48px;
            display: flex;
src/robotaxi/pages/begin-route/components/css-folder/release-be-being.less
@@ -1,7 +1,6 @@
.release-be-being {
    border-radius: 32px 32px 0 0;
    padding: 40px;
    padding-bottom: 94px;
    padding: 37px 40px 94px 40px;
    background: #FFFFFF;
    .top-box {
@@ -12,6 +11,7 @@
            justify-content: space-between;
            .title {
                margin-top: 3px;
                font-size: 36px;
                font-weight: bold;
                line-height: 52px;
@@ -19,13 +19,15 @@
                color: rgba(0, 0, 0, 0.9);
            }
            .cancel {
            .cancel-btn {
                padding: 0 24px;
                border-radius: 3112px;
                border: 1px solid #181916;
                font-size: 24px;
                line-height: 48px;
                text-align: center;
                height: 48px;
                display: flex;
                align-items: center;
                justify-content: center;
                letter-spacing: normal;
                color: #181916;
            }
src/robotaxi/pages/begin-route/components/release-be-being.tsx
@@ -39,7 +39,7 @@
            <View className="top-box">
                <View className="top-box-title">
                    <View className="title">正在发布行程...</View>
                    <View onClick={handleOpenCancelPopup} className="cancel">取消行程</View>
                    <View onClick={handleOpenCancelPopup} className="cancel-btn">取消行程</View>
                </View>
                <View className="progress-box">
                    <View className="progress" style={{ width: `${progress}%` }}></View>
src/robotaxi/pages/begin-route/index.less
@@ -56,22 +56,27 @@
    position: absolute;
    bottom: 0;
    z-index: 10;
    overflow: hidden;
    .origin-box {
        display: flex;
        justify-content: flex-end;
        padding-bottom: 32px;
        padding-right: 32px;
    &-inner {
        position: relative;
        .origin-icon {
            width: 88px;
            height: 88px;
        .origin-box {
            position: absolute;
            top: -120px;
            right: 32px;
            z-index: 101;
            .origin-icon {
                width: 88px;
                height: 88px;
            }
        }
    }
}
.custom-callout {
    position: relative;
    z-index: 999;
    display: flex;
    align-items: center;
    justify-content: center;
@@ -94,6 +99,9 @@
    font-size: 24px;
    letter-spacing: normal;
    color: #3D3D3D;
    display: flex;
    align-items: center;
    justify-content: center;
}
.car-time-num {
@@ -113,7 +121,7 @@
.waiting-callout {
    position: relative;
    z-index: 10;
    z-index: 999;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
@@ -175,9 +183,8 @@
    height: 70px;
    min-width: 155px;
    text-align: center;
    width: fit-content;
    display: flex;
    align-items: center;
    align-items: flex-start;
    justify-content: center;
}
@@ -191,11 +198,7 @@
}
.waiting-callout-bg-text {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    position: relative;
    z-index: 100;
    font-size: 22px;
    font-weight: 500;
@@ -204,6 +207,7 @@
    line-height: 48px;
    padding: 0 20px;
    box-sizing: border-box;
    white-space: nowrap;
}
.waiting-callout-txt {
src/robotaxi/pages/begin-route/index.tsx
@@ -31,7 +31,7 @@
  const [polyline, setPolyline] = useState<any[]>([]); // 路线
  const [startSite, setStartSite] = useState<any>(null); // 上车点
  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 [tripStatus, setTripStatus] = useState<number>(2); // 行程状态 1:发布行程 2:发布中 3:呼叫车中 4:车辆即将到达 5:车辆到达上车点 6:车辆行驶中 7:到达目的地 8:行程已结束 9:支付成功
  const [elapsedSeconds, setElapsedSeconds] = useState(0); // 等待计时秒数
  const [showCancelPopup, setShowCancelPopup] = useState(false); // 取消弹窗是否显示
  const [showAnnouncementsPopup, setShowAnnouncementsPopup] = useState(false); // 安全注意事项动画弹窗是否显示
@@ -39,8 +39,7 @@
  const [showPassengerNotice, setShowPassengerNotice] = useState(false); // 乘车需知弹窗是否显示
  const [cancelType, setCancelType] = useState(1); // 取消类型
  const [showEvaluatePopup, setShowEvaluatePopup] = useState(false); // 评价弹窗是否显示
  const router = Taro.useRouter();
  const [routePoints, setRoutePoints] = useState<any[]>([]); // 路线点集合
  // 监听行程状态,处理等待计时
@@ -65,15 +64,6 @@
      if (s && s.latitude && s.longitude) {
        setLatitude(s.latitude);
        setLongitude(s.longitude);
        // 增加延迟,确保地图上下文就绪
        setTimeout(() => {
          const mapCtx = Taro.createMapContext('map');
          mapCtx.moveToLocation({
            latitude: s.latitude,
            longitude: s.longitude
          });
        }, 500);
      }
      setElapsedSeconds(0);
@@ -120,6 +110,7 @@
      destination: `${endSite.longitude},${endSite.latitude}`,
      success: function (data) {
        const points = parseRouteToPoints(data);
        setRoutePoints(points);
        setPolyline([{
          points: points,
@@ -127,74 +118,93 @@
          width: 10,
          arrowLine: true,
        }])
        // 设置起点和终点标记
        if (points.length > 0) {
          const startPoint = points[0];
          const endPoint = points[points.length - 1];
          // 检查 storage 中的 startSite 是否存在
          const s = Taro.getStorageSync('startSite');
          if ([1, 2, 3, 4, 7, 8].includes(tripStatus)) {
            const isCar = [4, 7].includes(tripStatus);
            const startMarker: any = {
              id: 1,
              latitude: startPoint.latitude,
              longitude: startPoint.longitude,
              iconPath: isCar
                ? 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/e51e92e6-700d-4e83-b3e0-14e6f18e0b9a.png'
                : 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/51e7cb75-72af-4994-9d6e-d999efb2fbc7.png',
              width: isCar ? 40 : 22,
              height: isCar ? 20 : 40,
              anchor: { x: 0.5, y: 1 },
              zIndex: 2,
            };
            if (tripStatus === 4) {
              startMarker.name = '车辆位置';
              startMarker.customCallout = { anchorY: 0, anchorX: 0, display: 'ALWAYS' };
            } else if (tripStatus !== 7) {
              startMarker.name = s?.name || '当前位置';
              startMarker.customCallout = { anchorY: 8, anchorX: 0, display: 'ALWAYS' };
            }
            setMarkers([
              // 起点指针
              startMarker,
              // 终点指针
              {
                id: 2,
                latitude: endPoint.latitude,
                longitude: endPoint.longitude,
                iconPath: 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/6a56b1eb-ec37-4839-8733-262942af070d.png',
                width: 22,
                height: 40,
                anchor: { x: 0.5, y: 1 },
                zIndex: 2,
                name: tripStatus === 7 ? '已到达' : endSite.name,
                customCallout: {
                  anchorY: 8,
                  anchorX: 0,
                  display: 'ALWAYS'
                }
              },
            ])
          }
          // 调整视角以包含所有路线点
          const mapCtx = Taro.createMapContext('map');
          mapCtx.includePoints({
            points: points,
            padding: [50, 50, 300, 50]
          })
        }
      },
      fail: function (info) {
        console.log('规划失败', info)
      }
    })
  }, [tripStatus, endSite, startSite, longitude, latitude])
  }, [endSite.longitude, endSite.latitude, startSite?.longitude, startSite?.latitude, longitude, latitude])
  // 生成标记点
  useEffect(() => {
    if (routePoints.length > 0) {
      const startPoint = routePoints[0];
      const endPoint = routePoints[routePoints.length - 1];
      // 检查 storage 中的 startSite 是否存在
      const s = Taro.getStorageSync('startSite');
      if ([1, 2, 3, 4, 7, 8].includes(tripStatus)) {
        const isCar = [4, 7].includes(tripStatus);
        const startMarker: any = {
          id: Date.now() + 1,
          latitude: startPoint.latitude,
          longitude: startPoint.longitude,
          iconPath: isCar
            ? 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/e51e92e6-700d-4e83-b3e0-14e6f18e0b9a.png'
            : 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/51e7cb75-72af-4994-9d6e-d999efb2fbc7.png',
          width: isCar ? 40 : 22,
          height: isCar ? 20 : 40,
          anchor: { x: 0.5, y: 1 },
          zIndex: 2,
        };
        if (tripStatus === 4) {
          startMarker.name = '车辆位置';
          startMarker.customCallout = { anchorY: 0, anchorX: 0, display: 'ALWAYS' };
        } else if (tripStatus !== 7) {
          startMarker.name = s?.name || '当前位置';
          startMarker.customCallout = { anchorY: 8, anchorX: 0, display: 'ALWAYS' };
        }
        setMarkers([
          // 起点指针
          startMarker,
          // 终点指针
          {
            id: Date.now() + 2,
            latitude: endPoint.latitude,
            longitude: endPoint.longitude,
            iconPath: 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/6a56b1eb-ec37-4839-8733-262942af070d.png',
            width: 22,
            height: 40,
            anchor: { x: 0.5, y: 1 },
            zIndex: 2,
            name: tripStatus === 7 ? '已到达' : endSite.name,
            customCallout: {
              anchorY: 8,
              anchorX: 0,
              display: 'ALWAYS'
            }
          },
        ])
      }
    }
  }, [tripStatus, routePoints, startSite?.name, endSite.name])
  // 调整视角
  useEffect(() => {
    if (routePoints.length > 0) {
      const mapCtx = Taro.createMapContext('map');
      if (tripStatus === 1 || tripStatus === 8) {
        // 状态为1(发布行程)或8(行程已结束)时,展示整条路线
        mapCtx.includePoints({
          points: routePoints,
          padding: [50, 50, 300, 50]
        })
      } else if (tripStatus === 7) {
        mapCtx.includePoints({
          points: [routePoints[routePoints.length - 1]],
          padding: [50, 50, 600, 50]
        })
      } else {
        mapCtx.moveToLocation({
          longitude: routePoints[0].longitude,
          latitude: routePoints[0].latitude
        })
      }
    }
  }, [tripStatus, routePoints])
  const formatTime = (sec: number) => {
    const m = Math.floor(sec / 60);
@@ -227,8 +237,9 @@
  const toBack = () => {
    if (tripStatus == 1) {
    if (tripStatus == 9) {
      Taro.removeStorageSync('tripStatus');
      Taro.removeStorageSync('endSite');
    }
    Taro.switchTab({
      url: '/pages/robotaxi/home/index'
@@ -361,7 +372,23 @@
      >
        {/* 自定义callout */}
        {
          ![2, 3].includes(tripStatus) && markers.map((item) => {
          markers.map((item, index) => {
            const isWaiting = [2, 3].includes(tripStatus) && index === 0;
            if (isWaiting) {
              return (
                <CoverView slot="callout" marker-id={item.id} key={item.id} className="waiting-callout">
                  <CoverView className="waiting-callout-bg">
                    <CoverImage className="waiting-callout-bg-img" src="https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/f55ce277-68de-4615-9c7c-807f588afaac.png" />
                    <CoverView className="waiting-callout-bg-text">{formatTime(elapsedSeconds)}</CoverView>
                  </CoverView>
                  <CoverView className="waiting-callout-txt">
                    <CoverView className="waiting-callout-txt-content">{item.name}</CoverView>
                  </CoverView>
                </CoverView>
              )
            }
            return (
              <CoverView slot="callout" marker-id={item.id} key={item.id} className="custom-callout">
                {
@@ -373,41 +400,22 @@
                {
                  item.name === '车辆位置' &&
                  <CoverView className="custom-callout-content">
                    <Text className="car-time"><Text className="car-time-num">3</Text>公里<Text className="car-time-nuum">2</Text>分钟</Text>
                    <CoverView className="car-time">
                      <CoverView className="car-time-num">3</CoverView>
                      <CoverView>公里</CoverView>
                      <CoverView className="car-time-num">2</CoverView>
                      <CoverView>分钟</CoverView>
                    </CoverView>
                  </CoverView>
                }
                {
                  item.name === '已到达' &&
                  <CoverView className="custom-callout-content">
                    已到达
                    <CoverView className="custom-callout-content-arrive">已到达</CoverView>
                  </CoverView>
                }
              </CoverView>
            )
          })
        }
        {
          (tripStatus === 2 || tripStatus === 3) && markers.map((item) => {
            if (item.id === 1) {
              return (
                <CoverView slot="callout" marker-id={item.id} key={item.id} className="waiting-callout">
                  {tripStatus === 3 && (
                    <CoverView className="waiting-radiation-box">
                      <CoverView className="waiting-radiation-ring"></CoverView>
                      <CoverView className="waiting-radiation-ring"></CoverView>
                    </CoverView>
                  )}
                  <CoverView className="waiting-callout-bg">
                    <CoverImage className="waiting-callout-bg-img" src="https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/f55ce277-68de-4615-9c7c-807f588afaac.png" />
                    <CoverView className="waiting-callout-bg-text">{formatTime(elapsedSeconds)}</CoverView>
                  </CoverView>
                  <CoverView className="waiting-callout-txt">
                    <CoverView className="waiting-callout-txt-content">{item.name}</CoverView>
                  </CoverView>
                </CoverView>
              )
            }
            return null
          })
        }
        {/* 选择城市 */}
src/robotaxi/pages/select-city/index.tsx
@@ -3,7 +3,10 @@
import { View, Text, Input, ScrollView } from '@tarojs/components';
import './index.less';
const pendingCityList = [
import { AMAP_KEY } from '../../../constants/api';
// 仅用于前端拼音首字母映射,不作为数据源展示
const CITY_PINYIN_DICT = [
  { title: 'A', items: ['安庆', '安顺', '安阳', '鞍山'] },
  { title: 'B', items: ['北京', '保定', '包头', '北海'] },
  { title: 'C', items: ['重庆', '成都', '长沙', '长春'] },
@@ -24,6 +27,18 @@
  { title: 'Z', items: ['郑州', '珠海', '中山', '张家口'] }
];
const getCityPinyinLetter = (cityName: string) => {
  const firstChar = cityName[0];
  for (const group of CITY_PINYIN_DICT) {
    for (const item of group.items) {
      if (item.includes(firstChar)) {
        return group.title;
      }
    }
  }
  return '#';
};
export default function SelectCityPage() {
  const [scrollIntoView, setScrollIntoView] = useState('');
  const [searchValue, setSearchValue] = useState('');
@@ -39,13 +54,65 @@
      setSelectedCity(city);
    }
    // 初始化待开放城市,过滤掉已开放的城市
    const list = pendingCityList.map(group => ({
      ...group,
      items: group.items.filter(item => !openCityList.includes(item))
    })).filter(group => group.items.length > 0);
    // 调用高德接口获取全国城市数据
    Taro.request({
      url: 'https://restapi.amap.com/v3/config/district',
      data: {
        key: AMAP_KEY,
        subdistrict: 2,
        extensions: 'base'
      },
      success: (res) => {
        if (res.data.status === '1' && res.data.districts && res.data.districts.length > 0) {
          const allCities: string[] = [];
          const country = res.data.districts[0];
          if (country.districts) {
            country.districts.forEach(province => {
              // 直辖市处理:北京、天津、上海、重庆
              if (['110000', '120000', '310000', '500000'].includes(province.adcode)) {
                allCities.push(province.name);
              } else if (province.districts) {
                // 普通省份,遍历下级城市
                province.districts.forEach(city => {
                  if (city.level === 'city') {
                    allCities.push(city.name);
                  }
                });
              }
            });
          }
    setFilteredPendingList(list);
          // 分组与过滤
          const groups: Record<string, string[]> = {};
          allCities.forEach(city => {
            // 去除末尾的“市”以便于展示和匹配(如“成都市”->“成都”)
            // 注意:有些自治州不以市结尾,这里只处理以“市”结尾的情况
            const simpleName = city.endsWith('市') ? city.slice(0, -1) : city;
            // 过滤已开放城市
            if (openCityList.includes(simpleName)) return;
            const letter = getCityPinyinLetter(simpleName);
            if (!groups[letter]) groups[letter] = [];
            groups[letter].push(simpleName);
          });
          // 转换为数组格式并排序
          const list = Object.keys(groups).sort().map(key => ({
            title: key,
            items: groups[key]
          }));
          setFilteredPendingList(list);
        }
      },
      fail: (err) => {
        console.error('获取城市列表失败', err);
        // 降级处理:如果接口失败,可以使用字典作为备选(可选)
      }
    });
  }, [])
  const handleScrollTo = (letter) => {