feat(robotaxi): 新增路线选择组件并优化页面布局与样式
- 在“开始行程”页面添加路线选择卡片,包含路线类型、时间和信号灯信息
- 调整底部确认按钮区域布局,将价格信息移至独立区域
- 更新“站点搜索”页面收藏夹图标和快速入口图标尺寸
- 新增多个图标资源(收藏夹、步行图标、选择线路、选择路线、交通信号灯)
- 优化日志上传函数参数类型,统一使用对象格式
- 修复WebSocket消息处理和重连逻辑中的代码格式问题
| | |
| | | originOpacity: number; |
| | | toOrigin: () => void; |
| | | originIcon: string; |
| | | selectRouteIcon: string; |
| | | walkIcon: string; |
| | | renderTripCard: () => JSX.Element | null; |
| | | showCustomerService: boolean; |
| | | handleCloseCustomerService: () => void; |
| | |
| | | originOpacity, |
| | | toOrigin, |
| | | originIcon, |
| | | selectRouteIcon, |
| | | walkIcon, |
| | | renderTripCard, |
| | | showCustomerService, |
| | | handleCloseCustomerService, |
| | |
| | | {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()}> |
| | |
| | | .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; |
| | |
| | | |
| | | .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; |
| | |
| | | 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; |
| | |
| | | } |
| | | |
| | | .original-fare { |
| | | |
| | | |
| | | font-size: 24px; |
| | | font-weight: normal; |
| | | line-height: 41px; |
| | |
| | | } |
| | | |
| | | .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); |
| | | } |
| | | } |
| | | } |
| | | } |
| | |
| | | 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; |
| | |
| | | |
| | | 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"> |
| | |
| | | </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"> |
| | |
| | | |
| | | .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; |
| | | } |
| | | } |
| | | } |
| | |
| | | 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() { |
| | |
| | | originBoxHidden={originBoxHidden} |
| | | originOpacity={originOpacity} |
| | | toOrigin={toOrigin} |
| | | selectRouteIcon={selectRouteIcon} |
| | | walkIcon={walkIcon} |
| | | originIcon={originIcon} |
| | | renderTripCard={renderTripCard} |
| | | showCustomerService={showCustomerService} |
| | |
| | | justify-content: center; |
| | | |
| | | .fast-item-icon { |
| | | width: 28px; |
| | | height: 28px; |
| | | width: 23px; |
| | | height: 22px; |
| | | margin-right: 8px; |
| | | } |
| | | |
| | |
| | | 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' |
| | |
| | | {/* 收藏夹与地图选点 */} |
| | | <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> |
| | |
| | | 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; |
| | |
| | | .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的经过小程序封装的数据 |
| | |
| | | .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); |
| | |
| | | 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); |
| | |
| | | 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) { |
| | |
| | | this.notifyListeners('message', res.data); |
| | | return; |
| | | } |
| | | |
| | | |
| | | const content = safeParse(data.content || '{}', {}); |
| | | |
| | | |
| | | try { |
| | | this.handleIncomingMessage(data, content); |
| | | if (data.type) { |
| | |
| | | this.isConnected = false; |
| | | this.isConnecting = false; |
| | | this.stopHeartbeat(); |
| | | |
| | | |
| | | // 立即触发重连 |
| | | if (!this.isManualClose) { |
| | | this.handleReconnect(); |