Merge branch 'feature_v2.5.0' into feature_v2.4.0_ui_animation
| | |
| | | }} |
| | | > |
| | | <View className='origin-box-container'> |
| | | <Image onClick={props.onToOrigin} className="origin-icon" src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/origin-icon-20260716.png' /> |
| | | <View className='tips-and-origin'> |
| | | <Image onClick={props.onToOrigin} className="origin-icon" src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/origin-icon-20260716.png' /> |
| | | <MapOverlayTips |
| | | isGuest={props.shouldShowGuestTip} |
| | | isLocationAuthDenied={props.isLocationAuthDenied} |
| | | isOperatingArea={props.isOperatingArea} |
| | | isMaintain={props.isMaintain} |
| | | maintainBounce={props.maintainBounce} |
| | | hotfixEndText={props.hotfixEndText} |
| | | anchorBottom={props.overlayAnchorBottom ?? undefined} |
| | | opacity={props.originOpacity} |
| | | isDragging={props.isDragging} |
| | | onShowPrivacy={props.onShowPrivacy} |
| | | onOpenSetting={props.onOpenSetting} |
| | | onViewOperatingArea={props.onViewOperatingArea} |
| | | shouldShowOrderStatusCard={props.shouldShowOrderStatusCard} |
| | | /> |
| | | </View> |
| | | |
| | | {props.shouldShowOrderStatusCard && ( |
| | | <View className="order-status-overlay"> |
| | | <OrderStatusCard |
| | |
| | | )} |
| | | </View> |
| | | </View> |
| | | <MapOverlayTips |
| | | isGuest={props.shouldShowGuestTip} |
| | | isLocationAuthDenied={props.isLocationAuthDenied} |
| | | isOperatingArea={props.isOperatingArea} |
| | | isMaintain={props.isMaintain} |
| | | maintainBounce={props.maintainBounce} |
| | | hotfixEndText={props.hotfixEndText} |
| | | anchorBottom={props.overlayAnchorBottom ?? undefined} |
| | | opacity={props.originOpacity} |
| | | isDragging={props.isDragging} |
| | | onShowPrivacy={props.onShowPrivacy} |
| | | onOpenSetting={props.onOpenSetting} |
| | | onViewOperatingArea={props.onViewOperatingArea} |
| | | shouldShowOrderStatusCard={props.shouldShowOrderStatusCard} |
| | | /> |
| | | |
| | | <View className="map-entry"> |
| | | <View className="map-entry-box"> |
| | | <Image onClick={props.onToPersonalCenter} className="map-entry-icon" src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/personal-center-20260716.png' /> |
| | |
| | | * 地图顶部提示层。 |
| | | */ |
| | | export default function MapOverlayTips(props: Props) { |
| | | const anchorStyle = typeof props.anchorBottom === 'number' |
| | | ? { |
| | | top: 'auto', |
| | | bottom: `${props.anchorBottom}px`, |
| | | opacity: props.opacity !== undefined ? props.opacity : 1, |
| | | visibility: props.opacity !== undefined && props.opacity <= 0 ? 'hidden' : 'visible' as any, |
| | | pointerEvents: props.opacity !== undefined && props.opacity <= 0 ? 'none' : 'auto' as any, |
| | | transition: props.isDragging ? 'none' : 'opacity 0.2s ease-out' |
| | | } |
| | | : undefined; |
| | | return ( |
| | | <View className="content-box-top" style={anchorStyle}> |
| | | <View className="content-box-top"> |
| | | <View className="position-box"> |
| | | { |
| | | props.isGuest && <View className="visitor-stste ml-32"> |
| | | props.isGuest && <View className="visitor-stste"> |
| | | <View className="visitor-stste-tip">同意隐私协议体验更多约车体验</View> |
| | | <View onClick={props.onShowPrivacy} className="visitor-stste-go">去同意</View> |
| | | </View> |
| | | } |
| | | { |
| | | props.isLocationAuthDenied && <View className="visitor-stste ml-32 mt-20"> |
| | | props.isLocationAuthDenied && <View className="visitor-stste"> |
| | | <View className="visitor-stste-tip">未开启位置权限,开启后即可发布行程</View> |
| | | <View onClick={props.onOpenSetting} className="visitor-stste-go">去开启</View> |
| | | </View> |
| | | } |
| | | { |
| | | props.isMaintain && <View className={`is-maintain ml-32 ${props.maintainBounce ? 'maintain-bounce' : ''}`}>系统维护中,预计{props.hotfixEndText || '-:-'}恢复服务</View> |
| | | props.isMaintain && <View className={`is-maintain ${props.maintainBounce ? 'maintain-bounce' : ''}`}>系统维护中,预计{props.hotfixEndText || '-:-'}恢复服务</View> |
| | | } |
| | | { |
| | | !props.isOperatingArea && !props.shouldShowOrderStatusCard && <View onClick={props.onViewOperatingArea} className="is-operating-area mt-20">查看运营区域</View> |
| | |
| | | } |
| | | .check-all-box { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | align-items: flex-start; |
| | | justify-content: flex-start; |
| | | margin-top: 14px; |
| | | .all-name{ |
| | | font-size: 28px; |
| | |
| | | .rt-item-radio { |
| | | flex-shrink: 0; |
| | | margin-right: 8px; |
| | | margin-top: 6px; |
| | | } |
| | | } |
| | | |
| | |
| | | import Taro from '@tarojs/taro'; |
| | | import { View, Text, Image } from '@tarojs/components'; |
| | | import { useEffect, useMemo, useState } from 'react'; |
| | | import { useEffect, useState } from 'react'; |
| | | import './index.less'; |
| | | import { privicyShowManager } from './utils' |
| | | import { submitAgreement } from '../../../../../robotaxi/api/home' |
| | |
| | | onAgree?: () => void; |
| | | } |
| | | |
| | | /** |
| | | * 统一格式化协议名展示文案,避免协议名写死在页面中。 |
| | | */ |
| | | const formatAgreementNamesText = (list: LoginAgreementItem[]) => { |
| | | const names = list |
| | | .map((item: LoginAgreementItem) => String(item?.name || '').trim()) |
| | | .filter((name: string) => !!name); |
| | | if (names.length === 0) return ''; |
| | | return names.map((name: string) => `《${name}》`).join('、'); |
| | | }; |
| | | |
| | | export default function PrivacyPopup({ show = false, privacyUpdateInfo, onClose, onAgree }: Props) { |
| | | const [visible, setVisible] = useState(false); |
| | | const [animationData, setAnimationData] = useState<any>(); |
| | | const [agreementList, setAgreementList] = useState<LoginAgreementItem[]>([]); |
| | | const [isAgree, setIsAgree] = useState<Record<string, boolean>>({}); |
| | | const [isAllAgree, setIsAllAgree] = useState(false); |
| | | const agreementNamesText = useMemo(() => formatAgreementNamesText(agreementList), [agreementList]); // 协议说明文案由协议列表动态生成 |
| | | |
| | | // 拉取登录协议列表并初始化勾选状态 |
| | | const loadLoginAgreement = () => { |
| | |
| | | <View className='rt-privacy-text'> |
| | | <Text className='rt-text-content'> |
| | | {privacyUpdateInfo?.isUpdateAuth |
| | | ? `感谢您使用小鹏自动驾驶出行服务!为了更好地保障您隐私保护、个人信息保护等个人权益,我们更新了 ${agreementNamesText || ''},为保障您的出行权益,请您点击下方链接查阅完整协议,并完成授权确认后方可继续使用服务。` |
| | | : `欢迎使用小鹏自动驾驶出行服务!为了更好地保障您隐私保护、个人信息保护等个人权益,辛苦查阅 ${agreementNamesText || ''} 。` |
| | | ? `感谢您选择小鹏 Robotaxi 小程序!为更好保障您的个人合法权益,我们已更新相关协议。请点击下方链接查阅完整协议,完成授权确认后,即可继续使用服务。` |
| | | : `欢迎使用小鹏自动驾驶出行服务!为充分保障您的隐私与个人信息相关合法权益,请点击下方链接查阅完整协议,完成授权确认后,即可使用叫车服务。` |
| | | } |
| | | </Text> |
| | | <View className='rt-link-item mt-22 pt-10 check-all-box' onClick={checkAll}> |
| | | <Image className='rt-item-radio' src={isAllAgree ? 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/check-20260630.png' : 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/uncheck.png'} mode='aspectFill' /> |
| | | <View className='rt-item-name all-name'>我已满18周岁且具有完全民事行为能力,已阅读并同意:</View> |
| | | <Image className='rt-item-radio' src={isAllAgree ? 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/check-20260630.png' : 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/uncheck-20260804.png'} mode='aspectFill' /> |
| | | <View className='rt-item-name all-name'>我已满18周岁,已阅读并同意:</View> |
| | | </View> |
| | | </View> |
| | | |
| | |
| | | } |
| | | |
| | | .origin-icon { |
| | | position: absolute; |
| | | bottom: 0px; |
| | | right: 24px; |
| | | width: 80px; |
| | | height: 80px; |
| | | } |
| | | .tips-and-origin { |
| | | position: relative; |
| | | width: 100%; |
| | | min-height: 80px; |
| | | } |
| | | } |
| | | |
| | |
| | | |
| | | .content-box-top { |
| | | width: 100%; |
| | | position: fixed; |
| | | top: 55%; |
| | | left: 0px; |
| | | z-index: 2; |
| | | |
| | | .position-box { |
| | | .is-operating-area { |
| | |
| | | } |
| | | } |
| | | |
| | | .ml-32 { |
| | | margin-left: 32px; |
| | | } |
| | | |
| | | .mt-20 { |
| | | margin-top: 20px; |
| | |
| | | QueryOrderPaymentResultResponse, |
| | | QueryLedgerCreditOrderResultResponse |
| | | } from './types/begin-route'; |
| | | import { withApiLog } from '../utils/api-logger'; |
| | | |
| | | // 导航-获取路线和费用 |
| | | export const queryRouteAndFare = async (payload: QueryRouteAndFarePayload): Promise<QueryRouteAndFareResponse> => { |
| | |
| | | |
| | | // 设置温度 |
| | | export const setTemperature = async (payload: SetTemperaturePayload): Promise<SetTemperatureResponse> => { |
| | | return withApiLog('设置空调温度', payload, request({ |
| | | return request({ |
| | | url: _ROBOTAXI_HOST + '/vehicleConvoy/api/v1/mini-program/order/ac/set-temperature', |
| | | method: 'POST', |
| | | payload, |
| | | showToast: false |
| | | }, true)); |
| | | }, true); |
| | | } |
| | | |
| | | // 设置温度并跳过业务异常拦截,供初始化越界温度纠正场景使用 |
| | | export const setTemperatureIgnoreBusinessError = async (payload: SetTemperaturePayload): Promise<SetTemperatureResponse> => { |
| | | return withApiLog('设置空调温度(忽略业务异常)', payload, request({ |
| | | return request({ |
| | | url: _ROBOTAXI_HOST + '/vehicleConvoy/api/v1/mini-program/order/ac/set-temperature', |
| | | method: 'POST', |
| | | payload, |
| | | skipException: true, |
| | | showToast: false |
| | | }, true)); |
| | | }, true); |
| | | } |
| | | |
| | | // 解锁车辆 |
| | |
| | | export const queryTemperatureByOrderId = async ( |
| | | payload: OrderIdPayload |
| | | ): Promise<QueryTemperatureByOrderIdResponse> => { |
| | | return withApiLog('查询车辆温度', payload, request({ |
| | | return request({ |
| | | url: _ROBOTAXI_HOST + '/vehicleConvoy/api/v1/mini-program/order/queryTemperatureByOrderId', |
| | | method: 'GET', |
| | | payload |
| | | }, true)); |
| | | }, true); |
| | | } |
| | | |
| | | // 查询车辆空调开关状态 |
| | | export const queryVehicleAirConditionerSwitch = async ( |
| | | payload: OrderIdPayload |
| | | ): Promise<QueryVehicleAirConditionerSwitchResponse> => { |
| | | return withApiLog('查询空调开关状态', payload, request({ |
| | | return request({ |
| | | url: _ROBOTAXI_HOST + '/vehicleConvoy/api/v1/mini-program/order/queryVehicleAirConditionerSwitch', |
| | | method: 'GET', |
| | | payload |
| | | }, true)); |
| | | }, true); |
| | | } |
| | | |
| | | // 车辆空调开关控制 |
| | | export const airConditionerUnlock = async (payload: AirConditionerUnlockPayload): Promise<void> => { |
| | | return withApiLog('空调开关控制', payload, request({ |
| | | return request({ |
| | | url: _ROBOTAXI_HOST + '/vehicleConvoy/api/v1/mini-program/order/airConditionerUnlock', |
| | | method: 'POST', |
| | | payload, |
| | | showToast: false |
| | | }, true)); |
| | | }, true); |
| | | } |
| | | |
| | | // 查询后备箱开启状态 |
| | |
| | | import request from '../../utils/request'; |
| | | import { _ROBOTAXI_HOST } from '../../constants/api'; |
| | | import { withApiLog } from '../utils/api-logger'; |
| | | import type { |
| | | QueryCabinFragranceResponse, |
| | | SetCabinFragrancePayload |
| | |
| | | export const queryCabinFragrance = async ( |
| | | orderId: string |
| | | ): Promise<QueryCabinFragranceResponse> => { |
| | | const payload = { orderId }; |
| | | return withApiLog('查询车载香氛状态', payload, request({ |
| | | return request({ |
| | | url: _ROBOTAXI_HOST + '/vehicleConvoy/base/v1/mini-program/cabin/fragrance', |
| | | method: 'GET', |
| | | payload |
| | | }, true)); |
| | | payload: { orderId } |
| | | }, true); |
| | | }; |
| | | |
| | | // 设置车载香氛(聚合下发开关状态,开启时同时下发香型通道) |
| | | export const setCabinFragrance = async ( |
| | | payload: SetCabinFragrancePayload |
| | | ): Promise<void> => { |
| | | return withApiLog('设置车载香氛', payload, request({ |
| | | return request({ |
| | | url: _ROBOTAXI_HOST + '/vehicleConvoy/base/v1/mini-program/cabin/fragrance', |
| | | method: 'POST', |
| | | payload, |
| | | skipException: true |
| | | }, true)); |
| | | }, true); |
| | | }; |
| New file |
| | |
| | | import request from '../../utils/request'; |
| | | import { _ROBOTAXI_HOST } from '../../constants/api'; |
| | | import type { |
| | | QueryOrderGenreResponse, |
| | | SetOrderGenrePayload |
| | | } from './types/genre'; |
| | | |
| | | const GENRE_API_URL = _ROBOTAXI_HOST + '/vehicleConvoy/api/v1/mini-program/order/genre'; |
| | | |
| | | // 查询当前订单曲风状态(音乐开关、当前曲风、已启用曲风列表) |
| | | export const queryOrderGenre = async ( |
| | | orderId: string |
| | | ): Promise<QueryOrderGenreResponse> => { |
| | | return request({ |
| | | url: GENRE_API_URL, |
| | | method: 'GET', |
| | | payload: { orderId } |
| | | }, true); |
| | | }; |
| | | |
| | | // 设置当前订单曲风(下发音乐开关状态,开启时同时下发曲风 ID) |
| | | export const setOrderGenre = async ( |
| | | payload: SetOrderGenrePayload |
| | | ): Promise<void> => { |
| | | return request({ |
| | | url: GENRE_API_URL, |
| | | method: 'POST', |
| | | payload, |
| | | skipException: true |
| | | }, true); |
| | | }; |
| | |
| | | import request from '../../utils/request'; |
| | | import { _ROBOTAXI_HOST } from '../../constants/api'; |
| | | import { withApiLog } from '../utils/api-logger'; |
| | | import type { QuerySeatHvResponse, SetSeatHvPayload } from './types/seat-hv'; |
| | | |
| | | // 查询座椅通风/加热状态 |
| | | export const querySeatHv = async (orderId: string): Promise<QuerySeatHvResponse> => { |
| | | const payload = { orderId }; |
| | | return withApiLog('查询座椅通风加热状态', payload, request({ |
| | | return request({ |
| | | url: _ROBOTAXI_HOST + '/vehicleConvoy/base/v1/mini-program/cabin/seat/hv', |
| | | method: 'GET', |
| | | payload |
| | | }, true)); |
| | | payload: { orderId } |
| | | }, true); |
| | | }; |
| | | |
| | | // 设置座椅通风/加热(seats 仅含被改动的座椅,通风与加热一起下发) |
| | | export const setSeatHv = async (payload: SetSeatHvPayload): Promise<void> => { |
| | | return withApiLog('设置座椅通风加热', payload, request({ |
| | | return request({ |
| | | url: _ROBOTAXI_HOST + '/vehicleConvoy/base/v1/mini-program/cabin/seat/hv', |
| | | method: 'POST', |
| | | payload, |
| | | skipException: true |
| | | }, true)); |
| | | }, true); |
| | | }; |
| New file |
| | |
| | | import type { OrderId } from './begin-route'; |
| | | |
| | | /** 单个曲风。 */ |
| | | export interface GenreItem { |
| | | /** 曲风唯一标识 / 曲风 ID(下发时使用)。 */ |
| | | id: string; |
| | | /** 曲风显示名称。 */ |
| | | name: string; |
| | | /** 曲风描述(可选,后端未下发时前端可留空)。 */ |
| | | desc?: string; |
| | | } |
| | | |
| | | /** 订单曲风状态对象。 */ |
| | | export interface OrderGenreState { |
| | | /** 音乐开关状态。 */ |
| | | switchOn: boolean; |
| | | /** 当前选中曲风 ID(关闭时可为空)。 */ |
| | | currentGenreId?: string; |
| | | /** 后台已启用曲风列表(动态),空数组表示暂无可选曲风。 */ |
| | | availableGenres?: GenreItem[]; |
| | | } |
| | | |
| | | /** 查询订单曲风状态响应对象。 */ |
| | | export interface QueryOrderGenreResponse { |
| | | /** 曲风状态。 */ |
| | | genre?: OrderGenreState; |
| | | } |
| | | |
| | | /** 设置订单曲风请求。 */ |
| | | export interface SetOrderGenrePayload { |
| | | /** 订单 ID。 */ |
| | | orderId: OrderId; |
| | | /** 目标开关状态。 */ |
| | | switchOn: boolean; |
| | | /** 目标曲风 ID;switchOn 为 true 时必传,false 时可不传。 */ |
| | | genreId?: string; |
| | | } |
| New file |
| | |
| | | .common-popup { |
| | | position: fixed; |
| | | top: 0; |
| | | left: 0; |
| | | right: 0; |
| | | bottom: 0; |
| | | z-index: 99999; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | |
| | | .common-mask { |
| | | position: absolute; |
| | | top: 0; |
| | | left: 0; |
| | | right: 0; |
| | | bottom: 0; |
| | | background: rgba(0, 0, 0, 0.5); |
| | | } |
| | | |
| | | .common-content { |
| | | position: relative; |
| | | width: 600rpx; |
| | | height: 388rpx; |
| | | box-sizing: border-box; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | padding: 72rpx 40rpx 48rpx 40rpx; |
| | | gap: 54rpx; |
| | | border-radius: 48rpx; |
| | | background: #FFFFFF; |
| | | |
| | | .close-icon { |
| | | position: absolute; |
| | | top: 24rpx; |
| | | right: 24rpx; |
| | | width: 48rpx; |
| | | height: 48rpx; |
| | | } |
| | | |
| | | .common-title { |
| | | font-family: PingFang SC; |
| | | font-size: 36rpx; |
| | | font-weight: 500; |
| | | line-height: 52rpx; |
| | | text-align: center; |
| | | letter-spacing: normal; |
| | | color: #141E27; |
| | | } |
| | | |
| | | .common-btn { |
| | | width: 404rpx; |
| | | height: 110rpx; |
| | | box-sizing: border-box; |
| | | display: flex; |
| | | flex-direction: column; |
| | | justify-content: center; |
| | | align-items: center; |
| | | padding: 32rpx 80rpx; |
| | | gap: 16rpx; |
| | | z-index: 0; |
| | | border-radius: 1000rpx; |
| | | border: 4rpx solid #141E27; |
| | | background: #FFFFFF; |
| | | font-family: PingFang SC; |
| | | font-size: 36rpx; |
| | | font-weight: 500; |
| | | line-height: 52rpx; |
| | | letter-spacing: normal; |
| | | color: #141E27; |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | import Taro from '@tarojs/taro'; |
| | | import { View, Image } from '@tarojs/components'; |
| | | import { useEffect, useState } from 'react'; |
| | | import './index.less' |
| | | |
| | | type Props = { |
| | | show?: boolean; |
| | | onClose?: () => void; |
| | | showCloseIcon?: boolean; |
| | | title?: string; |
| | | btnText?: string; |
| | | onBtnClick?: () => void; |
| | | } |
| | | |
| | | export default function CommonPopup({ show = false, onClose, showCloseIcon = true, title = '', btnText = '', onBtnClick }: Props) { |
| | | const [visible, setVisible] = useState(false); |
| | | const [animationData, setAnimationData] = useState<any>(); |
| | | |
| | | useEffect(() => { |
| | | if (show && !visible) { |
| | | setVisible(true); |
| | | const animation = Taro.createAnimation({ |
| | | duration: 300, |
| | | timingFunction: 'ease-out' |
| | | }); |
| | | animation.opacity(1).scale(1).step(); |
| | | setAnimationData(animation.export()); |
| | | } else if (!show && visible) { |
| | | const animation = Taro.createAnimation({ |
| | | duration: 300, |
| | | timingFunction: 'ease-in' |
| | | }); |
| | | animation.opacity(0).scale(0.8).step(); |
| | | setAnimationData(animation.export()); |
| | | |
| | | setTimeout(() => { |
| | | setVisible(false); |
| | | }, 300); |
| | | } |
| | | }, [show]); |
| | | |
| | | if (!visible) return null; |
| | | return ( |
| | | <View className='common-popup' catchMove> |
| | | <View className='common-mask'></View> |
| | | <View className='common-content' animation={animationData}> |
| | | {showCloseIcon && ( |
| | | <Image onClick={onClose} className='close-icon' src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/close-icon.png" /> |
| | | )} |
| | | <View className='common-title'> |
| | | {title} |
| | | </View> |
| | | <View onClick={onBtnClick} className='common-btn'>{btnText}</View> |
| | | </View> |
| | | </View> |
| | | ) |
| | | } |
| | |
| | | left: 0; |
| | | width: 100%; |
| | | height: 100%; |
| | | background: rgba(0, 0, 0, 0.4); |
| | | background: rgba(0, 0, 0, 0.8); |
| | | z-index: 1; |
| | | } |
| | | |
| | |
| | | left: 0; |
| | | right: 0; |
| | | bottom: 0; |
| | | background: rgba(0, 0, 0, 0.5); |
| | | background: rgba(0, 0, 0, 0.8); |
| | | } |
| | | |
| | | .coupon-detail-content { |
| | |
| | | left: 0; |
| | | right: 0; |
| | | bottom: 0; |
| | | background: rgba(0, 0, 0, 0.5); |
| | | background: rgba(0, 0, 0, 0.8); |
| | | } |
| | | |
| | | .customer-service-content { |
| | |
| | | background: #ffffff; |
| | | } |
| | | } |
| | | } |
| | | } |
| | |
| | | import { TripStatus } from '../../constants/trip-status'; |
| | | // TODO(仅用于测试): 上线前删除以下通用 Toast 组件的 Demo 引入 |
| | | import { showToast, hideToast } from '../toast'; |
| | | // TODO(仅用于测试): 上线前删除以下通用弹窗组件的 Demo 引入 |
| | | import CommonPopup from '../common-popup'; |
| | | |
| | | const DEMO_ORDER_ID = '2063814874050772993'; |
| | | |
| | |
| | | } |
| | | type State = { |
| | | tripStatus: TripStatus | number; |
| | | showCommonPopup: boolean; |
| | | } |
| | | |
| | | export default class Loading extends PureComponent<Props, State> { |
| | |
| | | constructor (props) { |
| | | super(props) |
| | | this.state = { |
| | | tripStatus: TripStatus.ArrivedDestination, |
| | | tripStatus: TripStatus.VehicleApproaching, |
| | | showCommonPopup: false, |
| | | } |
| | | } |
| | | |
| | |
| | | </View> |
| | | </View> |
| | | |
| | | <Text>通用弹窗 Demo</Text> |
| | | {/* TODO(仅用于测试): 上线前删除 CommonPopup 测试渲染 */} |
| | | <View className='toast-demo'> |
| | | <View |
| | | className='toast-demo__btn' |
| | | onClick={() => this.setState({ showCommonPopup: true })} |
| | | > |
| | | 打开通用弹窗 |
| | | </View> |
| | | </View> |
| | | <CommonPopup |
| | | show={this.state.showCommonPopup} |
| | | showCloseIcon |
| | | title='这是一个通用弹窗的提示文案' |
| | | btnText='知道了' |
| | | onClose={() => this.setState({ showCommonPopup: false })} |
| | | onBtnClick={() => { |
| | | showToast({ title: '按钮点击(Demo)', icon: 'none' }); |
| | | this.setState({ showCommonPopup: false }); |
| | | }} |
| | | /> |
| | | |
| | | <Text>空调卡片 Demo</Text> |
| | | {/* TODO(仅用于测试): 上线前删除 TemperatureControl 测试渲染,.calling-car 包裹仅用于补全样式层级 */} |
| | | <View className='calling-car'> |
| | |
| | | left: 0; |
| | | right: 0; |
| | | bottom: 0; |
| | | background: rgba(0, 0, 0, 0.5); |
| | | background: rgba(0, 0, 0, 0.8); |
| | | } |
| | | |
| | | .tip-content { |
| | |
| | | {shouldRenderRight && ( |
| | | <View className="bluetooth-operate-card-right"> |
| | | {/* {!!actionText && <View>{actionText}</View>} */} |
| | | <Image src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/blue-right-icon-20260706.png" /> |
| | | <Image src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/bluetooth-arrow-20260706.png" /> |
| | | </View> |
| | | )} |
| | | </View> |
| | |
| | | </View> |
| | | <View className={`car-card-arrive-item ${lockStatus && 'close-btn-item'} ${loadingControlType ? 'trip-action-bar-item-disabled' : ''}`.trim()} onClick={() => handleUnlock('door', onDoorUnlockSuccess)}> |
| | | <View className="car-card-arrive-item-text">车锁</View> |
| | | <View className="car-card-arrive-item-status">{lockStatus ? '点击关闭' : '点击打开'}</View> |
| | | <View className="car-card-arrive-item-status">{lockStatus ? '车门已解锁' : '点击打开'}</View> |
| | | {renderControlIcon('door', lockStatus ? LOCK_OPEN_ICON_URL : LOCK_CLOSE_ICON_URL, 'car-card-arrive-item-img')} |
| | | </View> |
| | | </View> |
| | |
| | | type Props = { |
| | | tripStatus: TripStatus; |
| | | fareBreakdown: any; |
| | | manuallySelectedCouponName?: string; |
| | | orderId?: string; |
| | | resetDetailVersion?: number; |
| | | onToggleDetailFade?: (expanded: boolean, hasFare: boolean) => void; |
| | |
| | | export default function FareDetailCard({ |
| | | tripStatus, |
| | | fareBreakdown, |
| | | manuallySelectedCouponName = '', |
| | | orderId, |
| | | resetDetailVersion, |
| | | onToggleDetailFade, |
| | |
| | | }: Props) { |
| | | const [showDetail, setShowDetail] = useState(false); // 费用明细展开状态 |
| | | const canOpenCouponSelect = tripStatus === TripStatus.TripFinished && typeof onOpenCouponSelect === 'function'; // 仅待支付支持切换优惠券 |
| | | |
| | | const discountTitleText = manuallySelectedCouponName || '最佳优惠'; |
| | | const discountDescText = manuallySelectedCouponName |
| | | ? `已选择${manuallySelectedCouponName}` |
| | | : '已选择最佳优惠'; |
| | | const isCreditPayProcessing = ( |
| | | tripStatus === TripStatus.TripFinished |
| | | && !!orderId |
| | |
| | | (fareBreakdown?.operateFree != 1 && [TripStatus.TripFinished].includes(tripStatus)) && Number(fareBreakdown?.redPacketMoney + fareBreakdown?.couponMoney) > 0 && |
| | | <View className="car-card-end-through" onClick={canOpenCouponSelect ? onOpenCouponSelect : undefined}> |
| | | <View className="car-card-end-through-left"> |
| | | <View className="car-card-end-through-content-title">最佳优惠</View> |
| | | <View className="car-card-end-through-desc">已为您选择最佳优惠</View> |
| | | <View className="car-card-end-through-content-title">{discountTitleText}</View> |
| | | <View className="car-card-end-through-desc">{discountDescText}</View> |
| | | </View> |
| | | <View className="car-card-end-through-right"> |
| | | <View className="car-card-end-through-content-money">-{formatMoney(fareBreakdown?.redPacketMoney + fareBreakdown?.couponMoney)}元</View> |
| | |
| | | import { getOrderId } from '../../../../../utils/order'; |
| | | import { TripStatus } from '../../../../../constants/trip-status'; |
| | | import useBottomPopupAnimation from '../../hooks/use-bottom-popup-animation'; |
| | | import AcSwitch from '../ac-switch'; |
| | | import '../../css-folder/fragrance-control.less'; |
| | | |
| | | const FC_HEADER_ICON = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/fragrance-icon-20260716.png'; |
| | |
| | | const cnNum = scentCount <= 10 ? CN_NUMBERS[scentCount] : String(scentCount); |
| | | return `可选${cnNum}款香型`; |
| | | } |
| | | const FRAGRANCE_OPEN_SUCCESS_TEXT = '已开启香氛'; // 开启香氛成功提示文案 |
| | | const FRAGRANCE_OPEN_PREFIX = '已开启香氛:'; // 开启/切换香氛成功提示前缀(后接香氛名称) |
| | | const CLOSE_SUCCESS_TEXT = '已关闭香氛'; |
| | | const PANEL_CLOSE_DELAY_MS = 150; // 下发成功后延迟关闭弹窗,优化交互体验 |
| | | const APPLY_FAIL_TEXT = '网络异常,请重试'; |
| | | |
| | | /** 香调描述本地兜底表:接口未返回 desc 时按 id 取用。 */ |
| | |
| | | } |
| | | |
| | | /** |
| | | * 计算车端开启态下当前生效的香型通道。 |
| | | * 计算初始选中香型通道:优先车端当前香型,其次列表首项(关闭态也保留选中,用于卡片展示当前香氛)。 |
| | | */ |
| | | function resolveActiveChannelId(scents: FragranceScent[], switchOn: boolean, currentScent?: string): string { |
| | | if (!switchOn || !scents.length) { |
| | | return ''; |
| | | function resolveInitialChannelId(scents: FragranceScent[], currentScent?: string): string { |
| | | if (currentScent) { |
| | | const matched = scents.find((item) => item.id === currentScent); |
| | | if (matched) { |
| | | return matched.channelId; |
| | | } |
| | | } |
| | | const matched = currentScent ? scents.find((item) => item.id === currentScent) : undefined; |
| | | return (matched || scents[0]).channelId; |
| | | return scents.length ? scents[0].channelId : ''; |
| | | } |
| | | |
| | | export default function FragranceControl({ tripStatus, orderId, pageDidShowVersion = 0, hasPreviousOrder = false }: Props) { |
| | | // 车端真实状态 |
| | | const [switchOn, setSwitchOn] = useState(false); |
| | | const [scents, setScents] = useState<FragranceScent[]>([]); |
| | | // 当前生效香型通道(关闭时为空) |
| | | const [activeChannelId, setActiveChannelId] = useState(''); |
| | | // 卡片主标题(开启时展示香型名,关闭时展示"已关闭") |
| | | const [cardTitle, setCardTitle] = useState(CLOSED_TITLE); |
| | | // 当前选中香型通道(关闭态也保留,用于卡片展示当前香氛) |
| | | const [selectedChannelId, setSelectedChannelId] = useState(''); |
| | | // 面板显隐 + 下发锁 |
| | | const [panelVisible, setPanelVisible] = useState(false); |
| | | const [applying, setApplying] = useState(false); |
| | |
| | | |
| | | const { animationData, shouldRender } = useBottomPopupAnimation(panelVisible); |
| | | const hasScents = scents.length > 0; |
| | | const selectedScent = findScentByChannel(scents, selectedChannelId); |
| | | // 卡片主标题:始终展示当前选中香型名(关闭态由样式置灰),无香型时兜底"已关闭" |
| | | const cardTitle = selectedScent?.name || (hasScents ? scents[0].name : CLOSED_TITLE); |
| | | // 开关 loading 仅在由开关(而非香型磁贴)触发下发时展示 |
| | | const switchLoading = applying && applyingChannelId === ''; |
| | | |
| | | // 进入接驾态查询香氛状态;连环单不查询、不展示 |
| | | useEffect(() => { |
| | |
| | | const fragrance = res || {}; |
| | | const nextSwitchOn = fragrance.switchOn === true; |
| | | const nextScents: FragranceScent[] = Array.isArray(fragrance.availableScents) ? fragrance.availableScents : []; |
| | | const nextActiveChannelId = resolveActiveChannelId(nextScents, nextSwitchOn, fragrance.currentScent); |
| | | setSwitchOn(nextSwitchOn); |
| | | setScents(nextScents); |
| | | setActiveChannelId(nextActiveChannelId); |
| | | const activeName = findScentByChannel(nextScents, nextActiveChannelId)?.name || ''; |
| | | setCardTitle(nextSwitchOn && activeName ? activeName : CLOSED_TITLE); |
| | | setSelectedChannelId(resolveInitialChannelId(nextScents, fragrance.currentScent)); |
| | | }).catch(() => { |
| | | if (querySeqRef.current !== snapshotSeq) { |
| | | return; |
| | |
| | | // 查询失败按「已关闭」兜底 |
| | | setSwitchOn(false); |
| | | setScents([]); |
| | | setActiveChannelId(''); |
| | | setCardTitle(CLOSED_TITLE); |
| | | setSelectedChannelId(''); |
| | | }); |
| | | }, [tripStatus, orderId, pageDidShowVersion, hasPreviousOrder]); |
| | | |
| | |
| | | setPanelVisible(false); |
| | | }; |
| | | |
| | | // 点击香型即时下发:点选中项 → 关闭香氛;点其它项 → 开启并切换香型 |
| | | const handleScentClick = async (channelId: string) => { |
| | | if (applying) { |
| | | // 点击卡片:只有一个香氛时卡片本体不可点击(仅右上角开关控制);多个香氛时打开选择面板 |
| | | const handleCardClick = () => { |
| | | if (scents.length <= 1) { |
| | | return; |
| | | } |
| | | togglePanel(); |
| | | }; |
| | | |
| | | // 点击香型即时下发:点选中项 → 关闭香氛;点其它项 → 开启并切换香型 |
| | | /** |
| | | * 下发香氛设置到车端;成功后同步开关与选中香型、关闭弹窗并 toast,失败提示重试。 |
| | | * @param loadingChannelId 触发下发的香型通道 ID(用于在该磁贴展示 loading);由开关触发时传空串。 |
| | | */ |
| | | const applyFragrance = async (nextSwitchOn: boolean, channelId: string, loadingChannelId = '') => { |
| | | const targetOrderId = getOrderId(orderId); |
| | | if (!targetOrderId) { |
| | | return; |
| | | } |
| | | // 点击当前生效香型 → 关闭;否则 → 开启该香型 |
| | | const turningOff = switchOn && channelId === activeChannelId; |
| | | const nextSwitchOn = !turningOff; |
| | | setApplying(true); |
| | | setApplyingChannelId(channelId); |
| | | setApplyingChannelId(loadingChannelId); |
| | | try { |
| | | await setCabinFragrance({ |
| | | orderId: targetOrderId, |
| | | switchOn: nextSwitchOn, |
| | | ...(nextSwitchOn ? { channelId } : {}) |
| | | }); |
| | | // 下发成功后同步车端真实状态与卡片主标题 |
| | | // 下发成功后同步车端真实状态(关闭态保留选中香型不重置) |
| | | setSwitchOn(nextSwitchOn); |
| | | const nextActiveChannelId = nextSwitchOn ? channelId : ''; |
| | | setActiveChannelId(nextActiveChannelId); |
| | | const activeName = findScentByChannel(scents, nextActiveChannelId)?.name || ''; |
| | | setCardTitle(nextSwitchOn && activeName ? activeName : CLOSED_TITLE); |
| | | // 开启/关闭香氛均使用通用 Toast 组件提示(success),并关闭香氛设置弹窗 |
| | | showToast({ title: nextSwitchOn ? FRAGRANCE_OPEN_SUCCESS_TEXT : CLOSE_SUCCESS_TEXT, icon: 'success' }); |
| | | closePanel(); |
| | | if (nextSwitchOn && channelId) { |
| | | setSelectedChannelId(channelId); |
| | | } |
| | | // 开启/关闭香氛:通用 Toast 提示(success),并延迟 150ms 关闭香氛设置弹窗(优化交互体验) |
| | | const appliedScentName = findScentByChannel(scents, channelId)?.name || ''; |
| | | showToast({ title: nextSwitchOn ? `${FRAGRANCE_OPEN_PREFIX}${appliedScentName}` : CLOSE_SUCCESS_TEXT, icon: 'success' }); |
| | | setTimeout(() => closePanel(), PANEL_CLOSE_DELAY_MS); |
| | | } catch (error) { |
| | | showToast({ title: APPLY_FAIL_TEXT, icon: 'none' }); |
| | | } finally { |
| | | setApplying(false); |
| | | setApplyingChannelId(''); |
| | | } |
| | | }; |
| | | |
| | | // 点击香型磁贴:点当前生效香型 → 关闭;点其它 → 开启并切换到该香型 |
| | | const handleScentClick = (channelId: string) => { |
| | | if (applying) { |
| | | return; |
| | | } |
| | | const turningOff = switchOn && channelId === selectedChannelId; |
| | | applyFragrance(!turningOff, channelId, channelId); |
| | | }; |
| | | |
| | | // 切换总开关(卡片主开关 / 弹窗底部开关共用):打开→下发当前选中香型;关闭→下发关闭并保留选中 |
| | | const handleToggleSwitch = () => { |
| | | if (applying || !hasScents) { |
| | | return; |
| | | } |
| | | const nextSwitchOn = !switchOn; |
| | | const targetChannelId = selectedChannelId || scents[0]?.channelId || ''; |
| | | applyFragrance(nextSwitchOn, targetChannelId, ''); |
| | | }; |
| | | |
| | | // 连环单或非接驾态不展示香氛卡片 |
| | |
| | | {/* 香氛入口卡片(复用 tf-card 视觉):点击打开面板 */} |
| | | <View |
| | | className={`tf-card tf-card-fragrance ${fragranceCardStateClass}`} |
| | | onClick={togglePanel} |
| | | onClick={handleCardClick} |
| | | > |
| | | <View className='tf-card-header'> |
| | | <View className='tf-card-header-left'> |
| | | <Image className='tf-card-header-icon' src={FC_HEADER_ICON} /> |
| | | <Text className='tf-card-header-title'>香氛</Text> |
| | | </View> |
| | | <AcSwitch checked={switchOn} loading={switchLoading} onChange={handleToggleSwitch} /> |
| | | </View> |
| | | <View className='tf-card-bottom'> |
| | | <View className='tf-card-fragrance-name'>{cardTitle}</View> |
| | | <View className='tf-card-footer'> |
| | | <Text className='tf-card-footer-text'>{resolveCardSubtitle(scents.length)}</Text> |
| | | <Image className='tf-card-footer-arrow' src={ARROW_ICON_URL} /> |
| | | {scents.length > 1 && <Image className='tf-card-footer-arrow' src={ARROW_ICON_URL} />} |
| | | </View> |
| | | </View> |
| | | </View> |
| | |
| | | {hasScents ? ( |
| | | <View className='popup-scent-list'> |
| | | {scents.map((item) => { |
| | | const isActive = switchOn && item.channelId === activeChannelId; |
| | | const isActive = switchOn && item.channelId === selectedChannelId; |
| | | const isLoading = applyingChannelId === item.channelId; |
| | | const desc = resolveScentDesc(item); |
| | | return ( |
| New file |
| | |
| | | import { View, Text, Image } from '@tarojs/components'; |
| | | import { useEffect, useRef, useState } from 'react'; |
| | | import { queryOrderGenre, setOrderGenre } from '../../../../../api/genre'; |
| | | import type { GenreItem } from '../../../../../api/types/genre'; |
| | | import { getOrderId } from '../../../../../utils/order'; |
| | | import { TripStatus } from '../../../../../constants/trip-status'; |
| | | import useBottomPopupAnimation from '../../hooks/use-bottom-popup-animation'; |
| | | import AcSwitch from '../ac-switch'; |
| | | import { showToast } from '../../../../../components/toast'; |
| | | import '../../css-folder/music-control.less'; |
| | | |
| | | const GENRE_HEADER_ICON = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/genre-0731@2x.png'; |
| | | const ARROW_ICON_URL = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/temperature-fragrance-arrow-20260706.png'; |
| | | const CLOSE_ICON_URL = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/close-icon.png'; |
| | | const LOADING_ICON_URL = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/T-switch-loading-20260715.png'; |
| | | |
| | | const CARD_DEFAULT_TITLE = '车载音乐'; // 无可选曲风时卡片主标题兜底 |
| | | const NO_GENRE_TIP = '当前暂无可选曲风'; // 空曲风列表占位 |
| | | const PLAY_TIP_TEXT = '音乐将在您上车后开始播放'; // 弹窗底部开关文案 |
| | | const OPEN_PLAY_PREFIX = '上车后将自动播放:'; // 开启/切换曲风成功提示前缀(后接曲风名称) |
| | | const CLOSE_SUCCESS_TEXT = '音乐已关闭'; |
| | | const PANEL_CLOSE_DELAY_MS = 150; // 下发成功后延迟关闭弹窗,优化交互体验 |
| | | const APPLY_FAIL_TEXT = '网络异常,请重试'; |
| | | // 车端下发结果失败标识:接口 code 可能为 200,但 data 返回 'FAILED' 表示车端下发失败 |
| | | const APPLY_FAIL_RESULT = 'FAILED'; |
| | | |
| | | // 卡片副标题轮播文案(上下滚动轮播,动效由 CSS 实现) |
| | | const SUBTITLE_TEXTS = [ |
| | | '下一站,跟着节奏出发', |
| | | '好音乐,陪你出发', |
| | | '长风相送,雅韵随行', |
| | | '带上音乐,轻松启程', |
| | | '给旅程多一些旋律' |
| | | ]; |
| | | |
| | | type Props = { |
| | | tripStatus: TripStatus; |
| | | orderId?: string; |
| | | pageDidShowVersion?: number; |
| | | /** 后端推送:当前被指派车辆是否仍在执行上一单(连环单) */ |
| | | hasPreviousOrder?: boolean; |
| | | }; |
| | | |
| | | /** |
| | | * 从曲风列表中按 ID 查找曲风。 |
| | | */ |
| | | function findGenreById(genres: GenreItem[], genreId: string): GenreItem | undefined { |
| | | return genres.find((item) => item.id === genreId); |
| | | } |
| | | |
| | | /** |
| | | * 计算初始选中曲风:优先车端当前曲风,其次列表首项。 |
| | | */ |
| | | function resolveInitialGenreId(genres: GenreItem[], currentGenreId?: string): string { |
| | | if (currentGenreId && findGenreById(genres, currentGenreId)) { |
| | | return currentGenreId; |
| | | } |
| | | return genres.length ? genres[0].id : ''; |
| | | } |
| | | |
| | | export default function MusicControl({ tripStatus, orderId, pageDidShowVersion = 0, hasPreviousOrder = false }: Props) { |
| | | // 车端真实开关状态 |
| | | const [switchOn, setSwitchOn] = useState(false); |
| | | // 后台配置曲风列表 |
| | | const [genres, setGenres] = useState<GenreItem[]>([]); |
| | | // 当前选中曲风 ID(关闭态下也可有值,仅本地记录,不下发) |
| | | const [selectedGenreId, setSelectedGenreId] = useState(''); |
| | | // 面板显隐 + 下发锁 |
| | | const [panelVisible, setPanelVisible] = useState(false); |
| | | const [applying, setApplying] = useState(false); |
| | | // 正在下发的曲风磁贴 ID(该磁贴显示 loading),空串表示由开关触发(loading 落在开关上) |
| | | const [applyingGenreId, setApplyingGenreId] = useState(''); |
| | | const querySeqRef = useRef(0); |
| | | // 记录本次接驾态周期内已查询过曲风状态的订单 ID,避免重复请求(同一订单在初始化恢复 |
| | | // 流程中 tripStatus/orderId/hasPreviousOrder 会分批异步更新,触发本 effect 多次)。 |
| | | const queriedOrderIdRef = useRef(''); |
| | | |
| | | const { animationData, shouldRender } = useBottomPopupAnimation(panelVisible); |
| | | const hasGenres = genres.length > 0; |
| | | const selectedGenre = findGenreById(genres, selectedGenreId); |
| | | // 卡片主标题:始终展示曲风名(关闭态由样式置灰),无曲风时兜底 |
| | | const cardTitle = selectedGenre?.name || (hasGenres ? genres[0].name : CARD_DEFAULT_TITLE); |
| | | // 开关 loading 仅在由开关(而非磁贴)触发下发时展示 |
| | | const switchLoading = applying && applyingGenreId === ''; |
| | | |
| | | // 进入接驾态查询曲风状态;连环单不查询、不展示 |
| | | useEffect(() => { |
| | | if (tripStatus !== TripStatus.VehicleApproaching || hasPreviousOrder) { |
| | | // 离开接驾态:重置去重标记,允许下次进入接驾态时重新查询一次。 |
| | | queriedOrderIdRef.current = ''; |
| | | return; |
| | | } |
| | | const targetOrderId = getOrderId(orderId); |
| | | if (!targetOrderId) { |
| | | return; |
| | | } |
| | | // 去重:同一订单在本次接驾态周期内只查询一次。 |
| | | if (queriedOrderIdRef.current === targetOrderId) { |
| | | return; |
| | | } |
| | | queriedOrderIdRef.current = targetOrderId; |
| | | const snapshotSeq = ++querySeqRef.current; |
| | | queryOrderGenre(targetOrderId).then((res: any) => { |
| | | if (querySeqRef.current !== snapshotSeq) { |
| | | return; |
| | | } |
| | | const genre = res || {}; |
| | | const nextSwitchOn = genre.switchOn === true; |
| | | const nextGenres: GenreItem[] = Array.isArray(genre.availableGenres) ? genre.availableGenres : []; |
| | | setSwitchOn(nextSwitchOn); |
| | | setGenres(nextGenres); |
| | | setSelectedGenreId(resolveInitialGenreId(nextGenres, genre.currentGenreId)); |
| | | }).catch(() => { |
| | | if (querySeqRef.current !== snapshotSeq) { |
| | | return; |
| | | } |
| | | // 查询失败按「已关闭」兜底 |
| | | setSwitchOn(false); |
| | | setGenres([]); |
| | | setSelectedGenreId(''); |
| | | }); |
| | | }, [tripStatus, orderId, pageDidShowVersion, hasPreviousOrder]); |
| | | |
| | | // 点击卡片:切换面板开合 |
| | | const togglePanel = () => { |
| | | setPanelVisible((prev) => !prev); |
| | | }; |
| | | |
| | | const closePanel = () => { |
| | | setPanelVisible(false); |
| | | }; |
| | | |
| | | /** |
| | | * 下发曲风设置到车端;成功后同步开关状态、关闭弹窗并 toast,失败回滚并提示。 |
| | | * @param loadingGenreId 触发下发的曲风磁贴 ID(用于在该磁贴上展示 loading);由开关触发时传空串。 |
| | | */ |
| | | const applyGenre = async (nextSwitchOn: boolean, genreId: string, loadingGenreId = '') => { |
| | | const targetOrderId = getOrderId(orderId); |
| | | if (!targetOrderId) { |
| | | return; |
| | | } |
| | | setApplying(true); |
| | | setApplyingGenreId(loadingGenreId); |
| | | try { |
| | | const res: any = await setOrderGenre({ |
| | | orderId: targetOrderId, |
| | | switchOn: nextSwitchOn, |
| | | ...(nextSwitchOn ? { genreId } : {}) |
| | | }); |
| | | // skipException 下业务失败会以数据形式 resolve,需显式校验: |
| | | // ① HTTP 状态码非 200;② code 为 200 但 data 返回 'FAILED'(车端下发失败) |
| | | if (res && ((res.code !== undefined && res.code !== 200) || res.data === APPLY_FAIL_RESULT)) { |
| | | throw res; |
| | | } |
| | | setSwitchOn(nextSwitchOn); |
| | | if (nextSwitchOn && genreId) { |
| | | setSelectedGenreId(genreId); |
| | | } |
| | | // 打开/关闭成功:toast 提示,并延迟 150ms 关闭控制弹窗(优化交互体验) |
| | | const appliedGenreName = findGenreById(genres, genreId)?.name || ''; |
| | | showToast({ title: nextSwitchOn ? `${OPEN_PLAY_PREFIX}${appliedGenreName}` : CLOSE_SUCCESS_TEXT, icon: 'success' }); |
| | | setTimeout(() => setPanelVisible(false), PANEL_CLOSE_DELAY_MS); |
| | | } catch (error) { |
| | | // 下发失败:不改动本地状态(回滚),仅提示 |
| | | showToast({ title: APPLY_FAIL_TEXT, icon: 'none' }); |
| | | } finally { |
| | | setApplying(false); |
| | | setApplyingGenreId(''); |
| | | } |
| | | }; |
| | | |
| | | /** |
| | | * 点击曲风磁贴:点当前生效曲风 → 关闭;点其它曲风 → 开启并切换到该曲风。 |
| | | * 该磁贴上方展示 loading,成功后同步底部总开关状态。 |
| | | */ |
| | | const handleGenreClick = (genreId: string) => { |
| | | if (applying) { |
| | | return; |
| | | } |
| | | const turningOff = switchOn && genreId === selectedGenreId; |
| | | applyGenre(!turningOff, genreId, genreId); |
| | | }; |
| | | |
| | | /** |
| | | * 切换总开关(卡片主开关 / 弹窗底部开关共用): |
| | | * - 打开:下发 switchOn=true + 当前选中曲风(无则取列表首项); |
| | | * - 关闭:下发 switchOn=false,保留已选曲风不重置。 |
| | | */ |
| | | const handleToggleSwitch = () => { |
| | | if (applying || !hasGenres) { |
| | | return; |
| | | } |
| | | const nextSwitchOn = !switchOn; |
| | | const targetGenreId = selectedGenreId || genres[0]?.id || ''; |
| | | applyGenre(nextSwitchOn, targetGenreId, ''); |
| | | }; |
| | | |
| | | // 连环单或非接驾态不展示音乐卡片 |
| | | if (tripStatus !== TripStatus.VehicleApproaching || hasPreviousOrder) { |
| | | return null; |
| | | } |
| | | |
| | | // 音乐卡片背景随开关状态切换 |
| | | const musicCardStateClass = switchOn ? 'is-open' : 'is-close'; |
| | | |
| | | return ( |
| | | <View className='music-control'> |
| | | {/* 音乐入口卡片(复用 tf-card 视觉):点击打开面板 */} |
| | | <View |
| | | className={`tf-card tf-card-music ${musicCardStateClass}`} |
| | | onClick={togglePanel} |
| | | > |
| | | <View className='tf-card-header'> |
| | | <View className='tf-card-header-left'> |
| | | <Image className='tf-card-header-icon' src={GENRE_HEADER_ICON} /> |
| | | <Text className='tf-card-header-title'>音乐</Text> |
| | | </View> |
| | | <AcSwitch |
| | | checked={switchOn} |
| | | loading={switchLoading} |
| | | onChange={handleToggleSwitch} |
| | | /> |
| | | </View> |
| | | <View className='tf-card-bottom'> |
| | | <View className='tf-card-music-name'>{cardTitle}</View> |
| | | <View className='tf-card-footer'> |
| | | {/* 副标题上下滚动轮播:末尾克隆首条实现无缝循环,动效由 CSS 驱动 */} |
| | | <View className='tf-card-footer-text tf-card-subtitle'> |
| | | <View className='tf-card-subtitle-track'> |
| | | {SUBTITLE_TEXTS.map((text, idx) => ( |
| | | <View key={idx} className='tf-card-subtitle-item'>{text}</View> |
| | | ))} |
| | | <View className='tf-card-subtitle-item'>{SUBTITLE_TEXTS[0]}</View> |
| | | </View> |
| | | </View> |
| | | <Image className='tf-card-footer-arrow' src={ARROW_ICON_URL} /> |
| | | </View> |
| | | </View> |
| | | </View> |
| | | |
| | | {/* 控制面板(底部弹窗) */} |
| | | {shouldRender && ( |
| | | <View className={`music-popup ${panelVisible ? 'show' : ''}`} catchMove> |
| | | <View className='popup-mask' onClick={closePanel}></View> |
| | | <View className='popup-content' animation={animationData}> |
| | | <View className='popup-header'> |
| | | <Text className='popup-title'>车载音乐</Text> |
| | | <Image className='popup-close' src={CLOSE_ICON_URL} onClick={closePanel} /> |
| | | </View> |
| | | |
| | | <View className='popup-genre-area'> |
| | | {hasGenres ? ( |
| | | <View className='popup-genre-list'> |
| | | {genres.map((item) => { |
| | | const isActive = switchOn && item.id === selectedGenreId; |
| | | const isLoading = applyingGenreId === item.id; |
| | | return ( |
| | | <View |
| | | key={item.id} |
| | | className={`popup-genre-item ${isActive ? 'active' : ''} ${applying ? 'disabled' : ''}`} |
| | | onClick={() => handleGenreClick(item.id)} |
| | | > |
| | | { |
| | | isLoading ? |
| | | <Image className='popup-genre-loading' src={LOADING_ICON_URL} /> : |
| | | <Text className='popup-genre-name'>{item.name}</Text> |
| | | } |
| | | </View> |
| | | ); |
| | | })} |
| | | </View> |
| | | ) : ( |
| | | <View className='popup-genre-empty'>{NO_GENRE_TIP}</View> |
| | | )} |
| | | </View> |
| | | |
| | | {/* 底部播放开关 */} |
| | | <View className='popup-play-switch'> |
| | | <Text className='popup-play-text'>{PLAY_TIP_TEXT}</Text> |
| | | <AcSwitch checked={switchOn} loading={switchLoading} onChange={handleToggleSwitch} /> |
| | | </View> |
| | | </View> |
| | | </View> |
| | | )} |
| | | </View> |
| | | ); |
| | | } |
| | |
| | | import Taro from '@tarojs/taro'; |
| | | import { showToast } from '../../../../../components/toast'; |
| | | import { View, Image } from '@tarojs/components'; |
| | | import { View, Image, Text } from '@tarojs/components'; |
| | | import { useEffect, useRef, useState } from 'react'; |
| | | import { querySeatHv, setSeatHv } from '../../../../../api/seat-hv'; |
| | | import type { SeatHvItem, SeatSetItem } from '../../../../../api/types/seat-hv'; |
| | |
| | | import { TripStatus } from '../../../../../constants/trip-status'; |
| | | import '../../css-folder/seat-hv-control.less'; |
| | | |
| | | const SEAT_BG_URL = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/seat_bg_0715.png'; |
| | | const SEAT_BG_URL = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/seat_bg_0805@2x.png'; |
| | | const IMG_BASE = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/'; |
| | | const LOADING_ICON_URL = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/T-switch-loading-20260715.png'; |
| | | const NO_SEAT_TIP = '座椅调节暂不可用'; |
| | |
| | | {ventilationLoading |
| | | ? <Image className='seat-hv-btn-icon seat-hv-btn-loading' src={LOADING_ICON_URL} /> |
| | | : <Image className='seat-hv-btn-icon' src={resolveSeatIcon('ventilation', seat.ventilation)} />} |
| | | <Text className='seat-hv-btn-label'>通风</Text> |
| | | </View> |
| | | )} |
| | | {isPillLayout && <View className='seat-hv-divider'></View>} |
| | |
| | | {heatingLoading |
| | | ? <Image className='seat-hv-btn-icon seat-hv-btn-loading' src={LOADING_ICON_URL} /> |
| | | : <Image className='seat-hv-btn-icon' src={resolveSeatIcon('heating', seat.heating)} />} |
| | | <Text className='seat-hv-btn-label'>加热</Text> |
| | | </View> |
| | | )} |
| | | </View> |
| | |
| | | |
| | | {/* 空调温度滑动选择(底部白色圆角卡片) */} |
| | | <View className='popup-ac'> |
| | | <Text className='popup-ac-title'>温度调节</Text> |
| | | <Text className='popup-ac-title'>二排温度调节</Text> |
| | | <View className='popup-temp-wrap'> |
| | | <ScrollView |
| | | className='popup-temp-scroll' |
| | |
| | | import { View, Image } from "@tarojs/components"; |
| | | import { View, Image, ScrollView } from "@tarojs/components"; |
| | | import { useState, useEffect } from 'react'; |
| | | import './css-folder/calling-car.less' |
| | | import { formatRemainingDurationDisplay } from '../../../utils/format'; |
| | |
| | | import VehicleInfoCard from './calling-car-modules/vehicle-info-card'; // 车辆信息卡片 |
| | | import TemperatureControl from './calling-car-modules/temperature-control'; // 空调温控(含温度调节面板 + 座椅通风加热) |
| | | import FragranceControl from './calling-car-modules/fragrance-control'; // 车载香氛控制 |
| | | import MusicControl from './calling-car-modules/music-control'; // 车载音乐曲风控制 |
| | | import TripActionBar from './calling-car-modules/trip-action-bar'; // 行程操作栏 |
| | | import DoorTrunkControl from './calling-car-modules/door-trunk-control'; // 门箱控制 |
| | | import FareDetailCard from './calling-car-modules/fare-detail-card'; // 费用明细卡片 |
| | |
| | | finishCountdownSeconds?: number | null; |
| | | /** 费用明细 */ |
| | | fareBreakdown?: any; |
| | | /** 当前流程内用户手动切换后的优惠券名称 */ |
| | | manuallySelectedCouponName?: string; |
| | | /** 车辆信息 */ |
| | | vehicleInfo?: { |
| | | vehiclePictures?: string; |
| | |
| | | pickupCountdownSeconds = null, |
| | | finishCountdownSeconds = null, |
| | | fareBreakdown = null, |
| | | manuallySelectedCouponName = '', |
| | | vehicleInfo, |
| | | orderId, |
| | | hasBeforeUserEndSite = false, |
| | |
| | | { |
| | | [TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup, TripStatus.InService].includes(tripStatus) && |
| | | <View className='car-card'> |
| | | <View className='temp-fragrance-control'> |
| | | <TemperatureControl tripStatus={tripStatus} orderId={orderId} pageDidShowVersion={pageDidShowVersion} hasPreviousOrder={hasPreviousOrder} /> |
| | | <FragranceControl tripStatus={tripStatus} orderId={orderId} pageDidShowVersion={pageDidShowVersion} hasPreviousOrder={hasPreviousOrder} /> |
| | | </View> |
| | | {/* 车控卡片横向滑动(仅接驾态展示):weapp 中普通 View 的 overflow 滚动无法响应拖动,必须用 ScrollView */} |
| | | { |
| | | TripStatus.VehicleApproaching === tripStatus && |
| | | <ScrollView className='temp-fragrance-scroll' scrollX showScrollbar={false}> |
| | | <View className='temp-fragrance-control'> |
| | | <TemperatureControl tripStatus={tripStatus} orderId={orderId} pageDidShowVersion={pageDidShowVersion} hasPreviousOrder={hasPreviousOrder} /> |
| | | <MusicControl tripStatus={tripStatus} orderId={orderId} pageDidShowVersion={pageDidShowVersion} hasPreviousOrder={hasPreviousOrder} /> |
| | | <FragranceControl tripStatus={tripStatus} orderId={orderId} pageDidShowVersion={pageDidShowVersion} hasPreviousOrder={hasPreviousOrder} /> |
| | | </View> |
| | | </ScrollView> |
| | | } |
| | | |
| | | <DoorTrunkControl |
| | | tripStatus={tripStatus} |
| | |
| | | <FareDetailCard |
| | | tripStatus={tripStatus} |
| | | fareBreakdown={fareBreakdown} |
| | | manuallySelectedCouponName={manuallySelectedCouponName} |
| | | orderId={orderId} |
| | | resetDetailVersion={resetDetailVersion} |
| | | onToggleDetailFade={onToggleDetailFade} |
| | |
| | | <FareDetailCard |
| | | tripStatus={tripStatus} |
| | | fareBreakdown={fareBreakdown} |
| | | manuallySelectedCouponName={manuallySelectedCouponName} |
| | | orderId={orderId} |
| | | resetDetailVersion={resetDetailVersion} |
| | | onToggleDetailFade={handleFareToggle} |
| | |
| | | </View> |
| | | } |
| | | { |
| | | /* 行程操作栏:位于费用明细卡之后、门箱控制之前(对齐设计稿) */ |
| | | [TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup, TripStatus.InService, TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(tripStatus) && |
| | | <TripActionBar |
| | | tripStatus={tripStatus} |
| | |
| | | handleShareTrip={handleShareTrip} |
| | | /> |
| | | } |
| | | |
| | | <DoorTrunkControl |
| | | tripStatus={tripStatus === TripStatus.ArrivedDestination ? TripStatus.ArrivedDestination : -1} |
| | | orderId={orderId} |
| | |
| | | <View className='cancel-site-box-icon-end'></View> |
| | | </View> |
| | | <View className='cancel-site-box-content'> |
| | | <View>{startSiteName}</View> |
| | | <View className='cancel-site-box-content-name'>{startSiteName}</View> |
| | | <View className='cancel-site-box-divider'></View> |
| | | <View>{endSiteName}</View> |
| | | <View className='cancel-site-box-content-name'>{endSiteName}</View> |
| | | </View> |
| | | </View> |
| | | <View className='cancel-btn-box'> |
| | |
| | | left: 0; |
| | | right: 0; |
| | | bottom: 0; |
| | | background: rgba(0, 0, 0, 0); |
| | | background: rgba(0, 0, 0, 0.8); |
| | | } |
| | | |
| | | .announcements-content { |
| | |
| | | height: 313px; |
| | | } |
| | | } |
| | | } |
| | | } |
| | |
| | | .car-card { |
| | | position: relative; |
| | | z-index: 3; |
| | | margin-top: 8px; |
| | | border-radius: 60rpx; |
| | | } |
| | | .fare-box-card { |
| | |
| | | align-items: center; |
| | | } |
| | | .car-card-end-through-desc { |
| | | margin-left: 10px; |
| | | margin-left: 8px; |
| | | font-size: 24px; |
| | | line-height: 40px; |
| | | letter-spacing: normal; |
| | |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding: 32px 8px 0 8px; |
| | | margin-top: 8px; |
| | | padding: 32px 8px; |
| | | } |
| | | .trip-action-bar-item { |
| | | display: flex; |
| | |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: flex-start; |
| | | margin-top: 8px; |
| | | padding: 24px 0px; |
| | | border-radius: 16px; |
| | | background: @gary-theme; |
| | |
| | | font-weight: normal; |
| | | line-height: 40px; |
| | | letter-spacing: normal; |
| | | color: #6d7fd9; |
| | | color: @black-text-main; |
| | | image { |
| | | flex-shrink: 0; |
| | | width: 24px; |
| | |
| | | } |
| | | } |
| | | |
| | | /* 空调 + 香氛 左右布局容器(迁移自 temperature-fragrance-control.less) */ |
| | | .temp-fragrance-control { |
| | | /* 车控入口卡片横向滑动容器(空调 / 音乐 / 香氛,卡片固定宽度,超出宽度可横向滑动) |
| | | 用 ScrollView 而非 View + overflow:weapp WebView 渲染器会拦截 View 上的触摸手势, |
| | | CSS overflow 滚动在真机与模拟器均无法拖动。 */ |
| | | .temp-fragrance-scroll { |
| | | position: relative; |
| | | z-index: 2; |
| | | display: flex; |
| | | justify-content: space-between; |
| | | gap: 16px; |
| | | margin-top: 16px; |
| | | width: 100vw; /* 滑动区铺满整屏宽 */ |
| | | height: 240px; /* 与卡片等高,避免 scroll-view 高度塌陷 */ |
| | | margin-top: 8px; |
| | | /* 左移 48px 抵消 .calling-car-content-box 的左内边距,使滑动区左边界对齐屏幕左边缘, |
| | | 卡片可一直滑到屏幕两侧边缘(首/末卡片的 48px 视觉留白由内部轨道的 padding 承担)。 |
| | | 已显式指定 width: 100vw,故无需再写 margin-right(会因过约束被忽略)。 */ |
| | | margin-left: -48px; |
| | | white-space: nowrap; |
| | | |
| | | /* 隐藏横向滚动条(show-scrollbar 仅 enhanced 模式生效,这里用 CSS 兜底) */ |
| | | &::-webkit-scrollbar, |
| | | ::-webkit-scrollbar { |
| | | display: none; |
| | | width: 0; |
| | | height: 0; |
| | | } |
| | | } |
| | | |
| | | /* 滑动轨道:宽度由卡片内容决定 */ |
| | | .temp-fragrance-control { |
| | | display: inline-flex; |
| | | flex-wrap: nowrap; |
| | | align-items: flex-start; |
| | | gap: 24px; |
| | | /* 静止时首卡对齐 48px 内容左边界;滑到底时末卡右侧同样保留 48px 留白 */ |
| | | padding-left: 48px; |
| | | padding-right: 48px; |
| | | |
| | | /* 卡片不参与压缩,保证各卡片保持设计稿宽度并整体溢出滚动 */ |
| | | > .ac-control, |
| | | > .music-control, |
| | | > .fragrance-control { |
| | | flex-shrink: 0; |
| | | } |
| | | } |
| | | |
| | | /* 空调/香氛入口卡片开关 loading 旋转动画 */ |
| | |
| | | flex-shrink: 0; |
| | | position: relative; |
| | | box-sizing: border-box; |
| | | width: 312px; |
| | | width: 304px; /* 设计稿 152px(375 设计宽)× 2 */ |
| | | height: 240px; |
| | | padding: 24px; |
| | | padding: 8px 24px 24px; |
| | | border-radius: 24px; |
| | | overflow: hidden; |
| | | display: flex; |
| | |
| | | |
| | | .tf-card-header-icon { |
| | | flex-shrink: 0; |
| | | width: 28px; |
| | | height: 28px; |
| | | width: 32px; |
| | | height: 32px; |
| | | margin-right: 8px; |
| | | } |
| | | |
| | |
| | | left: 0; |
| | | right: 0; |
| | | bottom: 0; |
| | | background: rgba(0, 0, 0, 0.5); |
| | | background: rgba(0, 0, 0, 0.8); |
| | | } |
| | | |
| | | .cancel-content { |
| | |
| | | line-height: 48px; |
| | | letter-spacing: normal; |
| | | color: #141E27; |
| | | font-weight: 500; |
| | | .cancel-site-box-divider { |
| | | margin: 34px 32px 40px 0px; |
| | | border-top: 1px solid rgba(204, 208, 215, 0.25); |
| | | } |
| | | .cancel-site-box-content-name { |
| | | font-weight: 500; |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | .credit-pay-mask { |
| | | position: absolute; |
| | | inset: 0; |
| | | background: rgba(0, 0, 0, 0.45); |
| | | background: rgba(0, 0, 0, 0.8); |
| | | } |
| | | |
| | | .credit-pay-content { |
| | |
| | | left: 0; |
| | | right: 0; |
| | | bottom: 0; |
| | | background: rgba(0, 0, 0, 0.5); |
| | | background: rgba(0, 0, 0, 0.8); |
| | | } |
| | | |
| | | .evaluate-content { |
| | |
| | | } |
| | | |
| | | .evaluate-rating-switcher { |
| | | |
| | | |
| | | padding: 0 49px; |
| | | display: flex; |
| | | flex-direction: column; |
| | |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | background: rgba(0, 0, 0, 0.4); |
| | | background: rgba(0, 0, 0, 0.8); |
| | | color: #FFF; |
| | | gap: 8px; |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | /* 香氛入口卡片 tf-card 样式(自包含,迁移自 temperature-fragrance-control.less) */ |
| | | /* 香氛入口卡片:tf-card 基础样式(盒模型 + header/footer 等)统一由 calling-car.less 的全局 .tf-card 提供,此处只保留香氛专属样式 */ |
| | | .fragrance-control { |
| | | .tf-card { |
| | | flex-shrink: 0; |
| | | position: relative; |
| | | box-sizing: border-box; |
| | | width: 312px; |
| | | height: 240px; |
| | | padding: 24px; |
| | | border-radius: 24px; |
| | | overflow: hidden; |
| | | display: flex; |
| | | flex-direction: column; |
| | | justify-content: space-between; |
| | | background-repeat: no-repeat; |
| | | background-position: center; |
| | | background-size: 100% 100%; |
| | | } |
| | | |
| | | .tf-card-bottom { |
| | | display: flex; |
| | | flex-direction: column; |
| | | } |
| | | |
| | | .tf-card-header { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | } |
| | | |
| | | .tf-card-header-left { |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | |
| | | .tf-card-header-icon { |
| | | flex-shrink: 0; |
| | | width: 28px; |
| | | height: 28px; |
| | | margin-right: 8px; |
| | | } |
| | | |
| | | .tf-card-header-title { |
| | | font-size: 24px; |
| | | font-weight: 500; |
| | | line-height: normal; |
| | | letter-spacing: normal; |
| | | color: @black-text-main; |
| | | } |
| | | |
| | | /* 香氛名称,大小类似温度显示 */ |
| | | .tf-card-fragrance-name { |
| | | font-size: 40px; |
| | |
| | | white-space: nowrap; |
| | | text-overflow: ellipsis; |
| | | text-align: left; |
| | | } |
| | | |
| | | .tf-card-footer { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | } |
| | | |
| | | .tf-card-footer-text { |
| | | font-size: 20px; |
| | | line-height: normal; |
| | | letter-spacing: normal; |
| | | color: rgba(20, 30, 39, 0.5); |
| | | } |
| | | |
| | | .tf-card-footer-arrow { |
| | | flex-shrink: 0; |
| | | width: 20px; |
| | | height: 20px; |
| | | } |
| | | |
| | | /* 香氛入口卡片:背景随开关状态切换 */ |
| | |
| | | left: 0; |
| | | width: 100%; |
| | | height: 100%; |
| | | background: rgba(0, 0, 0, 0.4); |
| | | background: rgba(0, 0, 0, 0.8); |
| | | z-index: 102; |
| | | } |
| | | |
| | |
| | | .popup-scent-list { |
| | | display: flex; |
| | | align-items: stretch; |
| | | justify-content: space-between; |
| | | justify-content: center; |
| | | gap: 48px; |
| | | |
| | | // 香型卡片:竖排香型名 + 香调描述,背景使用设计切图(选中/未选) |
| | | .popup-scent-item { |
| New file |
| | |
| | | @import '../../../../style/ui-variables.less'; |
| | | |
| | | // 曲风磁贴下发 loading 旋转动画 |
| | | @keyframes music-genre-spin { |
| | | from { |
| | | transform: rotate(0deg); |
| | | } |
| | | |
| | | to { |
| | | transform: rotate(360deg); |
| | | } |
| | | } |
| | | |
| | | // 副标题上下滚动轮播:5 条文案 + 末尾克隆首条 = 轨道共 6 条,单条行高 24px |
| | | // 位移量必须用「相对轨道自身高度」的百分比而非 px:px 经 pxtransform 变成 rpx 后, |
| | | // 小程序会对每个 rpx 值独立换算成物理像素并取整,条目高度(24rpx)与位移量(96rpx/120rpx) |
| | | // 的取整结果不再是整数倍,误差逐条累计 —— 表现为滚到最后一条时露出下一条顶部的少量文字, |
| | | // 且回环瞬间克隆条与首条位置错开、出现上下抖动。 |
| | | // 百分比按轨道实际渲染高度解析,1 步恒等于 1 条(100% / 6 ≈ 16.66667%),不会累计误差。 |
| | | @keyframes music-subtitle-roll { |
| | | 0%, 16% { |
| | | transform: translateY(0); |
| | | } |
| | | 20%, 36% { |
| | | transform: translateY(-16.66667%); |
| | | } |
| | | 40%, 56% { |
| | | transform: translateY(-33.33333%); |
| | | } |
| | | 60%, 76% { |
| | | transform: translateY(-50%); |
| | | } |
| | | 80%, 96% { |
| | | transform: translateY(-66.66667%); |
| | | } |
| | | 100% { |
| | | transform: translateY(-83.33333%); |
| | | } |
| | | } |
| | | |
| | | /* 音乐入口卡片:tf-card 基础样式(盒模型 + header/footer 等)统一由 calling-car.less 的全局 .tf-card 提供,此处只保留音乐专属样式 */ |
| | | .music-control { |
| | | /* 音符图标(暂用字形占位,待设计切图后替换为 <Image>) */ |
| | | .tf-card-header-note { |
| | | flex-shrink: 0; |
| | | width: 28px; |
| | | height: 28px; |
| | | margin-right: 8px; |
| | | font-size: 28px; |
| | | line-height: 28px; |
| | | text-align: center; |
| | | color: @black-text-main; |
| | | } |
| | | |
| | | /* 曲风名称,大小类似温度/香氛名 */ |
| | | .tf-card-music-name { |
| | | font-size: 40px; |
| | | font-weight: normal; |
| | | line-height: 64px; |
| | | color: @black-text-main; |
| | | overflow: hidden; |
| | | white-space: nowrap; |
| | | text-overflow: ellipsis; |
| | | text-align: left; |
| | | } |
| | | |
| | | // 副标题上下滚动轮播:仅露出一行(24px),轨道竖排承载多条文案 |
| | | .tf-card-subtitle { |
| | | flex: 1; |
| | | min-width: 0; |
| | | height: 24px; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .tf-card-subtitle-track { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: flex-start; |
| | | animation: music-subtitle-roll 25s ease infinite; |
| | | } |
| | | |
| | | .tf-card-subtitle-item { |
| | | flex-shrink: 0; // 锁定单条高度,避免被父级压缩后「1 步 = 1 条」的对应关系被破坏 |
| | | height: 24px; |
| | | line-height: 24px; |
| | | overflow: hidden; |
| | | white-space: nowrap; |
| | | text-overflow: ellipsis; |
| | | } |
| | | |
| | | /* 音乐入口卡片:背景随开关状态切换 */ |
| | | .tf-card-music { |
| | | &.is-open { |
| | | background-image: url('https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/genre-close-0731@2x.png'); |
| | | } |
| | | |
| | | &.is-close { |
| | | background-image: url('https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/genre-open-0731@2x.png'); |
| | | |
| | | // 关闭态卡片文字统一弱化 |
| | | .tf-card-header-note, |
| | | .tf-card-header-title, |
| | | .tf-card-music-name, |
| | | .tf-card-footer-text { |
| | | color: rgba(20, 30, 39, 0.3); |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 底部弹窗面板 |
| | | .music-popup { |
| | | position: fixed; |
| | | top: 0; |
| | | left: 0; |
| | | right: 0; |
| | | bottom: 0; |
| | | z-index: 1000; |
| | | display: flex; |
| | | flex-direction: column; |
| | | justify-content: flex-end; |
| | | pointer-events: none; |
| | | |
| | | &.show { |
| | | pointer-events: auto; |
| | | } |
| | | |
| | | .popup-mask { |
| | | position: absolute; |
| | | top: 0; |
| | | left: 0; |
| | | width: 100%; |
| | | height: 100%; |
| | | background: rgba(0, 0, 0, 0.4); |
| | | z-index: 102; |
| | | } |
| | | |
| | | .popup-content { |
| | | position: relative; |
| | | z-index: 103; |
| | | background: #F1F5FA; |
| | | border-radius: 60px 60px 0 0; |
| | | width: 100%; |
| | | padding: 0 0 calc(48px + env(safe-area-inset-bottom)); |
| | | box-sizing: border-box; |
| | | display: flex; |
| | | flex-direction: column; |
| | | transform: translateY(100%); |
| | | } |
| | | |
| | | .popup-header { |
| | | height: 112px; |
| | | padding-top: 8px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | position: relative; |
| | | flex-shrink: 0; |
| | | |
| | | .popup-title { |
| | | font-size: 32px; |
| | | font-weight: 500; |
| | | line-height: 32px; |
| | | color: rgba(0, 0, 0, 0.9); |
| | | } |
| | | |
| | | .popup-close { |
| | | position: absolute; |
| | | right: 32px; |
| | | top: 50%; |
| | | transform: translateY(-50%); |
| | | width: 40px; |
| | | height: 40px; |
| | | } |
| | | } |
| | | |
| | | // 曲风磁贴区(每行 3 个自动换行) |
| | | .popup-genre-area { |
| | | margin-top: 24px; |
| | | padding: 0 40px; |
| | | } |
| | | |
| | | .popup-genre-list { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 22px; |
| | | } |
| | | |
| | | // 曲风磁贴 |
| | | .popup-genre-item { |
| | | position: relative; |
| | | box-sizing: border-box; |
| | | width: calc((100% - 22px) / 2); |
| | | height: 152px; |
| | | flex-shrink: 0; |
| | | border-radius: 32px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | background-repeat: no-repeat; |
| | | background-position: center; |
| | | background-size: 100% 100%; |
| | | background-image: url('https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/genre-item-close-0731@2x.png'); |
| | | |
| | | // 下发中 loading 图标(叠加于磁贴上方,水平居中,旋转) |
| | | .popup-genre-loading { |
| | | //position: absolute; |
| | | //top: 50%; |
| | | //left: 50%; |
| | | width: 64px; |
| | | height: 64px; |
| | | //transform: translate(-50%, -50%); |
| | | //z-index: 1; |
| | | animation: music-genre-spin 1s linear infinite; |
| | | } |
| | | |
| | | .popup-genre-name { |
| | | font-size: 28px; |
| | | font-weight: 500; |
| | | line-height: 40px; |
| | | color: rgba(20, 30, 39, 0.3); |
| | | overflow: hidden; |
| | | white-space: nowrap; |
| | | text-overflow: ellipsis; |
| | | } |
| | | |
| | | // 选中态:高亮描边 + 主题渐变底 |
| | | &.active { |
| | | background-image: url('https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/genre-item-open-0731@2x.png'); |
| | | |
| | | .popup-genre-name { |
| | | color: #fff; |
| | | } |
| | | } |
| | | |
| | | // 下发中禁止重复点击 |
| | | &.disabled { |
| | | opacity: 0.6; |
| | | } |
| | | } |
| | | |
| | | .popup-genre-empty { |
| | | padding: 96px 0; |
| | | text-align: center; |
| | | font-size: 28px; |
| | | line-height: 40px; |
| | | color: rgba(0, 0, 0, 0.4); |
| | | } |
| | | |
| | | // 底部播放开关行 |
| | | .popup-play-switch { |
| | | margin-top: 48px; |
| | | padding: 0 48px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | position: relative; |
| | | |
| | | .popup-play-text { |
| | | font-size: 24px; |
| | | line-height: 40px; |
| | | color: rgba(20, 30, 39, 0.5); |
| | | } |
| | | |
| | | .ac-switch { |
| | | position: absolute; |
| | | bottom: -12px; |
| | | right: 40px; |
| | | } |
| | | } |
| | | } |
| | | } |
| | |
| | | left: 0; |
| | | right: 0; |
| | | bottom: 0; |
| | | background: rgba(0, 0, 0, 0.6); |
| | | background: rgba(0, 0, 0, 0.8); |
| | | } |
| | | |
| | | .no-route-content { |
| | |
| | | left: 0; |
| | | right: 0; |
| | | bottom: 0; |
| | | background: rgba(0, 0, 0, 0.5); |
| | | background: rgba(0, 0, 0, 0.8); |
| | | } |
| | | |
| | | .over-content { |
| | |
| | | height: 100%; |
| | | overflow-y: scroll; |
| | | margin-top: 0; |
| | | padding-right: 8px; |
| | | } |
| | | |
| | | .passenger-content-desc-inner { |
| | |
| | | @import '../../../../style/ui-variables.less'; |
| | | |
| | | .pickup-exception-popup { |
| | | position: fixed; |
| | | inset: 0; |
| | |
| | | .pickup-exception-mask { |
| | | position: absolute; |
| | | inset: 0; |
| | | background: rgba(0, 0, 0, 0.6); |
| | | background: rgba(0, 0, 0, 0.8); |
| | | } |
| | | |
| | | .pickup-exception-content { |
| | |
| | | left: 0; |
| | | right: 0; |
| | | bottom: 0; |
| | | background: #ffffff; |
| | | border-radius: 32px 32px 0 0; |
| | | padding: 48px 40px 80px; |
| | | background: @gary-theme; |
| | | border-radius: 60px 60px 0 0; |
| | | padding: 48px 48px 80px 48px; |
| | | box-sizing: border-box; |
| | | } |
| | | |
| | |
| | | font-weight: 500; |
| | | line-height: 44px; |
| | | letter-spacing: normal; |
| | | color: rgba(0, 0, 0, 0.9); |
| | | color: @black-text-main; |
| | | } |
| | | |
| | | .pickup-exception-desc { |
| | |
| | | font-weight: normal; |
| | | letter-spacing: normal; |
| | | /* 文字色/黑色/辅助文字 40% */ |
| | | color: rgba(0, 0, 0, 0.4); |
| | | color: @black-text-04; |
| | | margin-top: 8px; |
| | | } |
| | | |
| | |
| | | position: relative; |
| | | width: 100%; |
| | | height: 358px; |
| | | border-radius: 16px; |
| | | border-radius: 32px; |
| | | overflow: hidden; |
| | | background: #D8D8D8; |
| | | background: @gary-theme; |
| | | margin-top: 48px; |
| | | } |
| | | |
| | |
| | | |
| | | .pickup-exception-message { |
| | | margin-top: 32px; |
| | | color: rgba(0, 0, 0, 0.9); |
| | | color: @black-text-main; |
| | | font-size: 28px; |
| | | line-height: 44px; |
| | | text-align: center; |
| | |
| | | line-height: 88px; |
| | | text-align: center; |
| | | color: #ffffff; |
| | | border-radius: 16px; |
| | | background: linear-gradient(96deg, #8475FF 1%, #7DD6FF 103%); |
| | | border-radius: 2000px; |
| | | background: @black-text-main; |
| | | font-size: 28px; |
| | | font-weight: 500; |
| | | border-radius: 16px; |
| | | } |
| | |
| | | |
| | | // 二排(目前唯一可控排):按钮位于座舱图中部横排,左右座椅中心分别约 28% / 72% |
| | | .seat-hv-node-SECOND_ROW_LEFT { |
| | | top: 45%; |
| | | left: 28.5%; |
| | | top: 29%; |
| | | left: 22.5%; |
| | | } |
| | | |
| | | .seat-hv-node-SECOND_ROW_RIGHT { |
| | | top: 45%; |
| | | left: 71.5%; |
| | | top: 29%; |
| | | left: 77.5%; |
| | | } |
| | | |
| | | // 三排:纵向约 60%,左右座椅中心分别约 28% / 72% |
| | |
| | | .seat-hv-btn { |
| | | position: relative; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | |
| | |
| | | height: 48px; |
| | | } |
| | | |
| | | &-label { |
| | | font-size: 20px; |
| | | font-weight: normal; |
| | | line-height: 34px; |
| | | color: #A1A1A1; |
| | | } |
| | | |
| | | // 下发中 loading 图标旋转(点击后至指令返回) |
| | | &-loading { |
| | | opacity: 1; |
| | |
| | | left: 0; |
| | | width: 100%; |
| | | height: 100%; |
| | | background: rgba(0, 0, 0, 0.4); |
| | | background: rgba(0, 0, 0, 0.8); |
| | | z-index: 102; |
| | | } |
| | | |
| | |
| | | }, 300); |
| | | }; |
| | | |
| | | /** |
| | | * 立即置为接驾态并暂停:一键把页面冻结在「车辆即将到达(4)」态,便于纯静态调 UI/样式。 |
| | | * |
| | | * 原理:新建模拟器后调用 start(),其内部 enterApproaching() 会同步注入 |
| | | * order_status(4) + 全量接驾路线事件;随即调用 stop() 清除定时器, |
| | | * 使车辆停在接驾路线起点、剩余路线全量绘制,画面固定在接驾态不再向后推进。 |
| | | */ |
| | | const handleFreezeApproaching = () => { |
| | | // 1) 绑定 socket 业务监听,确保注入事件能被消费(无真实连接也可) |
| | | props.bindSocketListeners(); |
| | | |
| | | // 2) 确保订单 ID 一致(守卫 isSameOrder) |
| | | const orderId = resolveOrderId(); |
| | | ensureSimulationOrderId(orderId); |
| | | props.setOrderId(orderId); |
| | | |
| | | // 3) 由上/下车点生成双腿路线 |
| | | const { approachRoute, serviceRoute } = buildDefaultRoutes(props.startSite, props.endSite); |
| | | const preset = SPEED_PRESETS[speedKey]; |
| | | |
| | | // 4) 新建模拟器,start() 后立即 stop(),冻结在接驾态初始帧 |
| | | simulatorRef.current?.reset(); |
| | | const simulator = new OrderFlowSimulator({ |
| | | orderId, |
| | | approachRoute, |
| | | serviceRoute, |
| | | tickIntervalMs: preset.tickIntervalMs, |
| | | pointsPerTick: preset.pointsPerTick, |
| | | onState: (state) => setSimState(state), |
| | | }); |
| | | simulatorRef.current = simulator; |
| | | setRunning(false); |
| | | // 延迟一帧等待订单 ID 写入页面 deps,再进入接驾态并立即暂停 |
| | | setTimeout(() => { |
| | | simulator.start(); // enterApproaching:注入 status(4) + 全量接驾路线 |
| | | simulator.stop(); // 立即停止推进,冻结在接驾态 |
| | | }, 300); |
| | | }; |
| | | |
| | | /** 停止模拟(保留当前画面)。 */ |
| | | const handleStop = () => { |
| | | simulatorRef.current?.stop(); |
| | |
| | | </View> |
| | | <View style={{ display: 'flex', marginBottom: '4px' }}> |
| | | <View |
| | | onClick={handleFreezeApproaching} |
| | | style={buttonStyle('#1d9bf0')} |
| | | > |
| | | 接驾态并暂停(4) |
| | | </View> |
| | | </View> |
| | | <View style={{ display: 'flex', marginBottom: '4px' }}> |
| | | <View |
| | | onClick={handleJumpToTripFinished} |
| | | style={buttonStyle('#0e9f6e')} |
| | | > |
| | |
| | | if (Array.isArray(linePoints) && linePoints.length > 1) { |
| | | nextPolylines.push({ |
| | | points: linePoints, |
| | | color: '#7B91FF', |
| | | color: '#DFB369', |
| | | width: 6, |
| | | dottedLine: true, |
| | | arrowLine: false |
| | |
| | | callImmediately: () => void; |
| | | onSelectSite?: (pageType: 'start' | 'end') => void; |
| | | fareEstimate?: any; |
| | | manuallySelectedCouponName?: string; |
| | | onOpenCouponSelect?: () => void; |
| | | handleOpenPassengerNotice?: () => void; |
| | | walkingTime?: number; |
| | |
| | | handleSelectRoute?: (index: number) => void; |
| | | }; |
| | | |
| | | export default function ReleaseRoute({ startSite, endSite, callImmediately, onSelectSite, fareEstimate = {}, onOpenCouponSelect, handleOpenPassengerNotice, walkingTime = 0, isCallDisabled = false, isRouteSelectExpanded = false, onToggleRouteSelect, routeOptions = [], selectedRouteIndex = 0, handleSelectRoute }: Props) { |
| | | export default function ReleaseRoute({ startSite, endSite, callImmediately, onSelectSite, fareEstimate = {}, manuallySelectedCouponName = '', onOpenCouponSelect, handleOpenPassengerNotice, walkingTime = 0, isCallDisabled = false, isRouteSelectExpanded = false, onToggleRouteSelect, routeOptions = [], selectedRouteIndex = 0, handleSelectRoute }: Props) { |
| | | const [boxHeight, setBoxHeight] = useState<number | null>(null); |
| | | const [bottomPaddingHeight, setBottomPaddingHeight] = useState<number | null>(null); |
| | | const startY = useRef(0); |
| | |
| | | } |
| | | return `${parseFloat(discountAmount.toFixed(2))}元`; |
| | | }; |
| | | |
| | | const discountTitleText = manuallySelectedCouponName || '最佳优惠'; |
| | | const discountDescText = manuallySelectedCouponName |
| | | ? `已选择${manuallySelectedCouponName}` |
| | | : '已选择最佳优惠'; |
| | | |
| | | useEffect(() => { |
| | | if (!isRouteSelectExpanded && boxHeight !== null && minHeight.current > 0) { |
| | |
| | | onClick={onOpenCouponSelect} |
| | | > |
| | | <View className="flex-center"> |
| | | <View className="discount-amount-text">最佳优惠</View> |
| | | <View className="discount-amount-desc" >已为您选择最佳优惠</View> |
| | | <View className="discount-amount-text">{discountTitleText}</View> |
| | | <View className="discount-amount-desc" >{discountDescText}</View> |
| | | </View> |
| | | <View className="flex-center"> |
| | | <View className="discount-amount-num">-{formatDiscountAmountText()}</View> |
| | |
| | | left: 0; |
| | | width: 100%; |
| | | height: 100%; |
| | | background: rgba(0, 0, 0, 0.4); |
| | | background: rgba(0, 0, 0, 0.8); |
| | | z-index: 102; |
| | | } |
| | | |
| | |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | |
| | | color: "#141E27", |
| | | fontSize: 12, |
| | | anchorX: 0, |
| | | anchorY: 9, // 图标下方 9px |
| | | anchorY: 7, // 图标下方 5px |
| | | textAlign: "center" as const, |
| | | bgColor: "#00000000", |
| | | fontWeight: 'bold', |
| | |
| | | borderRadius: 0, |
| | | padding: 0 |
| | | }; |
| | | // 到达上车点/待支付/完成态:站点名称在图标下方 8px,11px #141E27 居中 |
| | | // 到达上车点/待支付/完成态:站点名称在图标下方 7px,11px #141E27 居中 |
| | | const SITE_NAME_LABEL_BELOW_LG = { |
| | | color: "#141E27", |
| | | fontSize: 12, |
| | | anchorX: 0, |
| | | anchorY: 8, |
| | | anchorY: 7, |
| | | textAlign: "center" as const, |
| | | bgColor: "#00000000", |
| | | fontWeight: 'bold', |
| | |
| | | onPublishRouteSelectSite?: (pageType: 'start' | 'end') => void; |
| | | }; |
| | | |
| | | const PENDING_SELECTED_COUPON_ID_STORAGE_KEY = 'pendingSelectedCouponId'; |
| | | |
| | | /** |
| | | * 页面动作编排:统一处理服务入口、页面跳转、地图气泡点击行为。 |
| | | */ |
| | |
| | | const toBack = () => { |
| | | deps.onBeforeBack?.(); |
| | | Taro.removeStorageSync('selectedCouponId'); |
| | | Taro.removeStorageSync(PENDING_SELECTED_COUPON_ID_STORAGE_KEY); |
| | | clearModifyDestinationStorage(); |
| | | clearPassedRouteCache(); |
| | | if (deps.tripStatus === TripStatus.PaymentSuccess || deps.tripStatus === TripStatus.PublishRoute) { |
| | |
| | | |
| | | /** 已选优惠券缓存键。 */ |
| | | const SELECTED_COUPON_ID_STORAGE_KEY = 'selectedCouponId'; |
| | | /** 优惠券列表页“去使用”待消费缓存键。 */ |
| | | const PENDING_SELECTED_COUPON_ID_STORAGE_KEY = 'pendingSelectedCouponId'; |
| | | |
| | | type RequestRouteOptions = { |
| | | startPointId?: string | number; |
| | |
| | | }; |
| | | |
| | | /** |
| | | * 清理优惠券列表页“去使用”的待消费缓存。 |
| | | */ |
| | | const clearPendingSelectedCouponId = () => { |
| | | Taro.removeStorageSync(PENDING_SELECTED_COUPON_ID_STORAGE_KEY); |
| | | }; |
| | | |
| | | /** |
| | | * 归一化已选优惠券 ID,过滤空值和脏缓存值。 |
| | | */ |
| | | const normalizeSelectedCouponId = () => { |
| | |
| | | }; |
| | | |
| | | /** |
| | | * 归一化优惠券列表页“去使用”的待消费券 ID。 |
| | | */ |
| | | const normalizePendingSelectedCouponId = () => { |
| | | const rawCouponId = Taro.getStorageSync(PENDING_SELECTED_COUPON_ID_STORAGE_KEY); |
| | | const normalizedCouponId = String(rawCouponId ?? '').trim(); |
| | | if (!normalizedCouponId || ['null', 'undefined'].includes(normalizedCouponId)) { |
| | | return ''; |
| | | } |
| | | return normalizedCouponId; |
| | | }; |
| | | |
| | | /** |
| | | * 进入预估态前处理优惠券缓存: |
| | | * 无订单时校验券状态,行程中进入页面则直接清空。 |
| | | */ |
| | | const prepareSelectedCouponForEstimate = async (cachedOrderId: string, cachedTripStatus: TripStatus) => { |
| | | const cachedCouponId = normalizeSelectedCouponId(); |
| | | let cachedCouponId = normalizeSelectedCouponId(); |
| | | const rawCouponId = Taro.getStorageSync(SELECTED_COUPON_ID_STORAGE_KEY); |
| | | const rawPendingCouponId = Taro.getStorageSync(PENDING_SELECTED_COUPON_ID_STORAGE_KEY); |
| | | const pendingCouponId = normalizePendingSelectedCouponId(); |
| | | const canConsumePendingCoupon = cachedTripStatus === TripStatus.PublishRoute && !cachedOrderId; |
| | | |
| | | // 优惠券列表页“去使用”的待消费券进入确认行程页后,应只消费一次。 |
| | | // 只要存在有效 pending 值,就由它决定本次无订单预估态使用的券,并立即清空 pending, |
| | | // 避免旧 selectedCouponId 抢占优先级或 pending 残留到后续页面恢复中再次被消费。 |
| | | if (pendingCouponId) { |
| | | if (canConsumePendingCoupon) { |
| | | Taro.setStorageSync(SELECTED_COUPON_ID_STORAGE_KEY, pendingCouponId); |
| | | cachedCouponId = pendingCouponId; |
| | | } |
| | | clearPendingSelectedCouponId(); |
| | | } else if (rawPendingCouponId !== '' && rawPendingCouponId !== undefined && rawPendingCouponId !== null) { |
| | | clearPendingSelectedCouponId(); |
| | | } |
| | | |
| | | if (!cachedCouponId) { |
| | | const rawCouponId = Taro.getStorageSync(SELECTED_COUPON_ID_STORAGE_KEY); |
| | | if (rawCouponId !== '' && rawCouponId !== undefined && rawCouponId !== null) { |
| | | clearSelectedCouponId(); |
| | | } |
| | |
| | | ].includes(cachedTripStatus); |
| | | if (cachedOrderId && isTripOrderStatus) { |
| | | clearSelectedCouponId(); |
| | | clearPendingSelectedCouponId(); |
| | | return; |
| | | } |
| | | |
| | |
| | | handleEstimateSelectSite: (pageType: "start" | "end") => void; |
| | | /** 费用信息 */ |
| | | fareEstimate: any; |
| | | /** 预估态用户手动切换后的优惠券名称 */ |
| | | manuallySelectedCouponName: string; |
| | | /** 打开优惠券选择弹窗 */ |
| | | handleOpenCouponSelect: () => void; |
| | | /** 打开乘客须知 */ |
| | |
| | | }`.trim()} |
| | | style={{ minWidth: "40px", justifyContent: "center", zIndex: 2 }} |
| | | > |
| | | {!deps.hasBeforeUserEndSite && ( |
| | | {deps.hasBeforeUserEndSite && ( |
| | | <CoverView className="car-time car-before-order-tip"> |
| | | 即将完成上一单 |
| | | </CoverView> |
| | | )} |
| | | {!deps.hasBeforeUserEndSite ? ( |
| | | {deps.hasBeforeUserEndSite ? ( |
| | | <CoverView className="car-before-order-tip-text"> |
| | | <CoverView> |
| | | 全程 |
| | |
| | | callImmediately={deps.callImmediately} |
| | | onSelectSite={deps.handleEstimateSelectSite} |
| | | fareEstimate={deps.fareEstimate} |
| | | manuallySelectedCouponName={deps.manuallySelectedCouponName} |
| | | onOpenCouponSelect={deps.handleOpenCouponSelect} |
| | | handleOpenPassengerNotice={deps.handleOpenPassengerNotice} |
| | | walkingTime={deps.walkingTime} |
| | |
| | | pickupCountdownSeconds={pickupCountdownSeconds} |
| | | finishCountdownSeconds={finishCountdownSeconds} |
| | | fareBreakdown={fareBreakdown} |
| | | manuallySelectedCouponName={deps.manuallySelectedCouponName} |
| | | evaluateSubmitVersion={deps.evaluateSubmitVersion} |
| | | evaluateCloseVersion={deps.evaluateCloseVersion} |
| | | evaluatePopupStatus={deps.evaluatePopupStatus} |
| | |
| | | width: 24px; |
| | | height: 24px; |
| | | display: block; |
| | | margin-right: 8px; |
| | | margin-right: 4px; |
| | | } |
| | | |
| | | .site-countdown-time { |
| | | font-family: @font-xpsans-bold; |
| | | font-size: 24px; |
| | | font-weight: normal; |
| | | line-height: 32px; |
| | | text-align: center; |
| | | letter-spacing: 0.04em; |
| | | color: #FFD181; |
| | |
| | | // 蓝牙车控连接与鉴权编排 |
| | | import useBluetoothCarControl from './hooks/use-bluetooth-car-control'; |
| | | import { toTripStatus, TripStatus } from '../../constants/trip-status'; |
| | | import { MODIFY_DESTINATION_SOCKET_STATUS_KEY } from '../../constants/modify-destination'; |
| | | import { MODIFY_DESTINATION_SOCKET_STATUS_KEY, MODIFY_DESTINATION_SESSION_KEY } from '../../constants/modify-destination'; |
| | | import { cancelOrder, selectNavigationRoute, getUpdateAddressNum } from '../../api/begin-route'; |
| | | import { getAddressByLocation, getDistance, reportEvent } from '../../../utils/Robotaxi-public-method'; |
| | | import { CurrentUserLocation, RouteDistanceValue } from './hooks/use-route-manager.shared'; |
| | | import { MODIFY_DESTINATION_SESSION_KEY } from '../../constants/modify-destination'; |
| | | // import { MODIFY_DESTINATION_SESSION_KEY } from '../../constants/modify-destination'; |
| | | import { getOrderId } from '../../utils/order'; |
| | | import { getOrderDetail } from '../../api/order'; |
| | | |
| | |
| | | const [evaluateCloseVersion, setEvaluateCloseVersion] = useState(0); // 评价关闭版本号 |
| | | const [evaluateOrderRating, setEvaluateOrderRating] = useState(1); // 当前评价弹窗对应评分 |
| | | const [showCouponSelectPopup, setShowCouponSelectPopup] = useState(false); // 优惠券选择弹窗展示状态 |
| | | const [defaultPreviewCouponId, setDefaultPreviewCouponId] = useState(''); // 预估态默认命中的优惠券 ID |
| | | const [manuallySelectedCouponName, setManuallySelectedCouponName] = useState(''); // 当前流程内用户手动切换后的优惠券名称 |
| | | const [orderDetailViewportRefreshVersion, setOrderDetailViewportRefreshVersion] = useState(0); // 完成态视口刷新版本,关闭优惠券弹窗后触发 includePoints 纠偏 |
| | | const [showModifyDestinationFailPopup, setShowModifyDestinationFailPopup] = useState(false); // 修改目的地失败弹窗 |
| | | const [modifyDestinationFailMessage, setModifyDestinationFailMessage] = useState(''); // 修改目的地失败文案(默认空,避免闪文案) |
| | |
| | | const lastWaitingForPickupPageViewOrderIdRef = useRef(''); // 等待接驾页页面浏览埋点最近一次上报的订单 ID |
| | | const handledPickupExceptionSignatureRef = useRef(''); // 已处理接驾异常签名,避免同一异常重复弹窗 |
| | | const previousOrderIdRef = useRef(''); // 上一个订单 ID,用于跨单清理异常弹窗状态 |
| | | const pendingFareCouponSelectionRef = useRef({ couponId: '', couponName: '' }); // 待支付态最近一次手选券,待账单返回后校正名称 |
| | | const confirmRideEventSnapshotRef = useRef({ |
| | | startSite: null as any, |
| | | endSite: null as any, |
| | |
| | | setShowCouponSelectPopup(false); |
| | | return; |
| | | } |
| | | const selectedCouponName = String(item?.title || item?.couponName || '').trim(); |
| | | if (tripStatus === TripStatus.PublishRoute) { |
| | | setManuallySelectedCouponName( |
| | | selectedCouponId === defaultPreviewCouponId ? '' : selectedCouponName |
| | | ); |
| | | pendingFareCouponSelectionRef.current = { couponId: '', couponName: '' }; |
| | | } |
| | | Taro.setStorageSync('selectedCouponId', selectedCouponId); |
| | | setShowCouponSelectPopup(false); |
| | | if ([TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(tripStatus)) { |
| | | // 待支付态先即时回显用户刚选择的券名,接口返回后再按实际生效券校正。 |
| | | pendingFareCouponSelectionRef.current = { |
| | | couponId: selectedCouponId, |
| | | couponName: selectedCouponName |
| | | }; |
| | | setManuallySelectedCouponName(selectedCouponName); |
| | | setPriceUpdateVersion(v => v + 1); |
| | | return; |
| | | } |
| | |
| | | const activeCouponId = String( // 优惠券弹窗默认选中券ID |
| | | fareEstimate?.couponId || fareEstimate?.userCouponId || Taro.getStorageSync('selectedCouponId') || '' |
| | | ); |
| | | useEffect(() => { |
| | | if (tripStatus !== TripStatus.PublishRoute) { |
| | | return; |
| | | } |
| | | if (manuallySelectedCouponName) { |
| | | return; |
| | | } |
| | | setDefaultPreviewCouponId(String( |
| | | fareEstimate?.couponId || fareEstimate?.userCouponId || '' |
| | | ).trim()); |
| | | }, [tripStatus, fareEstimate?.couponId, fareEstimate?.userCouponId, manuallySelectedCouponName]); |
| | | const canWalkNavigateToPickup = useMemo(() => { |
| | | if (isModifyDestinationReviewVisible) { |
| | | return false; |
| | |
| | | isWaitingSignedCreditPayResult, |
| | | handleEstimateSelectSite, |
| | | fareEstimate, |
| | | manuallySelectedCouponName, |
| | | handleOpenCouponSelect, |
| | | handleOpenPassengerNotice, |
| | | walkingTime, |
| | |
| | | const couponOrderMoney = tripStatus === TripStatus.TripFinished |
| | | ? (Number(fareBreakdown?.orderMoney) || 0) |
| | | : (Number(fareEstimate?.orderMoney) || 0); |
| | | useEffect(() => { |
| | | if (![TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(tripStatus)) { |
| | | return; |
| | | } |
| | | const pendingSelection = pendingFareCouponSelectionRef.current; |
| | | if (!pendingSelection.couponId) { |
| | | return; |
| | | } |
| | | const resolvedCouponId = String(fareBreakdown?.userCouponId || '').trim(); |
| | | // 账单返回后,以实际生效券为准:命中用户所选则保留名称,否则清空避免价格与名称不一致。 |
| | | if (resolvedCouponId && resolvedCouponId === pendingSelection.couponId) { |
| | | setManuallySelectedCouponName(pendingSelection.couponName); |
| | | } else { |
| | | setManuallySelectedCouponName(''); |
| | | } |
| | | pendingFareCouponSelectionRef.current = { couponId: '', couponName: '' }; |
| | | }, [tripStatus, fareBreakdown?.userCouponId]); |
| | | // 渲染时优先使用 record*/set* 记录的最新值(ref),ref 为空再回退到 state,保证下次渲染拿到最新值 |
| | | // 【修复频繁切换目的地后地图闪现北京故宫的问题】 |
| | | // 当页面重新挂载且定位接口(getLocation)还未返回时,longitude/latitude 状态为 null。 |
| | |
| | | onConfirm={handleConfirmCreditPayPopup} |
| | | /> |
| | | <TripFinishedFireworks tripStatus={tripStatus} /> |
| | | {/* <DebugOrderSimulatorPanel |
| | | {/* 模拟器入口已关闭,如需本地调试可取消下方注释 |
| | | <DebugOrderSimulatorPanel |
| | | orderId={orderId} |
| | | setOrderId={setOrderId} |
| | | setTripStatus={setTripStatus} |
| | |
| | | setShowCancelPopup={setShowCancelPopup} |
| | | setShowModifyDestinationFailPopup={setShowModifyDestinationFailPopup} |
| | | setModifyDestinationFailMessage={setModifyDestinationFailMessage} |
| | | /> */} |
| | | /> |
| | | */} |
| | | </> |
| | | ) |
| | | } |
| | |
| | | left: 0; |
| | | right: 0; |
| | | bottom: 0; |
| | | background: rgba(0, 0, 0, 0.5); |
| | | background: rgba(0, 0, 0, 0.8); |
| | | } |
| | | |
| | | .privacy-content { |
| | |
| | | import { useState, useEffect, useRef } from "react"; |
| | | import { View, Text, Image, RichText, ScrollView } from "@tarojs/components"; |
| | | import "./index.less"; |
| | | |
| | | const PENDING_SELECTED_COUPON_ID_STORAGE_KEY = 'pendingSelectedCouponId'; |
| | | import { |
| | | getCouponList, |
| | | receiveCoupon, |
| | |
| | | |
| | | const toHome = (couponId?: string) => { |
| | | if (couponId) { |
| | | Taro.setStorageSync("selectedCouponId", couponId); |
| | | // 优惠券列表页“去使用”仅表达一次待带入发单页的意图, |
| | | // 不能污染发单/结算流程内部复用的 selectedCouponId。 |
| | | Taro.setStorageSync(PENDING_SELECTED_COUPON_ID_STORAGE_KEY, couponId); |
| | | } |
| | | Taro.reLaunch({ |
| | | url: "/pages/robotaxi/home/index" |
| | |
| | | align-items: center; |
| | | justify-content: center; |
| | | gap: 8px; |
| | | background: rgba(0, 0, 0, 0.4); |
| | | background: rgba(0, 0, 0, 0.8); |
| | | color: #FFFFFF; |
| | | |
| | | &-text { |
| | |
| | | |
| | | const DEFAULT_MAP_LONGITUDE = 116.397128; |
| | | const DEFAULT_MAP_LATITUDE = 39.916527; |
| | | const MAP_INCLUDE_POINTS_PADDING = [120, 10, 50, 10]; |
| | | const MAP_INCLUDE_POINTS_PADDING = [120, 20, 160, 20]; |
| | | // includePoints 缩放动画的预估时长,动画结束后再显示地图,避免“先展示再放大”的跳动 |
| | | const MAP_FIT_ANIMATION_DURATION = 700; |
| | | const POLYGON_STROKE_COLOR = '#DFB369'; // rgb(223, 179, 105) |
| | |
| | | @import '../../../style/ui-variables.less'; |
| | | |
| | | .order-detail-page { |
| | | background: #F2F3F7; |
| | | min-height: 100vh; |
| | |
| | | flex-direction: column; |
| | | |
| | | .car-info { |
| | | padding: 18px 32px 32px 32px; |
| | | border-radius: 24px; |
| | | padding: 48px 48px 40px 48px; |
| | | border-radius: 60px; |
| | | background: #FFFFFF; |
| | | |
| | | .info-top { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | position: relative; |
| | | |
| | | .car-name { |
| | | font-family: PingFang SC; |
| | | font-size: 28px; |
| | | font-weight: normal; |
| | | font-weight: 500; |
| | | line-height: 48px; |
| | | letter-spacing: normal; |
| | | color: rgba(0, 0, 0, 0.9); |
| | | color: @black-text-main; |
| | | margin-top: 14px; |
| | | } |
| | | |
| | |
| | | font-size: 24px; |
| | | font-variation-settings: "opsz" auto; |
| | | } |
| | | |
| | | } |
| | | .car-box { |
| | | position: absolute; |
| | | top: 50px; |
| | | right: 52px; |
| | | .car-img { |
| | | width: 236px; |
| | | height: 110px; |
| | | margin-right: 4px; |
| | | width: 272px; |
| | | height: 234px; |
| | | } |
| | | } |
| | | |
| | | .evaluate-card { |
| | | margin-top: 36px; |
| | | margin-top: 32px; |
| | | width: 638px; |
| | | height: 248px; |
| | | border-radius: 16px; |
| | | border-radius: 40px; |
| | | background: #F5F6FA; |
| | | padding: 32px 28px 24px 30px; |
| | | |
| | |
| | | line-height: 40px; |
| | | letter-spacing: normal; |
| | | /* 文字色/黑色/重要字 90% */ |
| | | color: rgba(0, 0, 0, 0.9); |
| | | color: @black-text-main; |
| | | margin: 0px 0px 24px 2px; |
| | | } |
| | | |
| | |
| | | text-align: center; |
| | | letter-spacing: normal; |
| | | /* 文字色/黑色/辅助文字 40% */ |
| | | color: rgba(0, 0, 0, 0.4); |
| | | color: @black-text-04; |
| | | } |
| | | |
| | | .active-text { |
| | | /* 文字色/黑色/重要字 90% */ |
| | | color: rgba(0, 0, 0, 0.9); |
| | | color: @black-text-main; |
| | | } |
| | | } |
| | | } |
| | |
| | | |
| | | .trip-info { |
| | | margin-top: 24px; |
| | | border-radius: 20.29px; |
| | | border-radius: 60px; |
| | | background: #FFFFFF; |
| | | padding: 32px 24px; |
| | | padding: 48px 48px 40px 48px; |
| | | |
| | | .trip-top { |
| | | display: flex; |
| | |
| | | font-size: 48px; |
| | | line-height: 40px; |
| | | letter-spacing: normal; |
| | | color: rgba(0, 0, 0, 0.9); |
| | | color: @black-text-main; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .unit { |
| | |
| | | font-weight: normal; |
| | | line-height: 26px; |
| | | letter-spacing: normal; |
| | | color: rgba(0, 0, 0, 0.9); |
| | | color: @black-text-main; |
| | | margin: 0 4px; |
| | | } |
| | | |
| | |
| | | line-height: 26px; |
| | | letter-spacing: normal; |
| | | text-decoration: line-through; |
| | | color: rgba(0, 0, 0, 0.4); |
| | | color: @black-text-04; |
| | | } |
| | | } |
| | | |
| | |
| | | line-height: 40px; |
| | | letter-spacing: normal; |
| | | /* 文字色/黑色/次要文字 60% */ |
| | | color: rgba(0, 0, 0, 0.6); |
| | | color: @black-text-04; |
| | | margin-right: 12px; |
| | | } |
| | | |
| | |
| | | line-height: 44px; |
| | | letter-spacing: normal; |
| | | /* 文字色/黑色/重要字 90% */ |
| | | color: rgba(0, 0, 0, 0.9); |
| | | color: @black-text-main; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | |
| | | |
| | | .line-icon { |
| | | /* 自动布局子元素 */ |
| | | width: 8px; |
| | | height: 8px; |
| | | width: 10px; |
| | | height: 10px; |
| | | z-index: 0; |
| | | background: #8CB654; |
| | | background: @black-text-main; |
| | | border-radius: 50%; |
| | | margin-right: 16px; |
| | | flex-shrink: 0; |
| | |
| | | font-weight: normal; |
| | | line-height: 40px; |
| | | letter-spacing: normal; |
| | | color: rgba(0, 0, 0, 0.4); |
| | | color: @black-text-04; |
| | | } |
| | | |
| | | .first-icon { |
| | | background: #6D7FD9; |
| | | background: @gold-light; |
| | | } |
| | | |
| | | } |
| | | } |
| | | } |
| | | |
| | | } |
| | | } |
| | |
| | | id: 1, |
| | | longitude: displayStartPoint.longitude, |
| | | latitude: displayStartPoint.latitude, |
| | | width: 20, |
| | | height: 36, |
| | | anchor: { x: 0.5, y: 1 }, |
| | | iconPath: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/start-text.png', |
| | | width: 34, |
| | | height: 46, |
| | | anchor: { x: 0.7, y: 0.8 }, |
| | | iconPath: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/start-site-order-20260804-v2.png', |
| | | viewportLongitude: startPoint?.longitude, |
| | | viewportLatitude: startPoint?.latitude, |
| | | } : null, |
| | |
| | | id: 2, |
| | | longitude: endPoint.longitude, |
| | | latitude: endPoint.latitude, |
| | | width: 20, |
| | | height: 36, |
| | | anchor: { x: 0.5, y: 1 }, |
| | | iconPath: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/end-text.png', |
| | | width: 34, |
| | | height: 46, |
| | | anchor: { x: 0.7, y: 0.8 }, |
| | | iconPath: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/end-site-order-20260804-v2.png', |
| | | } : null |
| | | ].filter(Boolean) as any[]; |
| | | setMarkers(markerList); |
| | |
| | | } |
| | | const polylineData = [{ |
| | | points: routePath, |
| | | color: '#6D7BC1', |
| | | width: 10, |
| | | color: '#59B688', |
| | | width: 7, |
| | | dottedLine: false, |
| | | arrowLine: false, |
| | | }, |
| | | { |
| | | points: routePath, |
| | | color: '#7B91FF', |
| | | width: 8, |
| | | color: '#59B688', |
| | | width: 7, |
| | | dottedLine: false, |
| | | arrowLine: true, |
| | | }]; |
| | |
| | | orderData.vehicleId && |
| | | <View className='info-top'> |
| | | <View> |
| | | <View className='car-name'>{orderData.plateNumber}</View> |
| | | <View className='car-name'>{orderData.plateNumber} · {orderData.vehicleColor}</View> |
| | | <View className='info'> |
| | | <Text className='car-color'>小鹏GX</Text> |
| | | <Text className='info-line'></Text> |
| | | <Text className='car-color'>{orderData.vehicleColor}</Text> |
| | | {/* <Text className='car-code'>车身编码: </Text> |
| | | <Text className='code'>{orderData.vehicleCode}</Text> */} |
| | | </View> |
| | | </View> |
| | | <Image src={safeParse(orderData.vehiclePictures, {})?.url || 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/default-vehicle-img-20260724-v2.png'} className='car-img' /> |
| | | |
| | | </View> |
| | | } |
| | | { |
| | | orderData.vehicleId && ( |
| | | <View className='car-box'> |
| | | <Image src={safeParse(orderData.vehiclePictures, {})?.url || 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/default-vehicle-img-20260724-v2.png'} className='car-img' /> |
| | | </View> |
| | | )} |
| | | { |
| | | orderData.status >= 9 && |
| | | <View className='evaluate-card'> |
| | |
| | | </View> |
| | | <View onClick={todescription} className='price-remark'> |
| | | <Text className='remark-text'>费用说明</Text> |
| | | <Image src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/right.png" className='remark-img' /> |
| | | <Image src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/text04-arrow-20260804.png" className='remark-img' /> |
| | | </View> |
| | | </View> |
| | | } |
| | |
| | | } |
| | | deps.setPolyline([{ |
| | | points: displayPoints, |
| | | color: '#6D7FD9', |
| | | color: '#DFB369', |
| | | width: deps.isAndroid ? 4 : 6, |
| | | dottedLine: true, |
| | | arrowLine: false, |
| | |
| | | |
| | | // 接驾态(前往上车点)/到达终点态 上车点三角图标(与 begin-route 对齐) |
| | | const PICKUP_APPROACHING_ICON_URL = |
| | | 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/up-triangle-2026022.png'; |
| | | 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/up-triangle-20260722.png'; |
| | | // 送驾态(前往目的地)/到达终点态 下车点三角图标(与 begin-route 对齐) |
| | | const DROPOFF_INSERVICE_ICON_URL = |
| | | 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/down-triangle-20260722.png'; |
| | |
| | | color: '#141E27', |
| | | fontSize: 12, |
| | | anchorX: 0, |
| | | anchorY: 9, // 图标下方 9px |
| | | anchorY: 7, // 图标下方 7px |
| | | textAlign: 'center' as const, |
| | | bgColor: '#00000000', |
| | | fontWeight: 'bold', |
| | |
| | | color: '#141E27', |
| | | fontSize: 12, |
| | | anchorX: 0, |
| | | anchorY: 8, |
| | | anchorY: 7, |
| | | textAlign: 'center' as const, |
| | | bgColor: '#00000000', |
| | | fontWeight: 'bold', |
| | |
| | | const isPickupArrivedDest = [TripStatus.ArrivedDestination, TripStatus.TripFinished, TripStatus.CancelOnlyPickup].includes(deps.tripStatus); |
| | | const isPickupTriangle = isPickupApproaching || isPickupArrivedDest; |
| | | const pickupNameLabelConfig = isPickupApproaching ? SITE_NAME_LABEL_BASE : SITE_NAME_LABEL_BELOW_LG; |
| | | const isVehicleArrivedPickup = deps.tripStatus === TripStatus.VehicleArrivedPickup; |
| | | const getCustomCalloutAnchorY = () => { |
| | | if (isPickupApproaching) { |
| | | return 12; |
| | | } |
| | | if (isVehicleArrivedPickup && deps.isAndroid) { |
| | | return 8; |
| | | } |
| | | return 6; |
| | | } |
| | | return { |
| | | id: 1, |
| | | latitude: pickupSite.latitude, |
| | |
| | | } |
| | | : { |
| | | customCallout: { |
| | | anchorY: [TripStatus.Publishing, TripStatus.CallingCar].includes(deps.tripStatus) ? 150 : -5, |
| | | anchorY: getCustomCalloutAnchorY(), |
| | | anchorX: 0, |
| | | display: 'ALWAYS' |
| | | } |
| | |
| | | } |
| | | : { |
| | | customCallout: { |
| | | anchorY: -5, |
| | | anchorY: 6, |
| | | anchorX: 0, |
| | | display: 'ALWAYS' |
| | | } |
| | |
| | | deps.isAndroid && [ |
| | | TripStatus.PublishRoute, |
| | | TripStatus.VehicleApproaching, |
| | | TripStatus.VehicleArrivedPickup, |
| | | TripStatus.TripFinished, |
| | | TripStatus.PaymentSuccess, |
| | | TripStatus.CancelOnlyPickup |
| | |
| | | : durationDisplay |
| | | ), [durationDisplay]); |
| | | const shouldHideAndroidMarkerCallout = deps.isAndroid |
| | | && [TripStatus.PublishRoute, TripStatus.VehicleArrivedPickup, TripStatus.TripFinished, TripStatus.PaymentSuccess, TripStatus.CancelOnlyPickup].includes(deps.tripStatus); |
| | | && [TripStatus.PublishRoute, TripStatus.TripFinished, TripStatus.PaymentSuccess, TripStatus.CancelOnlyPickup].includes(deps.tripStatus); |
| | | const androidCarCalloutContent = useMemo(() => ( |
| | | <AndroidCarCalloutContent |
| | | routeDistance={deps.routeDistance} |
| | |
| | | // 车型图:参考 calling-car 的 calling-car-vehicle-pic,浮于背景图顶部右侧 |
| | | .calling-car-vehicle-pic { |
| | | position: absolute; |
| | | top: 24px; |
| | | right: 24px; |
| | | top: -4px; |
| | | right: 116px; |
| | | width: 342px; |
| | | height: 294px; |
| | | z-index: 10; |
| | |
| | | left: 0; |
| | | width: 100%; |
| | | height: 100%; |
| | | background: rgba(0, 0, 0, 0.4); |
| | | background: rgba(0, 0, 0, 0.8); |
| | | z-index: 1; |
| | | } |
| | | |
| | |
| | | z-index: 11; |
| | | } |
| | | } |
| | | } |
| | | } |