| | |
| | | @import url('../../../../style/ui-variables.less'); |
| | | |
| | | .auth-bluetooth-popup { |
| | | position: fixed; |
| | | top: 0; |
| | |
| | | left: 0; |
| | | width: 100%; |
| | | height: 100%; |
| | | background: rgba(0, 0, 0, 0.4); |
| | | background: rgba(0, 0, 0, 0.8); |
| | | z-index: 1; |
| | | } |
| | | |
| | | .popup-content { |
| | | position: relative; |
| | | z-index: 2; |
| | | background: #FFFFFF; |
| | | border-radius: 40px 40px 0 0; |
| | | background: linear-gradient(180deg, #D9EBFF 0%, #FFFFFF 35%); |
| | | border-radius: 40px 40px 0 0; |
| | | background: @gary-theme; |
| | | border-radius: 60px 60px 0 0; |
| | | width: 100%; |
| | | transform: translateY(100%); |
| | | opacity: 1; |
| | |
| | | .content-main { |
| | | position: relative; |
| | | z-index: 2; |
| | | padding: 80px 58px 80px 40px; |
| | | padding: 96px 48px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | } |
| | |
| | | font-size: 36px; |
| | | font-weight: 500; |
| | | line-height: 52px; |
| | | color: rgba(0, 0, 0, 0.9); |
| | | color: @black-text-main; |
| | | } |
| | | |
| | | .popup-sub-title { |
| | | font-size: 36px; |
| | | font-weight: 500; |
| | | line-height: 52px; |
| | | color: rgba(0, 0, 0, 0.9); |
| | | color: @black-text-main; |
| | | } |
| | | |
| | | .popup-desc { |
| | | font-size: 28px; |
| | | line-height: 44px; |
| | | color: rgba(0, 0, 0, 0.6); |
| | | color: @black-text-04; |
| | | } |
| | | |
| | | .authorize-btn { |
| | | margin-top: 82px; |
| | | margin-top: 80px; |
| | | width: 100%; |
| | | height: 88px; |
| | | margin-left: 18px; |
| | | border-radius: 16px; |
| | | background: linear-gradient(90deg, #7b79ff 0%, #68c0ff 100%); |
| | | height: 106px; |
| | | box-sizing: border-box; |
| | | border-radius: 2000px; |
| | | background: @black-text-main; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | font-size: 28px; |
| | | text-align: center; |
| | | font-size: 32px; |
| | | font-weight: 500; |
| | | color: #ffffff; |
| | | } |
| | | } |
| | | } |
| | |
| | | @import url('../../../../../style/ui-variables.less'); |
| | | |
| | | .success-auth-popup { |
| | | position: fixed; |
| | | top: 0; |
| | |
| | | left: 0; |
| | | width: 100%; |
| | | height: 100%; |
| | | background: rgba(0, 0, 0, 0.4); |
| | | background: rgba(0, 0, 0, 0.8); |
| | | z-index: 1; |
| | | } |
| | | |
| | |
| | | position: relative; |
| | | z-index: 2; |
| | | width: 100%; |
| | | background: linear-gradient(180deg, #D9EBFF 0%, #FFFFFF 35%); |
| | | border-radius: 40px 40px 0 0; |
| | | background: @gary-theme; |
| | | border-radius: 60px 60px 0 0; |
| | | transform: translateY(100%); |
| | | opacity: 1; |
| | | padding: 58px 134px 80px 140px; |
| | | padding: 96px 120px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | } |
| | | |
| | | .popup-icon { |
| | | width: 148px; |
| | | height: 148px; |
| | | width: 144px; |
| | | height: 144px; |
| | | } |
| | | |
| | | .popup-title { |
| | |
| | | font-size: 34px; |
| | | font-weight: 500; |
| | | line-height: 52px; |
| | | color: rgba(0, 0, 0, 0.9); |
| | | color: @black-text-main; |
| | | letter-spacing: normal; |
| | | } |
| | | |
| | |
| | | margin-top: 4px; |
| | | font-size: 24px; |
| | | line-height: 36px; |
| | | color: rgba(0, 0, 0, 0.6); |
| | | color: @black-text-04; |
| | | text-align: center; |
| | | } |
| | | |
| | | .popup-confirm-btn { |
| | | margin-top: 66px; |
| | | width: 100%; |
| | | height: 88px; |
| | | border-radius: 16px; |
| | | border: 2px solid rgba(0, 0, 0, 1); |
| | | height: 106px; |
| | | border-radius: 2000px; |
| | | border: 4px solid @black-text-main; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | font-size: 28px; |
| | | color: rgba(0, 0, 0, 0.9); |
| | | font-size: 32px; |
| | | font-weight: 500; |
| | | color: @black-text-main; |
| | | } |
| | | } |
| | | } |
| | |
| | | <View className={`success-auth-popup ${show ? 'show' : ''}`} catchMove> |
| | | <View className='popup-mask' onClick={onClose}></View> |
| | | <View className='popup-content' animation={animationData}> |
| | | <Image className='popup-icon' src={'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/bluetooth-success-auth.png'} /> |
| | | <Image className='popup-icon' src={'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/bluetooth-popup-icon-20260726.png'} /> |
| | | <Text className='popup-title'>已授权蓝牙,无感自动解锁车门</Text> |
| | | <Text className='popup-desc'>授权并打开蓝牙连接,手机靠近车辆即可自动解锁车门</Text> |
| | | <View className='popup-confirm-btn' onClick={handleConfirm}> |
| | |
| | | mapScale: number; |
| | | /** 修改目的地预览态下隐藏车辆气泡。 */ |
| | | isModifyDestinationReviewVisible: boolean; |
| | | routeSummaryLocation: { latitude: number; longitude: number } | null; |
| | | startSite: any; |
| | | endSite: any; |
| | | vehicleLocation: { latitude: number; longitude: number } | null; |
| | |
| | | // 到达气泡样式与显示状态 |
| | | const [arriveCalloutStyle, setArriveCalloutStyle] = useState<any>({}); |
| | | const [arriveCalloutVisible, setArriveCalloutVisible] = useState(false); |
| | | // 路线摘要气泡样式与显示状态 |
| | | const [routeSummaryCalloutStyle, setRouteSummaryCalloutStyle] = useState<any>({}); |
| | | const [routeSummaryCalloutVisible, setRouteSummaryCalloutVisible] = useState(false); |
| | | const mapRectRef = useRef<any>(null); |
| | | |
| | | // 读取地图投影上下文,供路线点批量转换屏幕坐标使用。 |
| | |
| | | setArriveCalloutVisible(true); |
| | | }; |
| | | |
| | | // 更新路线摘要气泡位置 |
| | | const updateRouteSummaryCalloutPosition = async () => { |
| | | if (!deps.isAndroid || !deps.routeSummaryLocation) { |
| | | setRouteSummaryCalloutVisible(false); |
| | | return; |
| | | } |
| | | const rect = await ensureMapRect(); |
| | | const pos = await computeScreenPosition(deps.routeSummaryLocation, rect, 0); |
| | | if (!pos) { |
| | | setRouteSummaryCalloutVisible(false); |
| | | return; |
| | | } |
| | | setRouteSummaryCalloutStyle(buildTransformOverlayStyle({ |
| | | x: pos.x, |
| | | y: pos.y |
| | | }, '')); |
| | | setRouteSummaryCalloutVisible(true); |
| | | }; |
| | | |
| | | // 地图区域变化时同步覆盖物显隐与重算 |
| | | // 注意:这里把「begin 隐藏」与「end 重算」解耦,避免历史上的两类问题: |
| | | // 1) 若 begin 对所有区域变化都隐藏,则 fitRouteIntoView/includePoints 等程序化视口调整会 |
| | |
| | | setWaitingRadarVisible(false); |
| | | setCarCalloutVisible(false); |
| | | setArriveCalloutVisible(false); |
| | | setRouteSummaryCalloutVisible(false); |
| | | } |
| | | return; |
| | | } |
| | |
| | | updateWaitingRadarPosition(); |
| | | updateCarCalloutPosition(); |
| | | updateArriveCalloutPosition(); |
| | | updateRouteSummaryCalloutPosition(); |
| | | } |
| | | }; |
| | | |
| | |
| | | }, [deps.isAndroid, deps.tripStatus, deps.endSite?.latitude, deps.endSite?.longitude]); |
| | | |
| | | useEffect(() => { |
| | | updateRouteSummaryCalloutPosition(); |
| | | }, [deps.isAndroid, deps.routeSummaryLocation?.latitude, deps.routeSummaryLocation?.longitude, deps.mapScale, deps.tripStatus]); |
| | | |
| | | useEffect(() => { |
| | | if (!deps.isAndroid || deps.tripStatus !== TripStatus.ArrivedDestination) return; |
| | | if (deps.mapScale < 20) return; |
| | | const timer = setTimeout(() => { |
| | |
| | | carCalloutVisible, |
| | | arriveCalloutStyle, |
| | | arriveCalloutVisible, |
| | | routeSummaryCalloutStyle, |
| | | routeSummaryCalloutVisible, |
| | | updateWaitingRadarPosition, |
| | | updateCarCalloutPosition, |
| | | updateArriveCalloutPosition, |
| | | updateRouteSummaryCalloutPosition, |
| | | handleRegionChange |
| | | }; |
| | | } |
| | |
| | | useAndroidNativeTrip1Callout: boolean |
| | | ) => { |
| | | if (!(pickupSite?.latitude && pickupSite?.longitude)) return null; |
| | | const shouldUseChangePickupIcon = deps.isModifyDestinationReviewVisible && deps.tripStatus >= TripStatus.VehicleArrivedPickup; // 修改目的地确认态且进入行程阶段时使用换点上车图标 |
| | | const shouldHidePickupName = deps.tripStatus === TripStatus.InService && deps.isModifyDestinationReviewVisible; // 行程中修改目的地时只展示换点图标,不展示上车点名称 |
| | | // 修改目的地预览:起点改为 up-triangle 三角图标 + 名称显示在图标下方,不再用换点图标、不隐藏名称 |
| | | const shouldUseChangePickupIcon = false; |
| | | const shouldHidePickupName = false; |
| | | // 接驾态(前往上车点):上车点改用三角图标,名称移到图标下方,浮窗展示车图标+限停时间; |
| | | // 待支付/完成态:上车点同样三角图标,浮窗展示 end-site 图片,名称移到图标左侧。 |
| | | const isPickupApproaching = |
| | |
| | | const isPickupSettled = |
| | | [TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(deps.tripStatus) && |
| | | !shouldUseChangePickupIcon; |
| | | const isPickupTriangle = isPickupApproaching || isPickupArrived || isPickupSettled; |
| | | // 修改目的地预览:起点(上车点)统一用三角图标 up-triangle + 名称显示在图标下方 |
| | | const isPickupModifyPreview = deps.isModifyDestinationReviewVisible; |
| | | const isPickupTriangle = isPickupApproaching || isPickupArrived || isPickupSettled || isPickupModifyPreview; |
| | | // 名称在图标下方:接驾态 11px,到达/待支付/完成态 22px |
| | | const pickupNameLabelConfig = isPickupApproaching |
| | | ? SITE_NAME_LABEL_BASE |
| | |
| | | anchor: { x: 0.5, y: 1 }, |
| | | zIndex: 1, |
| | | name: pickupName, |
| | | hideNameCallout: deps.isModifyDestinationReviewVisible || shouldHidePickupName, // 修改目的地确认态隐藏上车点名称气泡 |
| | | hideNameCallout: shouldHidePickupName, // 名称通过图标下方 label 展示;修改态的名称气泡/限停/倒计时由各气泡分支单独控制 |
| | | // 三角态在图标下方展示站点名称 |
| | | ...(isPickupTriangle && !shouldHidePickupName |
| | | ? { label: { content: pickupName, ...pickupNameLabelConfig } } |
| | |
| | | zIndex: 1, |
| | | name: dropoffName, |
| | | // 三角态在图标下方展示目的地名称:送驾态 11px、待支付/完成态 22px |
| | | ...(isDropoffTriangle |
| | | // 修改目的地预览态下,站点名已在黑色浮窗(endpoint-summary)展示,隐藏图标下方的重复名称 |
| | | ...(isDropoffTriangle && !deps.isModifyDestinationReviewVisible |
| | | ? { |
| | | label: { |
| | | content: dropoffName, |
| | |
| | | waitingCalloutBgUrl: string; |
| | | /** 等待态雷达图地址。 */ |
| | | waitingRadarUrl: string; |
| | | /** 路线概览气泡背景图地址。 */ |
| | | routeSummaryBgUrl: string; |
| | | }; |
| | | |
| | | /** Hook 入参。 */ |
| | |
| | | waitingCalloutBgSrc: string; |
| | | /** 等待态雷达图本地地址。 */ |
| | | waitingRadarSrc: string; |
| | | /** 路线概览气泡背景图本地地址。 */ |
| | | routeSummaryBgSrc: string; |
| | | }; |
| | | |
| | | /** |
| | |
| | | const [customCalloutIconSrc, setCustomCalloutIconSrc] = useState(params.assetUrls.customCalloutIconUrl); // 自定义气泡箭头图本地地址 |
| | | const [waitingCalloutBgSrc, setWaitingCalloutBgSrc] = useState(params.assetUrls.waitingCalloutBgUrl); // 等待态气泡背景图本地地址 |
| | | const [waitingRadarSrc, setWaitingRadarSrc] = useState(params.assetUrls.waitingRadarUrl); // 等待态雷达图本地地址 |
| | | const [routeSummaryBgSrc, setRouteSummaryBgSrc] = useState(params.assetUrls.routeSummaryBgUrl); // 路线概览气泡背景图本地地址 |
| | | |
| | | /** |
| | | * 预加载单张图片并在组件仍挂载时回写结果。 |
| | |
| | | useEffect(() => { |
| | | const cancelledRef = { current: false }; |
| | | preloadSingleAsset(params.assetUrls.customCalloutIconUrl, setCustomCalloutIconSrc, cancelledRef); |
| | | preloadSingleAsset(params.assetUrls.routeSummaryBgUrl, setRouteSummaryBgSrc, cancelledRef); |
| | | return () => { |
| | | cancelledRef.current = true; |
| | | }; |
| | | }, [params.assetUrls.customCalloutIconUrl, params.assetUrls.routeSummaryBgUrl]); |
| | | }, [params.assetUrls.customCalloutIconUrl]); |
| | | |
| | | useEffect(() => { |
| | | if (![TripStatus.Publishing, TripStatus.CallingCar].includes(params.tripStatus)) { |
| | |
| | | return { |
| | | customCalloutIconSrc, |
| | | waitingCalloutBgSrc, |
| | | waitingRadarSrc, |
| | | routeSummaryBgSrc |
| | | waitingRadarSrc |
| | | }; |
| | | } |
| | |
| | | arriveCalloutVisible: boolean; |
| | | /** 到达气泡样式 */ |
| | | arriveCalloutStyle: any; |
| | | /** 路线摘要气泡可见 */ |
| | | routeSummaryCalloutVisible: boolean; |
| | | /** 路线摘要气泡样式 */ |
| | | routeSummaryCalloutStyle: any; |
| | | /** 到达态气泡渲染就绪(角度与缩放均就绪) */ |
| | | arrivedTooltipReady: boolean; |
| | | /** 路线距离 */ |
| | |
| | | hasPreviousOrder: boolean; |
| | | /** 自定义气泡图标 */ |
| | | customCalloutIconSrc: string; |
| | | /** 路线概览气泡背景图 */ |
| | | routeSummaryBgSrc: string; |
| | | /** 是否使用下单前端点气泡样式 */ |
| | | showTrip1StyleEndpoint: boolean; |
| | | /** 上车点 */ |
| | |
| | | /** 修改目的地预览态和接驾态切路线期间隐藏车辆公里/分钟气泡。 */ |
| | | const shouldShowVehicleMetricCallout = |
| | | !deps.isModifyDestinationReviewVisible && !deps.isPickupRouteChangeVisible; |
| | | const routeSummaryText = useMemo( |
| | | () => |
| | | deps.routeDistance && deps.routeDuration !== null |
| | | ? `${vehicleMetricDurationDisplay.value}${vehicleMetricDurationDisplay.unit |
| | | } ${deps.routeDistance.value}${deps.routeDistance.unit === "m" ? "米" : "公里" |
| | | }` |
| | | : "", |
| | | [deps.routeDistance, deps.routeDuration, vehicleMetricDurationDisplay] |
| | | ); |
| | | const shouldHideAndroidMarkerCallout = |
| | | deps.isAndroid && |
| | | ([ |
| | |
| | | } |
| | | style={item.id === 1 ? calloutRepaintStyle : undefined} |
| | | > |
| | | {item.isRouteSummaryBubble ? ( |
| | | routeSummaryText ? ( |
| | | // 路由摘要气泡 |
| | | <CoverView className="route-summary-callout"> |
| | | <CoverImage |
| | | className="route-summary-callout-bg" |
| | | src={deps.routeSummaryBgSrc} |
| | | /> |
| | | <CoverView className="route-summary-callout-text"> |
| | | {routeSummaryText} |
| | | {/* 占位 */} |
| | | <CoverView style={{ color: "transparent" }}>占</CoverView> |
| | | </CoverView> |
| | | </CoverView> |
| | | ) : null |
| | | ) : item.isBeforeUserSite ? ( |
| | | {item.isBeforeUserSite ? ( |
| | | <CoverView className="before-user-site-callout"> |
| | | <CoverView className="before-user-site-txt">上一站终点</CoverView> |
| | | </CoverView> |
| | |
| | | TripStatus.TripFinished, |
| | | TripStatus.PaymentSuccess |
| | | ].includes(deps.tripStatus) |
| | | ) && |
| | | !( |
| | | (item.id === 1 || item.id === 2) && |
| | | deps.isModifyDestinationReviewVisible |
| | | ) && |
| | | !item.hideNameCallout && ( |
| | | <> |
| | |
| | | </> |
| | | )} |
| | | {item.id === 2 && |
| | | deps.tripStatus === TripStatus.PublishRoute && |
| | | (deps.tripStatus === TripStatus.PublishRoute || deps.isModifyDestinationReviewVisible) && |
| | | !!deps.routeDistance && |
| | | deps.routeDuration !== null && |
| | | !item.hideNameCallout && ( |
| | |
| | | </CoverView> |
| | | </CoverView> |
| | | </CoverView> |
| | | <CoverImage |
| | | className="endpoint-summary-icon" |
| | | src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/map-white-arrow-20260708.png" |
| | | /> |
| | | {!deps.isModifyDestinationReviewVisible && ( |
| | | <CoverImage |
| | | className="endpoint-summary-icon" |
| | | src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/map-white-arrow-20260708.png" |
| | | /> |
| | | )} |
| | | </CoverView> |
| | | <CoverImage |
| | | className="callout-diamond-icon" |
| | |
| | | )} |
| | | {item.id === 1 && |
| | | deps.tripStatus === TripStatus.VehicleApproaching && |
| | | !deps.isModifyDestinationReviewVisible && |
| | | !item.hideNameCallout && ( |
| | | <> |
| | | <CoverView |
| | |
| | | )} |
| | | {item.id === 1 && |
| | | deps.tripStatus === TripStatus.VehicleArrivedPickup && |
| | | !deps.isModifyDestinationReviewVisible && |
| | | !item.hideNameCallout && |
| | | Number(pickupCountdownSeconds) > 0 && ( |
| | | <CoverView |
| | |
| | | </CoverView> |
| | | )} |
| | | {item.id === 1 && |
| | | [TripStatus.TripFinished, TripStatus.PaymentSuccess].includes( |
| | | ([TripStatus.TripFinished, TripStatus.PaymentSuccess].includes( |
| | | deps.tripStatus |
| | | ) && |
| | | ) || deps.isModifyDestinationReviewVisible) && |
| | | !item.hideNameCallout && ( |
| | | <CoverImage |
| | | className="site-status-end-img" |
| | |
| | | TripStatus.TripFinished, |
| | | TripStatus.PaymentSuccess |
| | | ].includes(deps.tripStatus) && |
| | | !deps.isModifyDestinationReviewVisible && |
| | | !item.hideNameCallout && ( |
| | | <CoverImage |
| | | className="site-status-end-img" |
| | |
| | | deps.isModifyDestinationReviewVisible, |
| | | deps.routeDistance, |
| | | deps.routeDuration, |
| | | deps.routeSummaryBgSrc, |
| | | deps.showTrip1StyleEndpoint, |
| | | deps.tripStatus, |
| | | deps.startSite?.time, |
| | | pickupCountdownSeconds, |
| | | routeSummaryText, |
| | | vehicleMetricDurationDisplay, |
| | | shouldShowVehicleMetricCallout, |
| | | calloutRepaintStyle |
| | |
| | | */ |
| | | const renderMarkerCallout = useCallback((item: any, index: number) => { |
| | | const calloutKey = `callout-${item.id}-${item.name || ''}`; |
| | | // 路线摘要气泡需跨平台强制显示,避免被 Android 普通气泡隐藏规则误伤 |
| | | if (item.isRouteSummaryBubble) { |
| | | if (deps.isAndroid) return null; |
| | | // iOS 发布路线态:路程/时间已合并进终点浮窗,避免与终点气泡重复展示 |
| | | if (deps.tripStatus === TripStatus.PublishRoute) return null; |
| | | return renderDefaultCallout(item, calloutKey); |
| | | } |
| | | // 待支付/完成态的上/下车点浮窗为富内容(end-site 图片),需走 cover-view,不能被 Android 常规隐藏规则误伤 |
| | | const isSettledSiteMarker = |
| | | (item.id === 1 || item.id === 2) && |
| | |
| | | style={deps.waitingRadarStyle} |
| | | /> |
| | | )} |
| | | {deps.isAndroid && deps.routeSummaryCalloutVisible && routeSummaryText && ( |
| | | <View |
| | | className="route-summary-callout" |
| | | style={deps.routeSummaryCalloutStyle} |
| | | > |
| | | <Image |
| | | className="route-summary-callout-bg" |
| | | src={deps.routeSummaryBgSrc} |
| | | /> |
| | | <View className="route-summary-callout-text"> |
| | | {routeSummaryText} |
| | | </View> |
| | | </View> |
| | | )} |
| | | {deps.isAndroid && deps.carCalloutVisible && shouldShowVehicleMetricCallout && ( |
| | | <View className="car-callout-android" style={deps.carCalloutStyle}> |
| | | {androidCarCalloutContent} |
| | |
| | | deps.isModifyDestinationReviewVisible, |
| | | deps.markers, |
| | | deps.routeDistance, |
| | | deps.routeSummaryBgSrc, |
| | | deps.routeSummaryCalloutStyle, |
| | | deps.routeSummaryCalloutVisible, |
| | | deps.shouldShowWaitingRadar, |
| | | deps.waitingRadarSrc, |
| | | deps.waitingRadarStyle, |
| | | deps.waitingRadarVisible, |
| | | renderMarkerCallout, |
| | | routeSummaryText, |
| | | shouldShowVehicleMetricCallout, |
| | | vehicleMetricDurationDisplay |
| | | ]); |
| | |
| | | padding-right: 20px; |
| | | } |
| | | |
| | | .route-summary-callout { |
| | | position: relative; |
| | | display: inline-flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | min-width: 169px; |
| | | height: 45px; |
| | | padding-left: 15px; |
| | | padding-right: 4px; |
| | | box-sizing: border-box; |
| | | } |
| | | |
| | | .route-summary-callout-bg { |
| | | position: absolute; |
| | | left: 0; |
| | | top: 0; |
| | | width: 100%; |
| | | height: 100%; |
| | | } |
| | | |
| | | .route-summary-callout-text { |
| | | position: relative; |
| | | z-index: 2; |
| | | font-size: 20px; |
| | | font-weight: 500; |
| | | line-height: 39px; |
| | | color: #FFFFFF; |
| | | white-space: nowrap; |
| | | text-align: center; |
| | | display: flex; |
| | | } |
| | | |
| | | .txt-center { |
| | | text-align: center; |
| | | } |
| | |
| | | import useSmoothRouteMotion from './hooks/use-smooth-route-motion'; |
| | | // Android 覆盖物状态管理 |
| | | import useAndroidOverlays from './hooks/use-android-overlays'; |
| | | import useRouteSummaryAnchorLocation from './hooks/use-route-summary-anchor-location'; |
| | | // 地图视口状态管理 |
| | | import useMapViewport from './hooks/use-map-viewport'; |
| | | import useActiveTripScale from './hooks/use-active-trip-scale'; |
| | |
| | | import { withToast } from '../../components/toast/with-toast'; |
| | | import type { PickupExceptionInfo } from './pickup-exception'; |
| | | |
| | | const ROUTE_SUMMARY_MARKER_ID = 999001; |
| | | const MODIFY_DESTINATION_PICKUP_STATUS_GUARD_MS = 5000; |
| | | const COUPON_POPUP_CLOSE_VIEWPORT_REFRESH_DELAY_MS = 320; // 优惠券弹窗退场动画结束后,再刷新完成态视口 |
| | | const ROUTE_SELECTABLE_TRIP_STATUSES = [TripStatus.PublishRoute, TripStatus.VehicleApproaching] as const; // 允许展示并切换候选路线的行程状态 |
| | |
| | | const customCalloutIconUrl = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/maker-tooltip-right.png'; // 自定义气泡图标URL |
| | | const waitingCalloutBgUrl = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/tooltip-bg-20260715.png'; // 等待气泡背景图URL |
| | | const waitingRadarUrl = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/waiting-radar.webp'; // 等待雷达动画图URL |
| | | const routeSummaryBgUrl = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/summary-icon.png'; // 路线概览气泡背景图URL |
| | | const [isRouteSelectExpanded, setIsRouteSelectExpanded] = useState(false); // 路线选择展开状态 |
| | | const [isPickupRouteChangeVisible, setIsPickupRouteChangeVisible] = useState(false); // 接驾态路线切换卡片展示状态 |
| | | const [isConfirmingPickupRouteChange, setIsConfirmingPickupRouteChange] = useState(false); // 接驾态确认切换路线提交中 |
| | |
| | | const vehicleMarkerId = !isAndroid && tripStatus === TripStatus.InService ? 30 : 3; // 车辆标记 ID,iOS 行驶中阶段使用不同 ID |
| | | const { |
| | | customCalloutIconSrc, |
| | | waitingRadarSrc, |
| | | routeSummaryBgSrc |
| | | waitingRadarSrc |
| | | } = usePreloadedMapAssets({ |
| | | tripStatus, |
| | | assetUrls: { |
| | | customCalloutIconUrl, |
| | | waitingCalloutBgUrl, |
| | | waitingRadarUrl, |
| | | routeSummaryBgUrl |
| | | waitingRadarUrl |
| | | } |
| | | }); // 地图气泡资源预加载 |
| | | const { |
| | |
| | | const isTripBeforePickupArrived = [TripStatus.Publishing, TripStatus.CallingCar, TripStatus.VehicleApproaching].includes(tripStatus); // 车辆未到达上车点前视为“行程未开始” |
| | | return { |
| | | ...baseRule, |
| | | showPickup: baseRule.showPickup, // 行程中(车辆已过上车点)即使在修改目的地态也不展示上车点,沿用基础规则 |
| | | showPickup: isModifyDestinationReviewVisible ? true : baseRule.showPickup, // 修改目的地预览强制展示起点(上车点) |
| | | // 预览态强制展示下车点;确认修改后(本地覆盖态)回退至基础规则,避免接驾态错误渲染目的地,且不影响送驾态的正常显示 |
| | | showDropoff: isModifyDestinationReviewVisible ? true : baseRule.showDropoff, |
| | | showCar: !isTripBeforePickupArrived && baseRule.showCar, |
| | |
| | | || (tripStatus === TripStatus.VehicleApproaching |
| | | ? isPickupRouteChangeVisible |
| | | : ROUTE_SELECTABLE_TRIP_STATUSES.includes(tripStatus)); // 接驾态仅在展示路线切换卡片时展示多条候选路线 |
| | | const shouldShowRouteSummaryBubble = allowSelectableRoutes; // 路线概览气泡与多路线展示条件保持一致 |
| | | const showTrip1StyleEndpoint = isModifyDestinationReviewVisible || hasLocalDestinationOverride || isPickupRouteChangeMapMode; |
| | | const vehicleState = useVehicleTracking({ |
| | | statusRule: effectiveStatusRule, |
| | |
| | | resetRouteRuntimeState(); |
| | | }; |
| | | const displayPolyline = polyline; |
| | | const routeSummaryBaseLocation = useMemo(() => { |
| | | if (!shouldShowRouteSummaryBubble || !routeDistance || routeDuration === null || !Array.isArray(routePoints) || routePoints.length === 0) { |
| | | return null; |
| | | } |
| | | const midpoint = routePoints[Math.max(0, Math.floor(routePoints.length / 2))]; |
| | | if (!midpoint?.latitude || !midpoint?.longitude) { |
| | | return null; |
| | | } |
| | | return { |
| | | latitude: midpoint.latitude, |
| | | longitude: midpoint.longitude |
| | | }; |
| | | }, [routeDistance, routeDuration, routePoints, shouldShowRouteSummaryBubble]); |
| | | const { |
| | | routeSummaryAnchorLocation, |
| | | handleRegionChange: handleRouteSummaryRegionChange |
| | | } = useRouteSummaryAnchorLocation({ |
| | | enabled: shouldShowRouteSummaryBubble && !!routeDistance && routeDuration !== null, |
| | | routeSummaryLocation: routeSummaryBaseLocation, |
| | | routePoints |
| | | }); |
| | | const routeSummaryMarker = useMemo(() => { |
| | | const summaryLocation = routeSummaryAnchorLocation || routeSummaryBaseLocation; |
| | | if (!summaryLocation?.latitude || !summaryLocation?.longitude) { |
| | | return null; |
| | | } |
| | | return { |
| | | id: ROUTE_SUMMARY_MARKER_ID, |
| | | latitude: summaryLocation.latitude, |
| | | longitude: summaryLocation.longitude, |
| | | iconPath: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/marker-car-v2.png', |
| | | width: 1, |
| | | height: 1, |
| | | alpha: 0, |
| | | zIndex: 0, |
| | | isRouteSummaryBubble: true, |
| | | customCallout: { anchorY: 22, anchorX: 42, display: 'ALWAYS' } |
| | | }; |
| | | }, [routeSummaryAnchorLocation, routeSummaryBaseLocation]); |
| | | const viewportState = useMapViewport({ |
| | | tripStatus: mapPresentationTripStatus, |
| | | statusRule: effectiveStatusRule, |
| | |
| | | carCalloutVisible, |
| | | arriveCalloutStyle, |
| | | arriveCalloutVisible, |
| | | routeSummaryCalloutStyle, |
| | | routeSummaryCalloutVisible, |
| | | handleRegionChange: handleAndroidOverlayRegionChange |
| | | } = useAndroidOverlays({ |
| | | shouldShowWaitingRadar, |
| | |
| | | tripStatus: mapPresentationTripStatus, |
| | | mapScale, |
| | | isModifyDestinationReviewVisible, |
| | | routeSummaryLocation: routeSummaryAnchorLocation || routeSummaryBaseLocation, |
| | | startSite, |
| | | endSite: effectiveEndSite, |
| | | vehicleLocation, |
| | |
| | | longitude, |
| | | latitude |
| | | }); // Android 覆盖物状态编排 |
| | | const markersWithRouteSummary = useMemo(() => { |
| | | if (!routeSummaryMarker) return markers; |
| | | return [...markers, routeSummaryMarker]; |
| | | }, [markers, routeSummaryMarker]); |
| | | const hasValidBeforeUserEndSite = useMemo(() => ( |
| | | Number.isFinite(Number(beforeUserEndSite?.latitude)) |
| | | && Number.isFinite(Number(beforeUserEndSite?.longitude)) |
| | |
| | | return; |
| | | } |
| | | activeTripScale.handleRegionChange(e); |
| | | handleRouteSummaryRegionChange(e); |
| | | handleAndroidOverlayRegionChange(e); |
| | | |
| | | // 用户手势导致的缩放/拖拽结束后,把原生地图的实际 scale 同步回 React 状态, |
| | |
| | | isAndroid, |
| | | tripStatus, |
| | | mapScale, |
| | | markers: markersWithRouteSummary, |
| | | markers: markers, |
| | | isRouteSelectExpanded, |
| | | isPickupRouteChangeVisible, |
| | | isConfirmingPickupRouteChange, |
| | |
| | | carCalloutStyle, |
| | | arriveCalloutVisible, |
| | | arriveCalloutStyle, |
| | | routeSummaryCalloutVisible, |
| | | routeSummaryCalloutStyle, |
| | | arrivedTooltipReady, |
| | | routeDistance, |
| | | routeDuration, |
| | | hasBeforeUserEndSite: hasValidBeforeUserEndSite, |
| | | hasPreviousOrder, |
| | | customCalloutIconSrc, |
| | | routeSummaryBgSrc, |
| | | showTrip1StyleEndpoint, |
| | | startSite, |
| | | elapsedSeconds, |
| | |
| | | mapLongitude, |
| | | mapLatitude, |
| | | mapScale: mapScaleRef.current ?? mapScale, |
| | | markers: markersWithRouteSummary, |
| | | markers: markers, |
| | | polyline: displayPolyline, |
| | | showLocation: !!longitude && showLocationFlag, |
| | | handleCalloutTap, |