| src/robotaxi/pages/begin-route/components/calling-car-modules/door-trunk-control.tsx | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/robotaxi/pages/begin-route/components/calling-car-modules/fare-detail-card.tsx | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/robotaxi/pages/begin-route/hooks/use-android-overlays.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/robotaxi/pages/begin-route/hooks/use-map-markers.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/robotaxi/pages/begin-route/index.tsx | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
src/robotaxi/pages/begin-route/components/calling-car-modules/door-trunk-control.tsx
@@ -7,6 +7,9 @@ const LOCK_CLOSE_ICON_URL = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/lock-close.png'; const LOCK_OPEN_ICON_URL = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/lock-open.png'; const TRUNK_CLOSE_ICON_URL = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/trunk-close.png'; const TRUNK_OPEN_ICON_URL = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/trunk-open.png'; const SWITCH_LOADING_ICON_URL = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/T-switch-loading-v2.png'; type Props = { tripStatus: TripStatus; @@ -27,6 +30,7 @@ }: Props) { const [trunkStatus, setTrunkStatus] = useState(orderTrunkStatus); // 后备箱开关状态 const [lockStatus, setLockStatus] = useState(false); // 车门开关状态 const [loadingControlType, setLoadingControlType] = useState<'door' | 'trunk' | null>(null); // 当前处于指令执行中的控件 const trunkPollTimerRef = useRef<any>(null); // 后备箱状态轮询定时器 const overRoutePopupDebounceTimerRef = useRef<any>(null); // 结束行程弹窗防抖定时器 const OVER_ROUTE_POPUP_DEBOUNCE_MS = 1000; // 结束行程弹窗点击防抖时长 @@ -106,6 +110,9 @@ const handleUnlock = async (doorType: 'door' | 'trunk', onSuccess?: () => void) => { const targetOrderId = getOrderId(orderId); if (!targetOrderId) return; if (loadingControlType) { return; } if (doorType === 'door' && lockStatus) { Taro.showToast({ title: '车门已解锁,请开门上车', icon: 'none' }); return; @@ -116,6 +123,7 @@ Taro.showToast({ title: '获取定位失败,请打开定位开关', icon: 'none' }); return; } setLoadingControlType(doorType); const isCloseAction = doorType === 'trunk' ? !!trunkStatus : false; const actionLoadingText = doorType === 'trunk' ? (isCloseAction ? '正在关闭后备箱' : '正在开启后备箱') @@ -130,6 +138,7 @@ if (res?.code !== 200 || !res?.data) { // Taro.hideLoading(); setLoadingControlType(null); Taro.showToast({ title: res?.msg || '解锁失败,请重试或联系客服', icon: 'none' }); return; } @@ -156,6 +165,7 @@ trunkPollTimerRef.current = setTimeout(doPoll, pollInterval); } else { stopPolling(); setLoadingControlType(null); // Taro.hideLoading(); Taro.showToast({ title: failToastText, @@ -188,6 +198,7 @@ stopPolling(); const isOpen = !isCloseAction; setTrunkStatus(isOpen); setLoadingControlType(null); // Taro.hideLoading(); Taro.showToast({ title: successToastText, @@ -206,6 +217,7 @@ if (doorType === 'door') { // Taro.hideLoading(); setLoadingControlType(null); Taro.showToast({ title: '右侧车门已解锁', icon: 'none' }); setLockStatus(true); onDoorUnlockSuccess?.(); @@ -213,20 +225,38 @@ } }).catch(() => { // Taro.hideLoading(); setLoadingControlType(null); Taro.showToast({ title: '解锁失败,请重试或联系客服', icon: 'none' }); }); }; /** 渲染带 loading 效果的控制按钮图标。 */ const renderControlIcon = (controlType: 'door' | 'trunk', defaultIconSrc: string, className: string) => { const isLoading = loadingControlType === controlType; return ( <View style={{ position: 'relative' }}> <Image className={className} style={isLoading ? { visibility: 'hidden' } : undefined} src={defaultIconSrc} /> {isLoading && ( <Image className={`${className} car-card-temp-switch-img-spinning`} style={{ position: 'absolute', top: 0, left: 0 }} src={SWITCH_LOADING_ICON_URL} /> )} </View> ); }; if (tripStatus === TripStatus.VehicleArrivedPickup) { return ( <View className="car-card-arrive"> <View className={`car-card-arrive-item ${trunkStatus && 'close-btn-item'}`} onClick={() => handleUnlock('trunk', () => setTrunkStatus(!trunkStatus))}> <Image className="car-card-arrive-item-img" src={trunkStatus ? 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/trunk-open.png' : 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/trunk-close.png'} /> <View className="car-card-arrive-item-text">{trunkStatus ? '后备箱' : '后备箱'}</View> <View className={`car-card-arrive-item ${trunkStatus && 'close-btn-item'} ${loadingControlType ? 'trip-action-bar-item-disabled' : ''}`.trim()} onClick={() => handleUnlock('trunk', () => setTrunkStatus(!trunkStatus))}> {renderControlIcon('trunk', trunkStatus ? TRUNK_OPEN_ICON_URL : TRUNK_CLOSE_ICON_URL, 'car-card-arrive-item-img')} <View className="car-card-arrive-item-text" style={loadingControlType === 'trunk' ? { visibility: 'hidden' } : undefined}>{trunkStatus ? '后备箱' : '后备箱'}</View> </View> <View className={`car-card-arrive-item ${lockStatus && 'close-btn-item'}`} onClick={() => handleUnlock('door')}> <Image className="car-card-arrive-item-img" src={lockStatus ? LOCK_OPEN_ICON_URL : LOCK_CLOSE_ICON_URL} /> <View className="car-card-arrive-item-text">车锁</View> <View className={`car-card-arrive-item ${lockStatus && 'close-btn-item'} ${loadingControlType ? 'trip-action-bar-item-disabled' : ''}`.trim()} onClick={() => handleUnlock('door')}> {renderControlIcon('door', lockStatus ? LOCK_OPEN_ICON_URL : LOCK_CLOSE_ICON_URL, 'car-card-arrive-item-img')} <View className="car-card-arrive-item-text" style={loadingControlType === 'door' ? { visibility: 'hidden' } : undefined}>车锁</View> </View> </View> ); @@ -238,9 +268,9 @@ <View className="car-card-reach-text">点击按钮开车门</View> <Image className='car-card-reach-img' src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/door-anim.webp' /> <View className="car-card-reach-btn"> <View className={`car-card-reach-btn-item ${trunkStatus && 'close-btn-item'}`} onClick={() => handleUnlock('trunk', () => setTrunkStatus(!trunkStatus))}> <Image className="car-card-reach-btn-item-img" src={trunkStatus ? 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/trunk-open.png' : 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/trunk-close.png'} /> <View className="car-card-reach-btn-item-text">{trunkStatus ? '后备箱' : '后备箱'}</View> <View className={`car-card-reach-btn-item ${trunkStatus && 'close-btn-item'} ${loadingControlType ? 'trip-action-bar-item-disabled' : ''}`.trim()} onClick={() => handleUnlock('trunk', () => setTrunkStatus(!trunkStatus))}> {renderControlIcon('trunk', trunkStatus ? TRUNK_OPEN_ICON_URL : TRUNK_CLOSE_ICON_URL, 'car-card-reach-btn-item-img')} <View className="car-card-reach-btn-item-text" style={loadingControlType === 'trunk' ? { visibility: 'hidden' } : undefined}>{trunkStatus ? '后备箱' : '后备箱'}</View> </View> <View className="car-card-reach-btn-item" onClick={handleOpenOverRoutePopupWithDebounce}> <Image className="car-card-reach-btn-item-img" src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/over-gps-icon.png" /> src/robotaxi/pages/begin-route/components/calling-car-modules/fare-detail-card.tsx
@@ -24,6 +24,7 @@ onOpenCouponSelect }: Props) { const [showDetail, setShowDetail] = useState(false); // 费用明细展开状态 const canOpenCouponSelect = tripStatus === TripStatus.TripFinished && typeof onOpenCouponSelect === 'function'; // 仅待支付支持切换优惠券 // 外部重置版本变化时收起明细 useEffect(() => { @@ -77,8 +78,8 @@ : <View className="car-card-end-particulars-money" onClick={handleToggleDetail}> <View className="car-card-end-particulars-money-amount">{formatMoney( (fareBreakdown?.orderMoney - fareBreakdown?.redPacketMoney - fareBreakdown?.couponMoney) <= 0 ? 0 : (fareBreakdown?.orderMoney - fareBreakdown?.redPacketMoney - fareBreakdown?.couponMoney))}</View> fareBreakdown?.payMoney <= 0 ? 0 : fareBreakdown?.payMoney)}</View> <View>元</View> { fareBreakdown?.redPacketMoney + fareBreakdown?.couponMoney > 0 && @@ -197,10 +198,12 @@ (fareBreakdown?.operateFree != 1 && [TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(tripStatus)) && Number(fareBreakdown?.redPacketMoney + fareBreakdown?.couponMoney) > 0 && <View className="car-card-end-through"> <View className="car-card-end-through-label">优惠活动</View> <View className="car-card-end-through-content" onClick={onOpenCouponSelect}> <View className="car-card-end-through-content" onClick={canOpenCouponSelect ? onOpenCouponSelect : undefined}> <View className="car-card-end-through-content-title">最佳优惠</View> <View className="car-card-end-through-content-money">-{formatMoney(fareBreakdown?.redPacketMoney + fareBreakdown?.couponMoney)}元</View> <Image src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/ccc-icon-right.png" /> {canOpenCouponSelect && ( <Image src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/ccc-icon-right.png" /> )} </View> </View> } @@ -211,11 +214,11 @@ { fareBreakdown?.operateFree != 1 ? <View className="pay-box-money"><View>{formatMoney( (fareBreakdown?.orderMoney - fareBreakdown?.redPacketMoney - fareBreakdown?.couponMoney) <= 0 ? 0 : (fareBreakdown?.orderMoney - fareBreakdown?.redPacketMoney - fareBreakdown?.couponMoney))}</View><View className="unit">元</View></View> fareBreakdown?.payMoney <= 0 ? 0 : fareBreakdown?.payMoney)}</View><View className="unit">元</View></View> : <View className="pay-box-free">载客测试免费</View> } }{/* */} <View onClick={handlePay} className="pay-box-btn">立即支付</View> </View> } src/robotaxi/pages/begin-route/hooks/use-android-overlays.ts
@@ -20,138 +20,7 @@ latitude: number | null; }; const CAR_CALLOUT_EDGE_GAP = 12; const CAR_CALLOUT_SIDE_GAP = 18; const CAR_CALLOUT_TOP_GAP = 20; const CAR_CALLOUT_BOTTOM_GAP = 34; const CAR_CALLOUT_DEFAULT_SIZE = { width: 166, height: 56 }; const CAR_CALLOUT_WITH_BEFORE_SIZE = { width: 188, height: 84 }; type CarCalloutPlacement = 'top' | 'bottom' | 'left' | 'right'; /** * 计算车辆附近路线切线方向,供气泡选边使用。 */ const resolveRouteVector = ( routePoints: Array<{ latitude: number; longitude: number }> = [], vehicleLocation: { latitude: number; longitude: number } | null ) => { if (!vehicleLocation || routePoints.length < 2) { return null; } let closestIndex = 0; let closestDistance = Infinity; routePoints.forEach((point, index) => { const lat = Number(point?.latitude); const lng = Number(point?.longitude); if (!Number.isFinite(lat) || !Number.isFinite(lng)) return; const distance = ((lat - vehicleLocation.latitude) ** 2) + ((lng - vehicleLocation.longitude) ** 2); if (distance < closestDistance) { closestDistance = distance; closestIndex = index; } }); const prevIndex = closestIndex <= 0 ? 0 : closestIndex - 1; const nextIndex = closestIndex >= routePoints.length - 1 ? routePoints.length - 1 : closestIndex + 1; if (prevIndex === nextIndex) { return null; } const prevPoint = routePoints[prevIndex]; const nextPoint = routePoints[nextIndex]; const deltaX = Number(nextPoint?.longitude) - Number(prevPoint?.longitude); const deltaY = Number(nextPoint?.latitude) - Number(prevPoint?.latitude); if (!Number.isFinite(deltaX) || !Number.isFinite(deltaY)) { return null; } return { deltaX, deltaY }; }; /** * 基于路线走向与当前屏幕空间,选择车辆气泡最稳妥的停靠方向。 */ const resolveCarCalloutPlacement = ( pos: { x: number; y: number }, rect: any, routeVector: { deltaX: number; deltaY: number } | null, calloutSize: { width: number; height: number }, verticalExtraOffset: number ) => { const routeIsMostlyHorizontal = !!routeVector && Math.abs(routeVector.deltaX) >= Math.abs(routeVector.deltaY); const topPreferred = rect ? (pos.y - rect.top) >= (rect.top + rect.height - pos.y) : false; const leftPreferred = rect ? (pos.x - rect.left) >= (rect.left + rect.width - pos.x) : false; const preferredOrder: CarCalloutPlacement[] = routeIsMostlyHorizontal ? (topPreferred ? ['top', 'bottom', leftPreferred ? 'left' : 'right', leftPreferred ? 'right' : 'left'] : ['bottom', 'top', leftPreferred ? 'left' : 'right', leftPreferred ? 'right' : 'left']) : (leftPreferred ? ['left', 'right', topPreferred ? 'top' : 'bottom', topPreferred ? 'bottom' : 'top'] : ['right', 'left', topPreferred ? 'top' : 'bottom', topPreferred ? 'bottom' : 'top']); const computeCandidate = (placement: CarCalloutPlacement) => { if (placement === 'top') { return { placement, left: pos.x - (calloutSize.width / 2), top: pos.y - calloutSize.height - CAR_CALLOUT_TOP_GAP, }; } if (placement === 'bottom') { return { placement, left: pos.x - (calloutSize.width / 2), top: pos.y + CAR_CALLOUT_BOTTOM_GAP + verticalExtraOffset, }; } if (placement === 'left') { return { placement, left: pos.x - calloutSize.width - CAR_CALLOUT_SIDE_GAP, top: pos.y - (calloutSize.height / 2), }; } return { placement, left: pos.x + CAR_CALLOUT_SIDE_GAP, top: pos.y - (calloutSize.height / 2), }; }; const scoreCandidate = (candidate: { placement: CarCalloutPlacement; left: number; top: number }) => { const right = candidate.left + calloutSize.width; const bottom = candidate.top + calloutSize.height; const minLeft = rect.left + CAR_CALLOUT_EDGE_GAP; const minTop = rect.top + CAR_CALLOUT_EDGE_GAP; const maxRight = rect.left + rect.width - CAR_CALLOUT_EDGE_GAP; const maxBottom = rect.top + rect.height - CAR_CALLOUT_EDGE_GAP; const overflowPenalty = ( Math.max(0, minLeft - candidate.left) + Math.max(0, minTop - candidate.top) + Math.max(0, right - maxRight) + Math.max(0, bottom - maxBottom) ) * 100; const routePenalty = routeIsMostlyHorizontal ? (candidate.placement === 'left' || candidate.placement === 'right' ? 24 : 0) : (candidate.placement === 'top' || candidate.placement === 'bottom' ? 24 : 0); const freeSpaceScore = Math.min( candidate.left - minLeft, candidate.top - minTop, maxRight - right, maxBottom - bottom ); const orderPenalty = preferredOrder.indexOf(candidate.placement) * 4; return freeSpaceScore - overflowPenalty - routePenalty - orderPenalty; }; const bestCandidate = preferredOrder .map(computeCandidate) .sort((first, second) => scoreCandidate(second) - scoreCandidate(first))[0]; const minLeft = rect.left + CAR_CALLOUT_EDGE_GAP; const minTop = rect.top + CAR_CALLOUT_EDGE_GAP; const maxLeft = rect.left + rect.width - calloutSize.width - CAR_CALLOUT_EDGE_GAP; const maxTop = rect.top + rect.height - calloutSize.height - CAR_CALLOUT_EDGE_GAP; return { left: Math.min(Math.max(bestCandidate.left, minLeft), Math.max(minLeft, maxLeft)), top: Math.min(Math.max(bestCandidate.top, minTop), Math.max(minTop, maxTop)) }; }; export default function useAndroidOverlays(deps: Deps) { // 等待雷达样式与显示状态 @@ -244,20 +113,9 @@ const pos = await computeScreenPosition(deps.vehicleLocation, rect, 0); if (!pos) return; const verticalExtraOffset = deps.hasBeforeUserEndSite ? 18 : 0; if (deps.tripStatus === TripStatus.InService) { const routeVector = resolveRouteVector(deps.routePoints || [], deps.vehicleLocation); const calloutSize = deps.hasBeforeUserEndSite ? CAR_CALLOUT_WITH_BEFORE_SIZE : CAR_CALLOUT_DEFAULT_SIZE; const placement = resolveCarCalloutPlacement(pos, rect, routeVector, calloutSize, verticalExtraOffset); setCarCalloutStyle(buildTransformOverlayStyle({ x: placement.left, y: placement.top }, '')); setCarCalloutVisible(true); return; } setCarCalloutStyle(buildTransformOverlayStyle({ x: pos.x, y: pos.y + 34 + verticalExtraOffset y: pos.y + CAR_CALLOUT_BOTTOM_GAP + verticalExtraOffset }, 'translate(-50%, 6px)')); setCarCalloutVisible(true); }; src/robotaxi/pages/begin-route/hooks/use-map-markers.ts
@@ -203,9 +203,9 @@ anchor: { x: 0.5, y: 0.5 }, zIndex: 2, name: '车辆位置', // ...(!shouldUsePureTranslateCarMarker ? { // rotate: typeof deps.vehicleDirection === 'number' ? deps.vehicleDirection : 0, // } : {}), ...(deps.isAndroid ? { rotate: typeof deps.vehicleDirection === 'number' ? deps.vehicleDirection : 0, } : {}), customCallout: { anchorY, anchorX, display: 'ALWAYS' } }; }; src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx
@@ -211,6 +211,12 @@ export default function useViewDelegates(deps: Deps) { /** 时长文案 */ const durationDisplay = useMemo(() => formatRemainingDurationDisplay(deps.routeDuration), [deps.routeDuration]); /** 车辆公里/分钟气泡专用时长文案:不足 1 分钟时统一展示 1 分钟。 */ const vehicleMetricDurationDisplay = useMemo(() => ( durationDisplay.unit === '秒' ? { value: 1, unit: '分钟' as const } : durationDisplay ), [durationDisplay]); /** 修改目的地预览态下隐藏车辆公里/分钟气泡。 */ const shouldShowVehicleMetricCallout = !deps.isModifyDestinationReviewVisible; const routeSummaryText = useMemo(() => ( @@ -223,9 +229,9 @@ <AndroidCarCalloutContent hasBeforeUserEndSite={deps.hasBeforeUserEndSite} routeDistance={deps.routeDistance} durationDisplay={durationDisplay} durationDisplay={vehicleMetricDurationDisplay} /> ), [deps.hasBeforeUserEndSite, deps.routeDistance, durationDisplay]); ), [deps.hasBeforeUserEndSite, deps.routeDistance, vehicleMetricDurationDisplay]); /** * 渲染等待态气泡。 @@ -282,8 +288,8 @@ <CoverView className='car-time'> <CoverView className='car-time-num'>{deps.routeDistance?.value ?? 0}</CoverView> <CoverView>{deps.routeDistance?.unit === 'm' ? '米' : '公里'}</CoverView> <CoverView style={{ marginLeft: '2.5px' }} className='car-time-num'>{durationDisplay.value}</CoverView> <CoverView>{durationDisplay.unit}</CoverView> <CoverView style={{ marginLeft: '2.5px' }} className='car-time-num'>{vehicleMetricDurationDisplay.value}</CoverView> <CoverView>{vehicleMetricDurationDisplay.unit}</CoverView> </CoverView> </CoverView> )} @@ -305,8 +311,8 @@ deps.routeSummaryBgSrc, deps.showTrip1StyleEndpoint, deps.tripStatus, durationDisplay, routeSummaryText, vehicleMetricDurationDisplay, shouldShowVehicleMetricCallout ]); src/robotaxi/pages/begin-route/index.tsx
@@ -1347,7 +1347,7 @@ }; // 打开优惠券选择弹窗 const handleOpenCouponSelect = () => { if (![TripStatus.PublishRoute, TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(tripStatus)) { if (![TripStatus.PublishRoute, TripStatus.TripFinished].includes(tripStatus)) { return; } setShowCouponSelectPopup(true); @@ -1356,6 +1356,12 @@ const handleCloseCouponSelect = () => { setShowCouponSelectPopup(false); }; // 支付成功后关闭优惠券选择弹窗 useEffect(() => { if (tripStatus === TripStatus.PaymentSuccess) { setShowCouponSelectPopup(false); } }, [tripStatus]); // 选择优惠券后仅刷新价格,不刷新地图路线 const handleSelectCoupon = (item: any) => { const selectedCouponId = String(item?.operationCouponId || item?.couponId || '');