hejianhao
2026-03-30 b298b7953fa88b274dcff9e20acd8e79243904f1
feat: 优化地图交互与UI样式

- 添加地图标记点击处理,支持点击标记后自动定位并显示信息
- 优化乘客须知弹窗,实现自定义滚动条以提升体验
- 调整多个页面的内边距、字体大小等样式细节
- 修复行程状态判断逻辑,避免因临时状态未定义导致的显示问题
- 改进运营区域页面,动态生成运营时间描述并优化布局
- 优化车辆位置更新逻辑,确保方向与位置同步更新
11个文件已修改
390 ■■■■■ 已修改文件
src/pages/robotaxi/home/index.tsx 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car.tsx 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/css-folder/passenger-notice.less 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/css-folder/release-route.less 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/passenger-notice.tsx 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/index.tsx 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/operating-area/index.less 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/operating-area/index.tsx 159 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/select-map-site/index.tsx 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/site-search/index.less 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/site-search/index.tsx 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/index.tsx
@@ -1208,6 +1208,29 @@
    jumpTo({ url: '/pages/user/login?hideBack=1' });
  }
  // 处理点击地图 marker
  const handleMarkerTap = (e: any) => {
    if (isLocationAuthDenied) return;
    const markerId = e?.detail?.markerId;
    if (markerId === undefined) return;
    const targetMarker = markers.find(m => m.id === markerId);
    if (targetMarker) {
      // 忽略因调用 moveToLocation 引起的 region change,避免重复吸附计算
      shouldIgnoreRegionChange.current = true;
      const mapCtx = Taro.createMapContext('map');
      mapCtx.moveToLocation({
        latitude: targetMarker.latitude,
        longitude: targetMarker.longitude,
      });
      setIsAdsorbed(true);
      setCurrentSite(targetMarker);
      setShowToolTip(true);
    }
  };
  return (
    <View className="home-container">
      <Map
@@ -1215,10 +1238,11 @@
        longitude={longitude ?? DEFAULT_LONGITUDE}
        latitude={latitude ?? DEFAULT_LATITUDE}
        scale={mapScale}
        markers={isLocationAuthDenied ? [] : markers}
        markers={isLocationAuthDenied ? [] : markers.map(m => ({ ...m, title: '', name: '' }))}
        polygons={polygons}
        showLocation={!!longitude}
        onRegionChange={handleRegionChange}
        onMarkerTap={handleMarkerTap}
        style={{ width: '100%', height: '85vh', flex: 'none' }}
      >
        {/* 选择城市 */}
src/robotaxi/pages/begin-route/components/calling-car.tsx
@@ -576,7 +576,7 @@
                        }
                    </View>
                    {
                        tripStatus == 4 && <View>
                        (tripStatus == 4 && typeof tempStatus === 'number') && <View>
                            <View className="car-card-line"></View>
                            {/* 温度调节 */}
                            <View className="car-card-temp">
src/robotaxi/pages/begin-route/components/css-folder/passenger-notice.less
@@ -33,12 +33,39 @@
            color: rgba(0, 0, 0, 0.9);
        }
        &-desc {
        &-desc-wrap {
            position: relative;
            margin-top: 34px;
            letter-spacing: normal;
            color: rgba(0, 0, 0, 0.9);
            height: 342px;
            overflow: hidden;
            .passenger-content-desc {
                height: 100%;
                overflow-y: scroll;
                margin-top: 0;
            }
            /* 自定义滚动条轨道 */
            .custom-scrollbar-track {
                position: absolute;
                right: 0;
                top: 0;
                width: 6px;
                height: 100%;
                background-color: rgba(0, 0, 0, 0.05);
                border-radius: 4px;
                z-index: 10;
            }
            /* 自定义滚动条滑块 */
            .custom-scrollbar-thumb {
                position: absolute;
                right: 0;
                top: 0;
                width: 6px;
                background-color: rgba(0, 0, 0, 0.25);
                border-radius: 4px;
                z-index: 11;
            }
        }
        &-checkbox {
src/robotaxi/pages/begin-route/components/css-folder/release-route.less
@@ -95,7 +95,7 @@
                    align-items: center;
                    .to-site-text {
                        font-size: 32px;
                        font-size: 28px;
                        font-weight: 500;
                        line-height: 48px;
                        letter-spacing: normal;
@@ -141,7 +141,6 @@
                    }
                    .money-num {
                        margin: 0 4px;
                        font-size: 48px;
                        line-height: 40px;
@@ -169,7 +168,7 @@
                    }
                    view:last-child {
                        font-size: 20px;
                        font-size: 24px;
                        font-weight: normal;
                        line-height: 36px;
                        letter-spacing: normal;
@@ -201,7 +200,7 @@
                    .original-fare {
                        
                        font-size: 28px;
                        font-size: 24px;
                        font-weight: normal;
                        line-height: 41px;
                        letter-spacing: normal;
src/robotaxi/pages/begin-route/components/passenger-notice.tsx
@@ -7,13 +7,52 @@
type Props = {
    show?: boolean;
    onClose?: () => void;
    onCheckedChange?: (checked: boolean) => void;
}
export default function PassengerNotice({ show = false, onClose }: Props) {
export default function PassengerNotice({ show = false, onClose, onCheckedChange }: Props) {
    const [visible, setVisible] = useState(false);
    const [animationData, setAnimationData] = useState<any>();
    const [checked, setChecked] = useState(true);
    const [noticeContent, setNoticeContent] = useState('');
    const [thumbHeight, setThumbHeight] = useState(0);
    const [thumbTop, setThumbTop] = useState(0);
    const [containerHeight, setContainerHeight] = useState(0);
    const [showScrollbar, setShowScrollbar] = useState(false);
    const onScroll = (e) => {
        const { scrollHeight, scrollTop } = e.detail;
        if (containerHeight > 0 && scrollHeight > containerHeight) {
            const newThumbTop = (scrollTop / scrollHeight) * containerHeight;
            setThumbTop(newThumbTop);
        }
    };
    useEffect(() => {
        if (noticeContent && visible) {
            // Give it a little time to render
            setTimeout(() => {
                const query = Taro.createSelectorQuery();
                query.select('.passenger-content-desc').boundingClientRect();
                query.select('.passenger-content-desc-inner').boundingClientRect();
                query.exec((res) => {
                    if (res[0] && res[1]) {
                        const wrapperHeight = res[0].height;
                        const contentHeight = res[1].height;
                        setContainerHeight(wrapperHeight);
                        if (contentHeight > wrapperHeight) {
                            setShowScrollbar(true);
                            // thumb height = (container height / content height) * container height
                            const calculatedThumbHeight = (wrapperHeight / contentHeight) * wrapperHeight;
                            setThumbHeight(Math.max(calculatedThumbHeight, 20)); // min height 20px
                        } else {
                            setShowScrollbar(false);
                        }
                    }
                });
            }, 300);
        }
    }, [noticeContent, visible]);
    useEffect(() => {
        if (show && !visible) {
@@ -60,13 +99,36 @@
    if (!visible) return null;
    return (
        <View className='passenger-popup'>
        <View className='passenger-popup' onClick={() => onCheckedChange?.(true)}>
            <View className='passenger-mask' catchMove></View>
            <View className='passenger-content' animation={animationData}>
                <View className='passenger-content-title'>乘车需知</View>
                <ScrollView className='passenger-content-desc' scrollY enhanced lowerThreshold={30} upperThreshold={30} showScrollbar={true}>
                    <RichText nodes={noticeContent} />
                </ScrollView>
                <View className='passenger-content-desc-wrap'>
                    <ScrollView
                        className='passenger-content-desc'
                        scrollY
                        enhanced
                        lowerThreshold={30}
                        upperThreshold={30}
                        showScrollbar={false}
                        onScroll={onScroll}
                    >
                        <View className='passenger-content-desc-inner'>
                            <RichText nodes={noticeContent} />
                        </View>
                    </ScrollView>
                    {showScrollbar && (
                        <View className='custom-scrollbar-track'>
                            <View
                                className='custom-scrollbar-thumb'
                                style={{
                                    height: `${thumbHeight}px`,
                                    transform: `translateY(${thumbTop}px)`
                                }}
                            />
                        </View>
                    )}
                </View>
                <View onClick={() => setChecked(!checked)} className='passenger-content-checkbox'>
                    {
                        checked ?
src/robotaxi/pages/begin-route/index.tsx
@@ -50,6 +50,7 @@
import { getOrderDistance } from '../../api/select-map-site';
import { checkOtp } from '../../../utils/tools';
import { jumpTo } from '../../../utils/jump';
import { _ROBOTAXI_WS_HOST } from '../../../constants/api';
export default function SelectMapSite() {
  const WAITING_TIMER_START_KEY = 'waitingTimerStartAt'; // 等待计时开始时间
@@ -756,7 +757,10 @@
        if (durationSeconds !== null) {
          setRouteDuration(durationSeconds);
        }
        setVehicleDirection(data?.direction);
        const dir = Number(data?.direction);
        if (Number.isFinite(dir)) {
          setVehicleDirection(dir);
        }
      })
      .catch(() => { });
  };
@@ -923,11 +927,11 @@
        return;
      }
      const points = payload?.points || [];
      const direction = payload?.direction;
      if (typeof direction === 'number' && Number.isFinite(direction)) {
      const direction = Number(payload?.direction);
      if (Number.isFinite(direction)) {
        setVehicleDirection(direction);
      }
      const distance = Number(JSON.parse(payload?.content?.route)?.distance);
      const distance = Number(JSON.parse(payload?.content?.route || '{}')?.distance);
      const duration = Number(JSON.parse(payload?.content?.route)?.duration);
      const distanceKm = normalizeDistanceKm(distance);
      if (distanceKm !== null) {
@@ -954,7 +958,7 @@
            applyRoutePoints(points);
          });
        }
        // 延迟执行车辆位置更新,确保路线擦除(同步调用或极短时间生效)先于小车平移发生
        if (firstPoint && typeof firstPoint.latitude === 'number' && typeof firstPoint.longitude === 'number') {
          setTimeout(() => {
@@ -1393,7 +1397,7 @@
    if (!carMarkerInitialized.current) return;
    if (!vehicleLocation) return;
    const mapCtx = Taro.createMapContext('map');
    // 增加延迟,确保 markers 已经在原生视图中完成渲染,避免 translateMarker 找不到 markerId 导致 fail
    const timer = setTimeout(() => {
      mapCtx.translateMarker({
@@ -1403,7 +1407,8 @@
          longitude: vehicleLocation.longitude,
        },
        autoRotate: false,
        rotate: typeof vehicleDirection === 'number' ? vehicleDirection : 0,
        moveWithRotate: true,
        rotate: typeof vehicleDirection === 'number' ? Number(vehicleDirection.toFixed(0)) : 0,
        duration: 100,
        fail: (err) => {
          console.log('translateMarker fail:', err, 'markerId:', vehicleMarkerId);
@@ -1445,6 +1450,7 @@
      return;
    }
    if (socketOpenHandlerRef.current) {
      wsManager.connect(_ROBOTAXI_WS_HOST);
      return;
    }
    const handleOpen = () => {
@@ -1452,6 +1458,7 @@
    };
    socketOpenHandlerRef.current = handleOpen;
    wsManager.on('open', handleOpen);
    wsManager.connect(_ROBOTAXI_WS_HOST);
  };
  // 页面初始化
@@ -1585,14 +1592,16 @@
  // 返回当前位置或车辆位置
  const toOrigin = () => {
    const mapCtx = Taro.createMapContext('map');
    // 如果是状态6(车辆行驶中)且有车辆位置,则回到车辆位置
    if (tripStatus === 6 && vehicleLocation?.latitude && vehicleLocation?.longitude) {
      mapCtx.moveToLocation({
        latitude: vehicleLocation.latitude,
        longitude: vehicleLocation.longitude,
    // 如果是状态6(车辆行驶中)且有路线数据,则展示完整路线
    if (tripStatus === 6 && routePoints.length > 0) {
      const bottomPadding = computeBottomPadding(routePoints, 'route');
      const topPadding = Math.max(24, resolveSafeTopInset() + 32);
      mapCtx.includePoints({
        points: routePoints,
        padding: [topPadding, 100, bottomPadding, 100]
      });
      // 恢复视口锁定状态,让地图重新跟随车辆
      tripStatus6ViewportLockedRef.current = false;
      // 锁定视口状态,防止后续路线数据更新时频繁改变缩放比例
      tripStatus6ViewportLockedRef.current = true;
    } else {
      mapCtx.moveToLocation();
    }
@@ -1941,6 +1950,9 @@
      url: `/robotaxi/pages/site-search/index?pageType=${pageType}&longitude=${startSite.longitude}&latitude=${startSite.latitude}&electronicAreaId=${startSite.electronicAreaId}`
    })
  }
  const changeCheckedEnter = (checked: boolean) => {
    isCheckedEnter.current = checked;
  }
  // 点击气泡
  const handleCalloutTap = (e) => {
@@ -2238,6 +2250,7 @@
        <PassengerNotice
          show={showPassengerNotice}
          onClose={handleClosePassengerNotice}
          onCheckedChange={changeCheckedEnter}
        />
      }
      {/* 评价弹窗 */}
src/robotaxi/pages/operating-area/index.less
@@ -16,7 +16,6 @@
    background: rgba(255, 255, 255, 0.9);
    backdrop-filter: blur(40px);
    box-shadow: 0px 4px 16px 0px rgba(84, 92, 143, 0.1);
    z-index: 100;
    .back-icon {
      width: 40px;
src/robotaxi/pages/operating-area/index.tsx
@@ -27,7 +27,7 @@
    getOpenCity().then((res: any) => {
      const openCities = res.cities || [];
      setCityList(openCities);
      const cachedCityCode = cachedSelectedCity?.code;
      const matchedCity = openCities.find((city: any) => (city.code || city.adcode) === cachedCityCode);
@@ -240,7 +240,7 @@
        setRawGeofenceData(normalizedGeofenceData);
        const firstPolygonId = normalizedGeofenceData.length > 0 ? normalizedGeofenceData[0].mapPolygonId : null;
        setSelectedPolygonId(firstPolygonId);
        if (firstPolygonId !== null) {
          const firstPolygonData = normalizedGeofenceData.find(item => item.mapPolygonId === firstPolygonId);
          setOperatingTimeInfo(firstPolygonData);
@@ -250,14 +250,14 @@
        const newPolygons = updatePolygonsStyle(normalizedGeofenceData, firstPolygonId);
        setPolygons(newPolygons);
        setPolylines(updatePolylineStyle(normalizedGeofenceData, firstPolygonId));
        if (normalizedGeofenceData.length > 0 && normalizedGeofenceData[0]?.points?.length > 0) {
          const firstPoint = normalizedGeofenceData[0].points[0];
          const lat = Number(firstPoint.lat);
          const lng = Number(firstPoint.lng);
          setLatitude(lat);
          setLongitude(lng);
          const mapCtx = Taro.createMapContext('map');
          const points = extractPointsFromPolygons(newPolygons);
          if (points.length > 0) {
@@ -293,7 +293,7 @@
    } else if (e.markerId) {
      tapPolygonId = e.markerId;
    }
    if (tapPolygonId !== null && rawGeofenceData.length > 0) {
      lastPolygonTapTimeRef.current = Date.now();
      selectPolygon(tapPolygonId, rawGeofenceData);
@@ -326,9 +326,7 @@
  };
  const toBack = () => {
    Taro.navigateBack({
      delta: 1,
    });
    Taro.navigateBack();
  };
  const toOrigin = () => {
@@ -339,6 +337,57 @@
        points,
        padding: [10, 10, 10, 10]
      });
    }
  };
  const renderOperatingTime = (info: any) => {
    if (!info?.rule) {
      return `全天`;
    }
    try {
      const rules = JSON.parse(info.rule);
      if (!Array.isArray(rules) || rules.length === 0) {
        return `全天`;
      }
      const getDayText = (typeStr: number[]) => {
        if (!typeStr || typeStr.length === 0 || typeStr.includes(0)) return '每天';
        const dayMap = { 1: '周一', 2: '周二', 3: '周三', 4: '周四', 5: '周五', 6: '周六', 7: '周日' };
        const days = typeStr.map(d => dayMap[d]).filter(Boolean);
        if (days.length === 7) return '每天';
        return days.join('、');
      };
      const parts = rules.map((rule: any) => {
        let prefix = '';
        if (rule.type === 0) {
          prefix = '工作日';
        } else if (rule.type === 1) {
          prefix = '节假日';
        } else if (rule.type === 2) {
          prefix = '非工作日';
        } else if (rule.type === 3) {
          prefix = getDayText(rule.typeStr);
        }
        let timeStr = '';
        if (rule.type === 3 && (!rule.time || rule.time.length === 0)) {
          timeStr = '全天';
        } else {
          timeStr = Array.isArray(rule.time) ? rule.time.join(', ') : rule.time;
        }
        return `${prefix}: ${timeStr}`;
      });
      const resultStr = parts.join('     ');
      if (resultStr.includes('工作日: 全天') && resultStr.includes('非工作日: 全天') || resultStr.includes('每天: 全天') && resultStr.includes('每天: 全天')) {
        return '';
      }
      return resultStr;
    } catch (e) {
      console.error('解析围栏规则失败', e);
      return ``;
    }
  };
@@ -357,66 +406,60 @@
        onPolygonTap={handlePolygonTap}
        onTap={handleMapTap}
      >
        {/* 返回按钮 */}
        <View onClick={toBack} className="back-btn">
          <Image className="back-icon" src={backIcon} />
        </View>
      </Map>
      {/* 返回按钮 */}
      <View onClick={toBack} className="back-btn">
        <Image className="back-icon" src={backIcon} />
      </View>
      {/* 底部面板 */}
      <View className="bottom-sheet">
        <Image className="origin-icon" src={originIcon} onClick={toOrigin} />
        <View className="header">
          <Text className="title">查看运营区域</Text>
          {/* <Image onClick={toBack} className="close-icon" src={closeIcon} /> */}
        </View>
        <ScrollView scrollX className="city-scroll">
          <View className="city-list">
            {cityList.map((city, index) => {
              const code = city.code || city.adcode;
              return (
                <View
                  key={index}
                  className={`city-item ${selectedCityCode === code ? 'active' : ''}`}
                  onClick={() => handleSelectCity(code)}
                >
                  {city.name}
                </View>
              );
            })}
            {/* Fallback mock data if cityList is empty */}
            {cityList.length === 0 && (
              <>
                <View className="city-item active">北京市</View>
                <View className="city-item">广州市</View>
                <View className="city-item">佛山市</View>
              </>
            )}
      {
        cityList.length != 0 &&
        <View className="bottom-sheet">
          <Image className="origin-icon" src={originIcon} onClick={toOrigin} />
          <View className="header">
            <Text className="title">查看运营区域</Text>
          </View>
        </ScrollView>
        <View className="content-box">
          <View className="content-title">区域运营时间:</View>
          <View className="content-time">
            工作日: {operatingTimeInfo?.workDayOpenTime || '07:00'}-{operatingTimeInfo?.workDayCloseTime || '22:00'}     周末: {operatingTimeInfo?.weekendOpenTime || '07:00'}-{operatingTimeInfo?.weekendCloseTime || '22:00'}
          </View>
          <View className="content-desc">
            <View className="desc-item">
              <Text className="index">1.</Text>
              <Text className="text">在运营时段可正常使用打车服务,非运营时段服务暂停</Text>
          <ScrollView scrollX className="city-scroll">
            <View className="city-list">
              {cityList.map((city, index) => {
                const code = city.code || city.adcode;
                return (
                  <View
                    key={index}
                    className={`city-item ${selectedCityCode === code ? 'active' : ''}`}
                    onClick={() => handleSelectCity(code)}
                  >
                    {city.name}
                  </View>
                );
              })}
            </View>
            <View className="desc-item">
              <Text className="index">2.</Text>
              <Text className="text">各站点运营时间可能存在调整,实际运营情况请以页面实时展示为准</Text>
          </ScrollView>
          <View className="content-box">
            <View className="content-title">区域运营时间:</View>
            <View className="content-time">
              {renderOperatingTime(operatingTimeInfo)}
            </View>
            <View className="desc-item">
              <Text className="index">3.</Text>
              <Text className="text">如有问题,请拨打客服热线xxxxxx</Text>
            <View className="content-desc">
              <View className="desc-item">
                <Text className="index">1.</Text>
                <Text className="text">在运营时段可正常使用打车服务,非运营时段服务暂停</Text>
              </View>
              <View className="desc-item">
                <Text className="index">2.</Text>
                <Text className="text">各站点运营时间可能存在调整,实际运营情况请以页面实时展示为准</Text>
              </View>
              <View className="desc-item">
                <Text className="index">3.</Text>
                <Text className="text">如有问题,请拨打客服热线xxxxxx</Text>
              </View>
            </View>
          </View>
        </View>
      </View>
      }
    </View>
  );
}
src/robotaxi/pages/select-map-site/index.tsx
@@ -455,6 +455,27 @@
    })
  }
  // 处理点击地图 marker
  const handleMarkerTap = (e: any) => {
    const markerId = e?.detail?.markerId;
    if (markerId === undefined) return;
    const targetMarker = markers.find(m => m.id === markerId);
    if (targetMarker) {
      const mapCtx = Taro.createMapContext('map');
      mapCtx.moveToLocation({
        latitude: targetMarker.latitude,
        longitude: targetMarker.longitude,
      });
      setSelectedSite(targetMarker);
      setOutOfServiceRange(getOutOfServiceRange(isOperatingArea, targetMarker));
      setLatitude(targetMarker.latitude);
      setLongitude(targetMarker.longitude);
      setShowToolTip(true);
    }
  };
  return (
    <View className="home-container">
      <Map
@@ -462,11 +483,12 @@
        longitude={longitude}
        latitude={latitude}
        scale={14}
        markers={markers}
        markers={markers.map(m => ({ ...m, title: '', name: '' }))}
        polyline={polyline}
        polygons={polygons}
        showLocation={!!longitude}
        onRegionChange={handleRegionChange}
        onMarkerTap={handleMarkerTap}
        style={{ width: '100%', height: '100%' }}
      >
        {/* 返回 */}
src/robotaxi/pages/site-search/index.less
@@ -180,7 +180,7 @@
    }
    .card-content {
      padding: 16px 24px;
      padding: 16px 0;
    }
  }
src/robotaxi/pages/site-search/index.tsx
@@ -460,9 +460,24 @@
      }
      {
        (!siteList.length && (!searchValue ? !nearbySiteList.length : true) && !nonOperationalSite.length && (!searchValue ? !historySite.length : true)) &&
        <View className='no-data-box'>
          <Image className='no-data-image' src={require('../../assets/no-data-site.png')} />
          <View>暂无站点</View>
        <View className='history-card'>
          <View className='card-header'>暂无搜索结果,为您推荐附近站点</View>
          <View className='card-content list-content'>
            {
              filteredNearbyList.slice(0, MAX_EXPANDED_COUNT).map((item, index) => {
                return <View hoverClass='common-hover-class' onClick={() => onChangeSelectSite(item)} className='history-item' key={item.id || index}>
                  <Image className='history-icon' src={address}></Image>
                  <View className='history-info'>
                    <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}</View>
                    </View>
                  </View>
                </View>
              })
            }
          </View>
        </View>
      }
      <TipPopup