hejianhao
2026-04-14 1c03c649f8cc0ea417f67d21219dcc8a6c8dc44c
feat(robotaxi): 新增路线选择组件并优化页面布局与样式

- 在“开始行程”页面添加路线选择卡片,包含路线类型、时间和信号灯信息
- 调整底部确认按钮区域布局,将价格信息移至独立区域
- 更新“站点搜索”页面收藏夹图标和快速入口图标尺寸
- 新增多个图标资源(收藏夹、步行图标、选择线路、选择路线、交通信号灯)
- 优化日志上传函数参数类型,统一使用对象格式
- 修复WebSocket消息处理和重连逻辑中的代码格式问题
5个文件已添加
11个文件已修改
380 ■■■■■ 已修改文件
src/robotaxi/assets/favorite.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/map.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/origin-icon.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/select-line.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/select-route.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/traffic-signal.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/walk-icon.png 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/begin-route-view.tsx 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/css-folder/release-route.less 228 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/release-route.tsx 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/index.less 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/index.tsx 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/site-search/index.less 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/site-search/index.tsx 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/request.ts 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/websocket.ts 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/assets/favorite.png
src/robotaxi/assets/map.png

src/robotaxi/assets/origin-icon.png

src/robotaxi/assets/select-line.png
src/robotaxi/assets/select-route.png
src/robotaxi/assets/traffic-signal.png
src/robotaxi/assets/walk-icon.png
src/robotaxi/pages/begin-route/components/begin-route-view.tsx
@@ -33,6 +33,8 @@
  originOpacity: number;
  toOrigin: () => void;
  originIcon: string;
  selectRouteIcon: string;
  walkIcon: string;
  renderTripCard: () => JSX.Element | null;
  showCustomerService: boolean;
  handleCloseCustomerService: () => void;
@@ -86,6 +88,8 @@
    originOpacity,
    toOrigin,
    originIcon,
    selectRouteIcon,
    walkIcon,
    renderTripCard,
    showCustomerService,
    handleCloseCustomerService,
@@ -145,6 +149,7 @@
            {shouldShowOrigin && (
              <View className='origin-box' style={{ opacity: originBoxHidden ? 0 : originOpacity, transition: 'opacity 0.2s ease-out', pointerEvents: originBoxHidden ? 'none' : 'auto' }}>
                <Image onClick={toOrigin} className='origin-icon' src={originIcon} />
                <Image className='select-route-icon' src={selectRouteIcon} />
              </View>
            )}
            <View catchMove onClick={(e) => e.stopPropagation()}>
src/robotaxi/pages/begin-route/components/css-folder/release-route.less
@@ -1,8 +1,117 @@
.release-route {
    .route-select {
        border-radius: 24px;
        background: #FFFFFF;
        box-sizing: border-box;
        border: 1px solid #FFFFFF;
        margin: 0 32px 16px 32px;
        padding: 16px 24px 24px 24px;
        .route-select-type {
            display: flex;
            align-items: center;
            font-size: 24px;
            font-weight: 500;
            line-height: 44px;
            letter-spacing: normal;
            color: rgba(0, 0, 0, 0.9);
            .route-select-type-recommend {
                line-height: 40px;
                letter-spacing: normal;
                color: #6D7FD9;
            }
            image {
                flex-shrink: 0;
                margin-left: 9.25px;
                height: 11.97px;
                width: 17.5px;
            }
        }
        .route-select-content {
            display: flex;
            justify-content: space-between;
            gap: 10px;
            margin-top: 16px;
            .route-select-content-item {
                flex: 1;
                padding: 16px 0 24px 24px;
                border-radius: 16px;
                background: #F5F6FA;
                border: 1px solid #F2F3F7;
                .route-select-content-item-title {
                    font-size: 20px;
                    line-height: 20px;
                    letter-spacing: normal;
                    color: #6D7FD9;
                }
                .route-select-content-item-time {
                    margin-top: 10px;
                    font-size: 28px;
                    font-weight: 500;
                    line-height: 40px;
                    letter-spacing: normal;
                    color: rgba(0, 0, 0, 0.9);
                }
                .route-select-content-item-distance {
                    display: flex;
                    align-items: center;
                    gap: 12px;
                    margin-top: 12px;
                    font-size: 20px;
                    line-height: 20px;
                    letter-spacing: normal;
                    color: rgba(0, 0, 0, 0.4);
                    .traffic-signal-item {
                        display: flex;
                        align-items: center;
                        gap: 4px;
                        .traffic-signal-icon {
                            flex-shrink: 0;
                            width: 20px;
                            height: 20px;
                        }
                    }
                }
            }
            .active-item {
                background: linear-gradient(97deg, rgba(132, 117, 255, 0.2) 0%, rgba(125, 214, 255, 0.2) 103%) !important;
                box-sizing: border-box;
                position: relative;
                &::after {
                    content: "";
                    position: absolute;
                    top: 0;
                    left: 0;
                    right: 0;
                    bottom: 0;
                    border-radius: 16px;
                    padding: 1px;
                    background: linear-gradient(180deg, #AAA8FF -10%, rgba(41, 159, 255, 0.0469) 108%, rgba(0, 0, 0, 0) 108%);
                    -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
                    -webkit-mask-composite: xor;
                    mask-composite: exclude;
                    pointer-events: none;
                }
            }
        }
    }
    .select-site {
        position: relative;
        padding: 32px;
        padding-bottom: 56px;
        padding-bottom: 232px;
        padding-top: 40px;
        background: #F2F3F7;
        border-top: 2px solid #fff;
@@ -108,8 +217,11 @@
        .route-content {
            position: relative;
            margin-top: 46px;
            margin-top: 28px;
            z-index: 10;
            display: flex;
            align-items: center;
            justify-content: space-between;
            .tip-icon {
                width: 22px;
@@ -117,71 +229,78 @@
                flex-shrink: 0;
            }
            .route-content-top {
                display: flex;
                align-items: center;
            }
            .route-content-txt {
                font-size: 24px;
                line-height: 40px;
                letter-spacing: normal;
                color: rgba(0, 0, 0, 0.6);
                color: rgba(0, 0, 0, 0.4);
            }
            .route-content-top {
                display: flex;
                justify-content: space-between;
            .route-content-bottom {
                .route-content-top-money {
                &-left {
                    display: flex;
                    align-items: center;
                    font-size: 24px;
                    line-height: 40px;
                    letter-spacing: normal;
                    color: rgba(0, 0, 0, 0.9);
                    color: rgba(0, 0, 0, 0.6);
                    .tip-icon {
                        margin-right: 4px;
                        margin-right: 6px;
                    }
                }
            }
        }
                    .money-num {
                        margin: 0 4px;
                        font-size: 48px;
                        line-height: 40px;
                        letter-spacing: normal;
                        color: rgba(0, 0, 0, 0.9);
                    }
    }
    .confirm-btn-box {
        position: fixed;
        bottom: 0;
        z-index: 11;
        width: 100%;
        padding: 24px 38px 62px 32px;
        background: #FEFEFF;
        display: flex;
        justify-content: space-between;
        .money-box {
            margin-top: 8px;
            .route-content-top-money {
                display: flex;
                align-items: center;
                font-size: 24px;
                line-height: 40px;
                letter-spacing: normal;
                color: rgba(0, 0, 0, 0.9);
                .tip-icon {
                    width: 22px;
                    height: 22px;
                    flex-shrink: 0;
                    margin-right: 4px;
                }
                .money-num {
                    margin: 0 4px;
                    font-size: 48px;
                    line-height: 40px;
                    letter-spacing: normal;
                    color: rgba(0, 0, 0, 0.9);
                }
            }
            .route-content-bottom {
                display: flex;
                align-items: center;
                justify-content: space-between;
                margin-top: 16px;
                &-left {
                    display: flex;
                    align-items: center;
                    view:first-child {
                        font-size: 24px;
                        line-height: 40px;
                        letter-spacing: normal;
                        color: rgba(0, 0, 0, 0.6);
                    }
                    view:last-child {
                        font-size: 24px;
                        font-weight: normal;
                        line-height: 36px;
                        letter-spacing: normal;
                        color: #6D7FD9;
                    }
                    .tip-icon {
                        margin-left: 8px;
                        margin-right: 6px;
                    }
                }
                &-right {
                    margin-left: 30px;
                    display: flex;
                    align-items: center;
                    gap: 8px;
@@ -199,7 +318,7 @@
                    }
                    .original-fare {
                        font-size: 24px;
                        font-weight: normal;
                        line-height: 41px;
@@ -212,29 +331,32 @@
        }
        .confirm-btn-wrapper {
            width: 296px;
            position: relative;
            margin-top: 64px;
            height: 88px;
            height: 94px;
        }
        .confirm-btn {
            height: 88px;
            width: 296px;
            height: 94px;
            display: flex;
            justify-content: center;
            align-items: center;
            border-radius: 16px;
            background: linear-gradient(95deg, #8475FF 0%, #7DD6FF 124%);
            border-radius: 20px;
            background: linear-gradient(102deg, #8475FF 0%, #7DD6FF 102%);
            font-size: 28px;
            letter-spacing: normal;
            color: #FFFFFF;
        }
        .confirm-btn-mask {
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            border-radius: 16px;
            border-radius: 20px;
            background: rgba(255, 255, 255, 0.4);
        }
    }
}
}
src/robotaxi/pages/begin-route/components/release-route.tsx
@@ -4,6 +4,8 @@
import './css-folder/release-route.less'
import tipBlueIcon from '../../../assets/tip-blue-icon.png'
import tipDefaultIcon from '../../../assets/tip-default-icon.png'
import selectLineIcon from '../../../assets/select-line.png'
import trafficSignalIcon from '../../../assets/traffic-signal.png'
type Props = {
    startSite: Site;
@@ -39,6 +41,30 @@
    return (
        <View className='release-route'>
            <View className="route-select">
                <View className="route-select-type">
                    <View>路线选择:</View>
                    <View className="route-select-type-recommend">智能推荐</View>
                    <Image src={selectLineIcon} />
                </View>
                <View className="route-select-content">
                    {
                        [1, 2, 3].map(item => (
                            <View className={`route-select-content-item ${item === 1 ? 'active-item' : ''}`} key={item}>
                                <View className="route-select-content-item-title">智能推荐</View>
                                <View className="route-select-content-item-time">10分钟</View>
                                <View className="route-select-content-item-distance">
                                    <View>10.6公里</View>
                                    <View className="traffic-signal-item">
                                        <Image className="traffic-signal-icon" src={trafficSignalIcon} />
                                        <Text>6个</Text>
                                    </View>
                                </View>
                            </View>
                        ))
                    }
                </View>
            </View>
            <View className="select-site">
                <Image className="site-top-bg" src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/site-top-bg.png' />
                <View className="site-boarding-point">
@@ -65,30 +91,34 @@
                </View>
                <View className="route-content">
                    <View className="route-content-top">
                        <View className="route-content-txt">预估等待{fareEstimate.waitingTime || '-'}分钟</View>
                        <View className="route-content-top-money" onClick={() => toPriceDescription()} >
                            <Image className="tip-icon" src={tipDefaultIcon} />
                            预估
                            <Text className="money-num">{fareEstimate.finalFareStr || '-'}</Text>
                            元
                        </View>
                        <View className="route-content-txt">预估等待{fareEstimate.waitingTime || '-'}分钟 | </View>
                        <View className="route-content-txt">限乘{fareEstimate.maxPeopleNumber || '-'}人</View>
                    </View>
                    <View className="route-content-bottom">
                        <View onClick={handleOpenPassengerNotice} className="route-content-bottom-left">
                            <View>限乘{fareEstimate.maxPeopleNumber || '-'}人</View>
                            <Image className="tip-icon" src={tipBlueIcon} />
                            <Image className="tip-icon" src={tipDefaultIcon} />
                            <View>乘车须知</View>
                        </View>
                        <View className="route-content-bottom-right">
                            {
                                fareEstimate.discountAmount > 0 &&
                                <View className="discount-amount">已优惠{fareEstimate.discountAmount}元</View>
                            }
                            {
                                fareEstimate.originalFareStr > 0 &&
                                <View className="original-fare">{fareEstimate.originalFareStr || '-'}元</View>
                            }
                        </View>
                    </View>
                </View>
            </View>
            <View className="confirm-btn-box">
                <View className="money-box">
                    <View className="route-content-top-money" onClick={() => toPriceDescription()} >
                        <Image className="tip-icon" src={tipDefaultIcon} />
                        预估
                        <Text className="money-num">{fareEstimate.finalFareStr || '-'}</Text>
                        元
                    </View>
                    <View className="route-content-bottom-right">
                        {
                            fareEstimate.discountAmount > 0 &&
                            <View className="discount-amount">已优惠{fareEstimate.discountAmount}元</View>
                        }
                        {
                            fareEstimate.originalFareStr > 0 &&
                            <View className="original-fare">{fareEstimate.originalFareStr || '-'}元</View>
                        }
                    </View>
                </View>
                <View className="confirm-btn-wrapper">
src/robotaxi/pages/begin-route/index.less
@@ -62,13 +62,23 @@
        .origin-box {
            position: absolute;
            top: -120px;
            right: 32px;
            bottom: 100%; /* 相对于父级(底部卡片容器)顶部 */
            margin-bottom: 24px; /* 距离底部卡片24px */
            right: 24px;
            z-index: 101;
            display: flex;
            flex-direction: column;
            align-items: center;
            .origin-icon {
                width: 88px;
                height: 88px;
                width: 72px;
                height: 72px;
            }
            .select-route-icon {
                margin-top: 16px;
                width: 72px;
                height: 72px;
            }
        }
    }
src/robotaxi/pages/begin-route/index.tsx
@@ -36,6 +36,8 @@
import coupon from '../../../robotaxi/assets/coupon.png'
import suggest from '../../../robotaxi/assets/suggest.png'
import originIcon from '../../../robotaxi/assets/origin-icon.png'
import selectRouteIcon from '../../../robotaxi/assets/select-route.png'
import walkIcon from '../../../robotaxi/assets/walk-icon.png'
import { _ROBOTAXI_WS_HOST } from '../../../constants/api';
export default function BeginRoute() {
@@ -520,6 +522,8 @@
      originBoxHidden={originBoxHidden}
      originOpacity={originOpacity}
      toOrigin={toOrigin}
      selectRouteIcon={selectRouteIcon}
      walkIcon={walkIcon}
      originIcon={originIcon}
      renderTripCard={renderTripCard}
      showCustomerService={showCustomerService}
src/robotaxi/pages/site-search/index.less
@@ -92,8 +92,8 @@
      justify-content: center;
      .fast-item-icon {
        width: 28px;
        height: 28px;
        width: 23px;
        height: 22px;
        margin-right: 8px;
      }
src/robotaxi/pages/site-search/index.tsx
@@ -3,6 +3,7 @@
import { View, Text, Image, Input } from '@tarojs/components';
import './index.less';
import map from '../../assets/map.png'
import favorite from '../../assets/favorite.png'
import address from '../../assets/address.png'
import arrow from '../../assets/arrow.png'
import activeStar from '../../assets/active-star.png'
@@ -312,7 +313,7 @@
      {/* 收藏夹与地图选点 */}
      <View className='fast-box'>
        <View className='fast-item' onClick={() => setShowFavoritePopup(true)}>
          <Image className='fast-item-icon' src={map}></Image>
          <Image className='fast-item-icon' src={favorite}></Image>
          <Text className='fast-item-text'>收藏夹</Text>
        </View>
        <View className='fast-split'></View>
src/utils/request.ts
@@ -36,8 +36,8 @@
  const { url, method = 'POST', showToast = true, autoLogin = true, skipException = false, originResponse = false } = options;
  // 对payload进行排序,保证 payload 里面的顺序和生成签名时 payload 的顺序一致,因为后端要拿 payload 计算签名,所以顺序是必须一致的,同时,后端也需要对 payload 进行排序,后端通过 XpClientCheck 注解
  if(needLogin) {
    options.payload = objectSort(options.payload, {keyUpperCase: false})
  if (needLogin) {
    options.payload = objectSort(options.payload, { keyUpperCase: false })
  }
  const payload = options.payload;
@@ -55,13 +55,15 @@
    .then(async (res) => {
      if (!isMiniLogUploadUrl(url)) {
        uploadMiniLog(
          JSON.stringify({
            type: 'http_response',
            url,
            request: payload,
            response: res?.data,
            timestamp: Date.now()
          })
          {
            data: JSON.stringify({
              type: 'http_error',
              url,
              request: payload,
              response: res?.data,
              timestamp: Date.now()
            })
          }
        );
      }
      // res为包含cookies、header的经过小程序封装的数据
@@ -100,13 +102,15 @@
    .catch((err) => {
      if (!isMiniLogUploadUrl(url)) {
        uploadMiniLog(
          JSON.stringify({
            type: 'http_error',
            url,
            request: payload,
            response: err,
            timestamp: Date.now()
          })
          {
            data: JSON.stringify({
              type: 'http_error',
              url,
              request: payload,
              response: err,
              timestamp: Date.now()
            })
          }
        );
      }
      console.log('request err:', url);
@@ -143,7 +147,7 @@
  return typeof url === 'string' && url.includes(MINI_LOG_UPLOAD_PATH);
};
export const uploadMiniLog = async (logContent: string) => {
export const uploadMiniLog = async (logContent: any) => {
  if (!logContent) return;
  try {
    const header = await buildHeader({ method: 'POST', payload: logContent }, false);
src/utils/websocket.ts
@@ -119,16 +119,16 @@
          console.log('收到 WebSocket 消息:', res.data);
          if (!this.isHeartbeatMessage(data)) {
            const messageContent = typeof res.data === 'string' ? res.data : JSON.stringify(res.data);
            uploadMiniLog(messageContent);
            uploadMiniLog({ data: messageContent });
          }
        } catch (e) {
          console.warn('解析 WebSocket data 失败:', res.data);
          this.notifyListeners('message', res.data);
          return;
        }
        const content = safeParse(data.content || '{}', {});
        try {
          this.handleIncomingMessage(data, content);
          if (data.type) {
@@ -196,9 +196,9 @@
        this.notifyListeners('message', res.data);
        return;
      }
      const content = safeParse(data.content || '{}', {});
      try {
        this.handleIncomingMessage(data, content);
        if (data.type) {
@@ -370,7 +370,7 @@
    this.isConnected = false;
    this.isConnecting = false;
    this.stopHeartbeat();
    // 立即触发重连
    if (!this.isManualClose) {
      this.handleReconnect();