src/robotaxi/api/begin-route.ts
@@ -40,7 +40,8 @@ UnlockVehiclePayload, UnlockVehicleResponse, OrderPaymentResponse, QueryOrderPaymentResultResponse QueryOrderPaymentResultResponse, QueryLedgerCreditOrderResultResponse } from './types/begin-route'; // 导航-获取路线和费用 @@ -116,7 +117,8 @@ return request({ url: _ROBOTAXI_HOST + `/order/api/v1/mini-program/order/creditPay/createOrder`, method: 'POST', payload payload, showToast: false }, true); } @@ -322,6 +324,19 @@ }, true); } // 查询信用分授权结果 export const queryLedgerCreditOrderResult = async (orderId: OrderId): Promise<QueryLedgerCreditOrderResultResponse> => { return request({ url: _ROBOTAXI_HOST + `/order/api/v1/mini-program/order/queryLedgerCreditOrderResult`, method: 'POST', payload: { orderId: String(orderId) }, skipException: true, showToast: false }, true); } // 订单查询实时轨迹路线 export const queryOrderRoute = async ( payload: QueryOrderRoutePayload src/robotaxi/api/types/begin-route.ts
@@ -605,6 +605,12 @@ /** 订单支付结果查询返回值。 */ export type QueryOrderPaymentResultResponse = { status: RobotaxiOrderStatus | number; orderId: string; payStatus: number; payType: number; payTime: string; }; // 信用分授权结果查询响应 export type QueryLedgerCreditOrderResultResponse = { creditOrderStatus: 'DOING' | 'REVOKED' | 'EXPIRED' | string; }; src/robotaxi/pages/begin-route/components/calling-car-modules/fare-detail-card.tsx
@@ -25,6 +25,12 @@ }: Props) { const [showDetail, setShowDetail] = useState(false); // 费用明细展开状态 const canOpenCouponSelect = tripStatus === TripStatus.TripFinished && typeof onOpenCouponSelect === 'function'; // 仅待支付支持切换优惠券 const isCreditPayProcessing = ( tripStatus === TripStatus.TripFinished && !!orderId && String(Taro.getStorageSync('creditPayOrderId') || '') === String(orderId) ); // 信用下单结束后,展示免密支付中并阻止重复点击 const payButtonText = isCreditPayProcessing ? '免密支付中' : '立即支付'; // 外部重置版本变化时收起明细 useEffect(() => { @@ -210,7 +216,15 @@ : <View className="pay-box-free">员工内部测试</View> }{/* */} <View onClick={handlePay} className="pay-box-btn">立即支付</View> <View className="pay-box-btn-wrapper"> <View onClick={isCreditPayProcessing ? undefined : handlePay} className="pay-box-btn" > {payButtonText} </View> {isCreditPayProcessing && <View className="pay-box-btn-mask" />} </View> </View> } </View> src/robotaxi/pages/begin-route/components/css-folder/calling-car.less
@@ -708,6 +708,12 @@ } } &-btn-wrapper { width: 296px; height: 94px; position: relative; } &-btn { width: 296px; height: 94px; @@ -720,6 +726,16 @@ letter-spacing: normal; color: #FFFFFF; } &-btn-mask { position: absolute; top: 0; left: 0; right: 0; bottom: 0; border-radius: 20px; background: rgba(255, 255, 255, 0.4); } } .trip-action-bar-item-hover { src/robotaxi/pages/begin-route/components/css-folder/wechat-pay-score-loading.less
New file @@ -0,0 +1,70 @@ .wechat-pay-score-loading-mask { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; z-index: 999999; display: flex; align-items: center; justify-content: center; /* 背景全透明,只有中间的盒子有颜色 */ background: transparent; } .wechat-pay-score-loading-box { width: 260px; height: 260px; background-color: rgba(50, 50, 50, 0.95); border-radius: 16px; display: flex; flex-direction: column; align-items: center; justify-content: center; color: #fff; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); } .wechat-pay-score-icon { width: 88px; height: 88px; margin-bottom: 24px; } .wechat-pay-score-text { font-size: 30px; font-weight: 400; margin-bottom: 32px; line-height: 1; } .wechat-pay-score-dots { display: flex; gap: 12px; } .wechat-pay-score-dot { width: 12px; height: 12px; background-color: #fff; border-radius: 50%; opacity: 0.3; animation: wechat-pay-score-dot-blink 1.4s infinite ease-in-out both; } .wechat-pay-score-dot:nth-child(1) { animation-delay: -0.32s; } .wechat-pay-score-dot:nth-child(2) { animation-delay: -0.16s; } @keyframes wechat-pay-score-dot-blink { 0%, 80%, 100% { opacity: 0.3; } 40% { opacity: 1; } } src/robotaxi/pages/begin-route/components/release-be-being.tsx
@@ -1,16 +1,19 @@ import Taro from "@tarojs/taro"; import { useEffect, useState } from 'react'; import { useEffect, useRef, useState } from 'react'; import { Image, View } from "@tarojs/components"; import './css-folder/release-be-being.less' type Props = { handleOpenCancelPopup?: () => void; onFinish?: () => void; finishBlocked?: boolean; } export default function ReleaseBeBeing({ handleOpenCancelPopup, onFinish }: Props) { export default function ReleaseBeBeing({ handleOpenCancelPopup, onFinish, finishBlocked = false }: Props) { const [progress, setProgress] = useState(0); // 发布进度条 const [completedCount, setCompletedCount] = useState(0); // 已完成步骤数 const [finishReady, setFinishReady] = useState(false); // 发布动画是否已走完 const finishTriggeredRef = useRef(false); // 防止 onFinish 重复触发 const startSite = Taro.getStorageSync('startSite') || {}; const endSite = Taro.getStorageSync('endSite') || {}; const startSiteName = startSite?.name || startSite?.address || '-'; @@ -27,9 +30,9 @@ const step1 = setTimeout(() => setCompletedCount(1), 1000); const step2 = setTimeout(() => setCompletedCount(2), 2000); // 3秒后进入呼叫车中状态 // 3 秒后标记动画完成;是否真正切页由 finishBlocked 控制 const step3 = setTimeout(() => { onFinish?.(); setFinishReady(true); }, 3000); return () => { @@ -40,6 +43,15 @@ }; }, []); useEffect(() => { // 信用分下单时,需要等待用户从支付分确认页返回后再推进到“呼叫车中”。 if (!finishReady || finishBlocked || finishTriggeredRef.current) { return; } finishTriggeredRef.current = true; onFinish?.(); }, [finishBlocked, finishReady, onFinish]); return ( <View className='release-be-being'> <View className="top-box"> src/robotaxi/pages/begin-route/components/wechat-pay-score-loading.tsx
New file @@ -0,0 +1,30 @@ import { View, Image } from '@tarojs/components'; import React from 'react'; import './css-folder/wechat-pay-score-loading.less'; // 模拟微信支付分的图标 SVG Base64 const PAY_SCORE_ICON_BASE64 = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDQiIGhlaWdodD0iNDQiIHZpZXdCb3g9IjAgMCA0NCA0NCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8cGF0aCBkPSJNMjIgM0wyNiA3SDMyVjEzTDM2IDE3TDI2IDIxVjI3SDI2TDIyIDMxTDE4IDI3SDEyVjIxTDggMTdMMTIgMTNWN0gxOEwyMiAzWiIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSIyLjUiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KICA8Y2lyY2xlIGN4PSIyMiIgY3k9IjE0IiByPSI0IiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIuNSIvPgogIDxwYXRoIGQ9Ik0xNSAyNUMxNSAyMS4xMzQgMTguMTM0IDE4IDIyIDE4QzI1Ljg2NiAxOCAyOSAyMS4xMzQgMjkgMjUiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iMi41IiBzdHJva2UtbGluZWNhcD0icm91bmQiLz4KICA8cGF0aCBkPSJNMjQgMjdMMjggMzFMMzggMjEiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iMyIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+Cjwvc3ZnPg=='; type Props = { visible: boolean; }; const WechatPayScoreLoading: React.FC<Props> = ({ visible }) => { if (!visible) return null; return ( <View className="wechat-pay-score-loading-mask"> <View className="wechat-pay-score-loading-box"> <Image className="wechat-pay-score-icon" src={PAY_SCORE_ICON_BASE64} mode="aspectFit" /> <View className="wechat-pay-score-text">微信支付分</View> <View className="wechat-pay-score-dots"> <View className="wechat-pay-score-dot" /> <View className="wechat-pay-score-dot" /> <View className="wechat-pay-score-dot" /> </View> </View> </View> ); }; export default React.memo(WechatPayScoreLoading); src/robotaxi/pages/begin-route/hooks/use-credit-pay-return.ts
@@ -1,9 +1,16 @@ import { useCallback, useEffect, useRef } from 'react'; import Taro from '@tarojs/taro'; import { queryLedgerCreditOrderResult } from '../../../api/begin-route'; type Deps = { /** 支付分页面返回后的回调。 */ onReturn: () => void; /** 当前正在下单的订单 ID,用于查询授权结果。 */ orderId?: string; /** 支付分授权成功后的回调。 */ onAuthorizedReturn: (queryId: string) => void | Promise<void>; /** 支付分未授权后的回调。 */ onUnauthorizedReturn: (orderId?: string) => void | Promise<void>; /** 支付分页面拉起失败后的回调。 */ onLaunchFailed: (orderId?: string) => void | Promise<void>; }; const PAY_SCORE_REFERRER_APP_ID = 'wxd8f3793ea3b935b8'; @@ -28,22 +35,25 @@ /** * 拉起微信支付分订单确认页。 */ const openCreditPayScoreUse = useCallback((packageValue: string) => { const openCreditPayScoreUse = useCallback((packageValue: string, targetOrderId?: string) => { const queryOrderId = targetOrderId || deps.orderId; if (!packageValue) { Taro.showToast({ title: '支付参数缺失,请稍后重试', icon: 'none' }); return false; void deps.onLaunchFailed(queryOrderId); return true; } // @ts-ignore if (!wx.openBusinessView) { Taro.showToast({ title: '请升级微信版本后重试', icon: 'none' }); return false; void deps.onLaunchFailed(queryOrderId); return true; } clearAppShowListener(); /** 支付分页面返回监听函数。 */ const handleAppShow = (res) => { const handleAppShow = async (res) => { if (res?.scene !== 1038) { return; } @@ -53,8 +63,88 @@ return; } const queryId = String(res?.referrerInfo?.extraData?.query_id || '').trim(); console.log('[信用分授权返回][AppShow]', { scene: res?.scene, referrerInfo: res?.referrerInfo, queryId }); clearAppShowListener(); deps.onReturn(); if (!queryId) { void deps.onUnauthorizedReturn(queryOrderId); return; } // 根据 query_id 轮询查询真实的授权结果 if (!queryOrderId) { console.log('[信用分授权查询失败] 缺失 orderId'); void deps.onUnauthorizedReturn(queryOrderId); return; } Taro.showLoading({ title: '授权结果查询中', mask: true }); const maxPollingCount = 120; let currentPollingCount = 0; const pollStatus = async () => { try { const result = await queryLedgerCreditOrderResult(queryOrderId); const status = result?.data?.creditOrderStatus; console.log(`[信用分授权查询结果] 第${currentPollingCount + 1}次`, result); if (status === 'REVOKED' || status === 'EXPIRED') { Taro.hideLoading(); void deps.onUnauthorizedReturn(queryOrderId); return; } if (status === 'DOING' || result?.code === 20710) { Taro.hideLoading(); void deps.onAuthorizedReturn(queryId); return; } if (status === 'CREATED') { // CREATED 状态也当作中间状态继续轮询 currentPollingCount++; if (currentPollingCount >= maxPollingCount) { Taro.hideLoading(); console.log('[信用分授权查询超时] CREATED 状态持续2分钟'); void deps.onUnauthorizedReturn(queryOrderId); return; } setTimeout(pollStatus, 1000); return; } // 其他状态,继续轮询 currentPollingCount++; if (currentPollingCount >= maxPollingCount) { Taro.hideLoading(); console.log('[信用分授权查询超时]'); void deps.onUnauthorizedReturn(queryOrderId); return; } setTimeout(pollStatus, 1000); } catch (error: any) { const errorCode = String(error?.code || error?.data?.code || ''); if (errorCode === '20710') { console.log(`[信用分授权查询遇到20710] 第${currentPollingCount + 1}次,当作授权成功处理`); Taro.hideLoading(); void deps.onAuthorizedReturn(queryId); return; } Taro.hideLoading(); console.log('[信用分授权查询失败]', error); void deps.onUnauthorizedReturn(queryOrderId); } }; await pollStatus(); }; appShowListenerRef.current = handleAppShow; @@ -66,14 +156,19 @@ extraData: { package: packageValue }, success: (res) => { console.log('[信用分授权拉起][openBusinessView]', res); }, fail: () => { clearAppShowListener(); console.log('[信用分授权拉起失败][openBusinessView]'); Taro.showToast({ title: '拉起支付分订单失败', icon: 'none' }); void deps.onLaunchFailed(queryOrderId); } }); return true; }, [clearAppShowListener, deps.onReturn]); }, [clearAppShowListener, deps.onAuthorizedReturn, deps.onLaunchFailed, deps.onUnauthorizedReturn, deps.orderId]); useEffect(() => clearAppShowListener, [clearAppShowListener]); src/robotaxi/pages/begin-route/hooks/use-order-actions.shared.ts
@@ -126,7 +126,9 @@ /** 未签约态支付处理。 */ handleUnsignedPayment: (token: string) => void | Promise<void>; /** 已签约态支付处理。 */ handleSignedCreditPayment: (value: SignedOrderPaymentValue) => void | Promise<void>; handleSignedCreditPayment: (value: SignedOrderPaymentValue, targetOrderId?: string) => boolean | Promise<boolean>; /** 结束行程后进入待支付态时,触发信用扣款结果查询。 */ handleCreditPayTripFinished?: (targetOrderId: string) => void | Promise<void>; /** 同步上车点。 */ setStartSite?: (site: any) => void; }; src/robotaxi/pages/begin-route/hooks/use-order-cancel.ts
@@ -50,6 +50,7 @@ deps.setVehicleInfo({}); clearOrderTripStorage(); Taro.removeStorageSync('pickupEndTime'); Taro.removeStorageSync('creditPayOrderId'); Taro.removeStorageSync(WAITING_TIMER_START_STORAGE_KEY); Taro.removeStorageSync(WAITING_TIMER_ELAPSED_STORAGE_KEY); }; src/robotaxi/pages/begin-route/hooks/use-order-payment.ts
@@ -92,7 +92,10 @@ } else { syncTripFinishedStatus(); } return orderDetail; return { orderDetail, nextTripStatus }; }; /** @@ -145,7 +148,11 @@ try { const endOrderResult: any = await endOrder(targetOrderId); const orderDetail = await syncPostEndOrderTripStatus(targetOrderId); const { orderDetail, nextTripStatus } = await syncPostEndOrderTripStatus(targetOrderId); if (nextTripStatus === TripStatus.TripFinished) { // 结束行程后若仍待支付,立即触发信用扣款结果查询,避免依赖页面 effect 才能进入。 await deps.handleCreditPayTripFinished?.(targetOrderId); } reportTripPayStartEvent(orderDetail); if (endOrderResult) { Taro.showToast({ @@ -171,7 +178,11 @@ await endOrder(targetOrderId); } catch (error) { } await syncPostEndOrderTripStatus(targetOrderId); const { nextTripStatus } = await syncPostEndOrderTripStatus(targetOrderId); if (nextTripStatus === TripStatus.TripFinished) { // 倒计时自动结束行程时也复用同一套信用扣款结果查询逻辑。 await deps.handleCreditPayTripFinished?.(targetOrderId); } }; /** @@ -192,35 +203,34 @@ mask: false }); try { await getOrderPayment({ orderId: targetOrderId }); syncPaymentSuccessStatus(); // await getOrderPayment({ orderId: targetOrderId }); // syncPaymentSuccessStatus(); // const paymentResult = await getOrderPayment({ orderId: targetOrderId }); // const paymentStatus = Number(paymentResult?.status); const paymentResult = await getOrderPayment({ orderId: targetOrderId }); const paymentStatus = Number(paymentResult?.status); // if (paymentStatus === RobotaxiPaymentEntryStatus.Unsigned) { // const cashierToken = paymentResult?.value?.token; // if (!cashierToken) { // Taro.showToast({ title: '支付凭证缺失,请稍后重试', icon: 'none' }); // return; // } // await deps.handleUnsignedPayment(cashierToken); // return; // } if (paymentStatus === RobotaxiPaymentEntryStatus.Unsigned) { const cashierToken = paymentResult?.value?.token; if (!cashierToken) { Taro.showToast({ title: '支付凭证缺失,请稍后重试', icon: 'none' }); return; } await deps.handleUnsignedPayment(cashierToken); return; } // if (paymentStatus === RobotaxiPaymentEntryStatus.Signed) { // const signedPaymentValue = paymentResult?.value as SignedOrderPaymentValue; // if (!signedPaymentValue?.packageValue) { // Taro.showToast({ title: '支付参数缺失,请稍后重试', icon: 'none' }); // return; // } // await deps.handleSignedCreditPayment(signedPaymentValue); // return; // } if (paymentStatus === RobotaxiPaymentEntryStatus.Signed) { const signedPaymentValue = paymentResult?.value as SignedOrderPaymentValue; if (!signedPaymentValue?.packageValue) { Taro.showToast({ title: '支付参数缺失,请稍后重试', icon: 'none' }); return; } await deps.handleSignedCreditPayment(signedPaymentValue); return; } // Taro.showToast({ title: '暂不支持当前支付方式', icon: 'none' }); Taro.showToast({ title: '暂不支持当前支付方式', icon: 'none' }); } catch (error) { Taro.showToast({ title: '支付失败,请重试', icon: 'none' }); } finally { src/robotaxi/pages/begin-route/hooks/use-order-submit.ts
@@ -51,6 +51,9 @@ callImmediately: () => void; }; /** 当前路线是否为载客测试免费。 */ const isOperateFreeRoute = (selectedRoute: any) => Number(selectedRoute?.operateFree) === 1; /** 标准化文本,兼容普通空格与不可见空格差异。 */ const normalizeText = (value: any) => String(value || '').replace(/\s+/g, ' ').trim().toLowerCase(); @@ -465,9 +468,13 @@ /** * 下单前校验免密状态。 */ const checkCreditPayReadyBeforeOrder = useCallback(async () => { const checkCreditPayReadyBeforeOrder = useCallback(async (selectedRoute: any) => { if (isOperateFreeRoute(selectedRoute)) { // 载客测试免费单直接走普通下单,不拉起微信信用下单弹窗。 return 'normal_submit' as CreditPayCheckResult; } // 免密状态校验,先默认返回正常下单,后续根据校验结果再处理。 return 'normal_submit' as CreditPayCheckResult; // return 'normal_submit' as CreditPayCheckResult; try { const hasEnabledCreditPay = await getPayPwd(); if (hasEnabledCreditPay === true) { @@ -565,7 +572,7 @@ let submitMode: CreditPayCheckResult = 'normal_submit'; if (!options.skipCreditPayCheck) { submitMode = await checkCreditPayReadyBeforeOrder(); submitMode = await checkCreditPayReadyBeforeOrder(selectedRoute); if (submitMode === 'blocked') { return; } @@ -575,34 +582,60 @@ await waitForOrderSuccessSubscribePopup(); try { if (submitMode === 'credit_pay_submit') { Taro.showLoading({ title: '加载中', mask: true }); } const submitResult = submitMode === 'credit_pay_submit' ? await submitCreditPayOrder(startSite, openCityId, selectedRoute) : await submitOrder(startSite, openCityId); if (!submitResult?.orderId) { Taro.hideLoading(); stopCallSubmitting(); return; } deps.setOrderId(submitResult.orderId); deps.setTripStatus(TripStatus.Publishing); deps.bindSocketListeners(); void subscribeAfterOrderSuccess(submitResult.orderId); if (submitMode !== 'credit_pay_submit') { Taro.removeStorageSync('creditPayOrderId'); } if (submitMode === 'credit_pay_submit') { Taro.setStorageSync('creditPayOrderId', submitResult.orderId); const creditPayPackageValue = String(submitResult.creditPayPackageValue || '').trim(); if (!creditPayPackageValue) { Taro.hideLoading(); Taro.showToast({ title: '支付参数缺失,请稍后重试', icon: 'none' }); return; } // 复用现有支付分确认页拉起与回跳查询链路。 void deps.handleSignedCreditPayment({ const launched = await deps.handleSignedCreditPayment({ businessOrderId: '', authOrderId: '', channelOrderId: '', payType: 13, packageValue: creditPayPackageValue }); }, submitResult.orderId); Taro.hideLoading(); // 只有拉起失败才立刻进入发单状态(或者依靠 abort 逻辑终止) if (!launched) { deps.setTripStatus(TripStatus.Publishing); } return; } deps.setTripStatus(TripStatus.Publishing); } catch (error) { reportPayResultEvent(resolvePayResultStatus((error as any)?.msg || (error as any)?.message || error)); Taro.hideLoading(); const errorMessage = String((error as any)?.msg || (error as any)?.message || '下单失败,请重试'); if (submitMode === 'credit_pay_submit') { Taro.showToast({ title: errorMessage, icon: 'none' }); } reportPayResultEvent(resolvePayResultStatus(errorMessage)); stopCallSubmitting(); } }; src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx
@@ -61,6 +61,8 @@ endSite: any; /** 发单 */ callImmediately: () => void; /** 是否正在等待信用分确认页返回结果 */ isWaitingSignedCreditPayResult?: boolean; /** 预估态选择上下车点 */ handleEstimateSelectSite: (pageType: "start" | "end") => void; /** 费用信息 */ @@ -84,6 +86,8 @@ handlePickupTimeoutAutoCancel: () => void; /** 行程状态设置 */ setTripStatus: (value: TripStatus) => void; /** 发布中动画结束 */ handlePublishingAnimationFinish?: () => void; /** 完成倒计时启用 */ finishCountdownEnabled: boolean; /** 订单 ID */ @@ -695,7 +699,12 @@ handleOpenCancelPopup={() => deps.handleOpenCancelPopupByTripStatus(TripStatus.Publishing) } finishBlocked={Boolean(deps.isWaitingSignedCreditPayResult)} onFinish={() => { if (deps.handlePublishingAnimationFinish) { deps.handlePublishingAnimationFinish(); return; } deps.setTripStatus(TripStatus.CallingCar); Taro.setStorageSync("tripStatus", TripStatus.CallingCar); }} src/robotaxi/pages/begin-route/hooks/use-web-socket-events.ts
@@ -17,6 +17,10 @@ export type UseWebSocketEventsDeps = { tripStatusRef: React.MutableRefObject<TripStatus>; isWaitingSignedCreditPayResultRef?: React.MutableRefObject<boolean>; pendingSignedCreditPayTripStatusRef?: React.MutableRefObject<TripStatus | null>; isHoldingTripStatusDuringPublishingAnimationRef?: React.MutableRefObject<boolean>; pendingTripStatusAfterPublishingAnimationRef?: React.MutableRefObject<TripStatus | null>; pickupArrivedReportedOrderIdRef: React.MutableRefObject<string>; tripInProgressReportedOrderIdRef: React.MutableRefObject<string>; arrivedDestinationReportedOrderIdRef: React.MutableRefObject<string>; src/robotaxi/pages/begin-route/hooks/web-socket-handlers/create-order-status-socket-handler.ts
@@ -96,6 +96,14 @@ if (currentTripStatus === mappedTripStatus) { return mappedTripStatus; } if (deps.isWaitingSignedCreditPayResultRef?.current) { deps.pendingSignedCreditPayTripStatusRef && (deps.pendingSignedCreditPayTripStatusRef.current = mappedTripStatus); return currentTripStatus; } if (deps.isHoldingTripStatusDuringPublishingAnimationRef?.current && currentTripStatus === TripStatus.Publishing) { deps.pendingTripStatusAfterPublishingAnimationRef && (deps.pendingTripStatusAfterPublishingAnimationRef.current = mappedTripStatus); return currentTripStatus; } deps.setTripStatus(mappedTripStatus); deps.tripStatusRef.current = mappedTripStatus; Taro.setStorageSync('tripStatus', mappedTripStatus); src/robotaxi/pages/begin-route/index.tsx
@@ -60,11 +60,12 @@ 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 { selectNavigationRoute } from '../../api/begin-route'; import { cancelOrder, selectNavigationRoute } 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 { getOrderId } from '../../utils/order'; import { getOrderDetail } from '../../api/order'; import { _ROBOTAXI_WS_HOST } from '../../../constants/api'; import { RobotaxiSignedPayType, SignedOrderPaymentValue } from '../../api/types/begin-route'; @@ -305,6 +306,7 @@ const [pageDidShowVersion, setPageDidShowVersion] = useState(0); // 页面展示版本号,用于触发子模块重查 const [resetDetailVersion, setResetDetailVersion] = useState(0); // 重置详情版本号,用于触发组件更新 const [priceUpdateVersion, setPriceUpdateVersion] = useState(0); // 价格更新版本号 const [, setCreditPayFallbackRefreshVersion] = useState(0); // 信用扣款回落普通支付后,触发页面重渲染 const [evaluateSubmitVersion, setEvaluateSubmitVersion] = useState(0); // 评价提交版本号 const [evaluateCloseVersion, setEvaluateCloseVersion] = useState(0); // 评价关闭版本号 const [evaluateOrderRating, setEvaluateOrderRating] = useState(1); // 当前评价弹窗对应评分 @@ -315,7 +317,16 @@ const [pickupExceptionInfo, setPickupExceptionInfo] = useState<PickupExceptionInfo | null>(null); // 接驾异常信息 const [showPickupExceptionPopup, setShowPickupExceptionPopup] = useState(false); // 接驾异常弹窗展示状态 const [showCreditPayPopup, setShowCreditPayPopup] = useState(false); // 免密支付开通弹窗 const [isWaitingSignedCreditPayResult, setIsWaitingSignedCreditPayResult] = useState(false); // 信用分确认页已拉起,等待用户返回结果 const creditPayResolverRef = useRef<((value: boolean) => void) | null>(null); // 免密弹窗的 Promise resolve 回调 const isWaitingSignedCreditPayResultRef = useRef(false); // 信用分确认结果等待态引用,供 socket 状态同步拦截使用 const pendingSignedCreditPayTripStatusRef = useRef<TripStatus | null>(null); // 等待信用分结果期间暂存的最新行程状态 const pendingPublishingAfterSignedCreditPayRef = useRef(false); // 信用分下单成功后,等待结果返回再进入发布态 const isHoldingTripStatusDuringPublishingAnimationRef = useRef(false); // 信用分返回后,发布中 3 秒动画期间暂存 socket 状态 const pendingTripStatusAfterPublishingAnimationRef = useRef<TripStatus | null>(null); // 发布中动画结束后待恢复的最新状态 const clearActiveOrderRuntimeRef = useRef<(() => void) | null>(null); // 信用分取消发单后复用订单清理逻辑 const restartPlanningRouteRef = useRef<(() => void) | null>(null); // 信用分取消发单后复用发单前页面恢复逻辑 const stopCallSubmittingRef = useRef<(() => void) | null>(null); // 信用分中止发单后复位“立即呼叫”提交态 const [orderRouteMetrics, setOrderRouteMetrics] = useState<OrderRouteMetrics>({ distanceMeters: null, durationSeconds: null @@ -1357,6 +1368,52 @@ }, []); /** * 统一维护“等待信用分结果”状态,同时同步 ref,供异步回调和 socket 读取。 */ const syncWaitingSignedCreditPayResult = useCallback((value: boolean) => { isWaitingSignedCreditPayResultRef.current = value; setIsWaitingSignedCreditPayResult(value); }, []); /** * 信用分结果返回后,恢复页面状态推进。 */ const resumeTripStatusAfterSignedCreditPayReturn = useCallback(() => { syncWaitingSignedCreditPayResult(false); const deferredTripStatus = pendingSignedCreditPayTripStatusRef.current; pendingSignedCreditPayTripStatusRef.current = null; if (pendingPublishingAfterSignedCreditPayRef.current) { pendingPublishingAfterSignedCreditPayRef.current = false; pendingTripStatusAfterPublishingAnimationRef.current = deferredTripStatus; isHoldingTripStatusDuringPublishingAnimationRef.current = true; setTripStatus(TripStatus.Publishing); tripStatusRef.current = TripStatus.Publishing; Taro.setStorageSync('tripStatus', TripStatus.Publishing); return; } if (deferredTripStatus !== null) { setTripStatus(deferredTripStatus); tripStatusRef.current = deferredTripStatus; Taro.setStorageSync('tripStatus', deferredTripStatus); } }, [syncWaitingSignedCreditPayResult]); /** * 发布中 3 秒动画结束后,再恢复信用分结果等待期间暂存的最新状态。 */ const handlePublishingAnimationFinish = useCallback(() => { const deferredTripStatus = pendingTripStatusAfterPublishingAnimationRef.current; pendingTripStatusAfterPublishingAnimationRef.current = null; isHoldingTripStatusDuringPublishingAnimationRef.current = false; const nextTripStatus = deferredTripStatus ?? TripStatus.CallingCar; setTripStatus(nextTripStatus); tripStatusRef.current = nextTripStatus; Taro.setStorageSync('tripStatus', nextTripStatus); }, []); /** * 确认开通免密支付。 */ const handleConfirmCreditPayPopup = useCallback(() => { @@ -1390,6 +1447,10 @@ const { bindSocketListeners, ensureSocketListeners } = useWebSocketEvents({ tripStatusRef, isWaitingSignedCreditPayResultRef, pendingSignedCreditPayTripStatusRef, isHoldingTripStatusDuringPublishingAnimationRef, pendingTripStatusAfterPublishingAnimationRef, pickupArrivedReportedOrderIdRef, tripInProgressReportedOrderIdRef, arrivedDestinationReportedOrderIdRef, @@ -1491,9 +1552,155 @@ onPaySuccess: handlePaymentSuccess }); // 信用分结束行程 loading 状态 const creditPayLoadingTimerRef = useRef<any>(null); const creditPayFallbackCheckTimerRef = useRef<any>(null); const creditPayFallbackActiveOrderIdRef = useRef(''); const clearCreditPayFallbackRuntime = useCallback((hideLoading = true) => { if (creditPayLoadingTimerRef.current) { clearTimeout(creditPayLoadingTimerRef.current); creditPayLoadingTimerRef.current = null; } if (creditPayFallbackCheckTimerRef.current) { clearInterval(creditPayFallbackCheckTimerRef.current); creditPayFallbackCheckTimerRef.current = null; } creditPayFallbackActiveOrderIdRef.current = ''; if (hideLoading) { Taro.hideLoading(); } }, []); const handleCreditPayTripFinished = useCallback(async (targetOrderId: string) => { const resolvedOrderId = String(targetOrderId || '').trim(); const creditPayOrderId = String(Taro.getStorageSync('creditPayOrderId') || '').trim(); if (!resolvedOrderId || !creditPayOrderId || creditPayOrderId !== resolvedOrderId) { return; } if ( creditPayFallbackActiveOrderIdRef.current === resolvedOrderId && (creditPayLoadingTimerRef.current || creditPayFallbackCheckTimerRef.current) ) { return; } clearCreditPayFallbackRuntime(false); creditPayFallbackActiveOrderIdRef.current = resolvedOrderId; Taro.showLoading({ title: '扣款结果查询中,请勿重复支付', mask: true }); const checkCreditPayFallback = async () => { try { const orderDetail: any = await getOrderDetail(resolvedOrderId); if (Number(orderDetail?.payType) === 1) { Taro.removeStorageSync('creditPayOrderId'); clearCreditPayFallbackRuntime(); setCreditPayFallbackRefreshVersion((prev) => prev + 1); Taro.showToast({ title: '免密扣款失败,请手动支付', icon: 'none' }); } } catch (error) { } }; // 先立即查一次,再轮询订单详情感知 payType 是否从信用支付回落到普通支付。 await checkCreditPayFallback(); if (creditPayFallbackActiveOrderIdRef.current !== resolvedOrderId) { return; } creditPayFallbackCheckTimerRef.current = setInterval(() => { void checkCreditPayFallback(); }, 1000); creditPayLoadingTimerRef.current = setTimeout(() => { clearCreditPayFallbackRuntime(); Taro.redirectTo({ url: '/pages/robotaxi/home/index', }); }, 10000); }, [clearCreditPayFallbackRuntime]); useEffect(() => { if (tripStatus === TripStatus.TripFinished) { void handleCreditPayTripFinished(orderId); return () => { clearCreditPayFallbackRuntime(); }; } else { clearCreditPayFallbackRuntime(); } return () => { clearCreditPayFallbackRuntime(); }; }, [tripStatus, orderId, handleCreditPayTripFinished, clearCreditPayFallbackRuntime]); /** * 信用分流程未完成时,统一取消订单并终止发单。 */ const abortSignedCreditPayFlow = useCallback(async (reason: 'unauthorized' | 'launch_failed', fallbackOrderId?: string) => { const targetOrderId = getOrderId(orderId) || fallbackOrderId; console.log('[信用分流程中止]', { reason, orderId: targetOrderId }); stopCallSubmittingRef.current?.(); syncWaitingSignedCreditPayResult(false); pendingPublishingAfterSignedCreditPayRef.current = false; pendingSignedCreditPayTripStatusRef.current = null; pendingTripStatusAfterPublishingAnimationRef.current = null; isHoldingTripStatusDuringPublishingAnimationRef.current = false; if (!targetOrderId) { clearActiveOrderRuntimeRef.current?.(); restartPlanningRouteRef.current?.(); return; } try { await cancelOrder(targetOrderId); // Taro.showToast({ // title: reason === 'launch_failed' ? '拉起信用分失败,已取消发单' : '未完成信用分授权,已取消发单', // icon: 'none' // }); clearActiveOrderRuntimeRef.current?.(); if (restartPlanningRouteRef.current) { restartPlanningRouteRef.current(); return; } setTripStatus(TripStatus.PublishRoute); tripStatusRef.current = TripStatus.PublishRoute; Taro.setStorageSync('tripStatus', TripStatus.PublishRoute); } catch (error) { console.log('[信用分流程中止][取消订单失败]', { reason, error }); Taro.showToast({ title: '取消订单失败,请重试', icon: 'none' }); } }, [orderId, syncWaitingSignedCreditPayResult]); /** * 信用分页面返回但未拿到 query_id,视为未授权。 */ const handleSignedCreditPayUnauthorizedReturn = useCallback((targetOrderId?: string) => { return abortSignedCreditPayFlow('unauthorized', targetOrderId); }, [abortSignedCreditPayFlow]); /** * 信用分页面拉起失败,直接终止本次发单。 */ const handleSignedCreditPayLaunchFailed = useCallback((targetOrderId?: string) => { return abortSignedCreditPayFlow('launch_failed', targetOrderId); }, [abortSignedCreditPayFlow]); const { openCreditPayScoreUse } = useCreditPayReturn({ // 信用下单链路回跳后仅关闭监听,不再主动查询支付结果。 onReturn: () => {} orderId, // 信用下单链路回跳后,有 query_id 且结果为 DOING 或已确认时,继续正常发单。 onAuthorizedReturn: () => resumeTripStatusAfterSignedCreditPayReturn(), // 授权结果为失败/撤销等,立即取消订单并终止发单。 onUnauthorizedReturn: handleSignedCreditPayUnauthorizedReturn, // 拉起失败同样取消订单并终止发单。 onLaunchFailed: handleSignedCreditPayLaunchFailed }); useCashierPayListener({ @@ -1513,14 +1720,30 @@ /** * 处理已签约态支付分支付。 */ const handleSignedCreditPayment = useCallback((value: SignedOrderPaymentValue) => { const handleSignedCreditPayment = useCallback((value: SignedOrderPaymentValue, targetOrderId?: string) => { if (Number(value?.payType) !== RobotaxiSignedPayType.WechatCredit) { Taro.showToast({ title: '暂不支持当前支付方式', icon: 'none' }); return; return false; } openCreditPayScoreUse(value.packageValue); }, [openCreditPayScoreUse]); pendingPublishingAfterSignedCreditPayRef.current = true; pendingSignedCreditPayTripStatusRef.current = null; syncWaitingSignedCreditPayResult(true); const launched = openCreditPayScoreUse(value.packageValue, targetOrderId); if (!launched) { pendingPublishingAfterSignedCreditPayRef.current = false; syncWaitingSignedCreditPayResult(false); } return launched; }, [openCreditPayScoreUse, syncWaitingSignedCreditPayResult]); useEffect(() => { if (tripStatus !== TripStatus.Publishing) { syncWaitingSignedCreditPayResult(false); isHoldingTripStatusDuringPublishingAnimationRef.current = false; pendingTripStatusAfterPublishingAnimationRef.current = null; } }, [tripStatus, syncWaitingSignedCreditPayResult]); const orderActions = useOrderActions({ tripStatus, @@ -1559,6 +1782,7 @@ routeRequestRouteAndFare, handleUnsignedPayment, handleSignedCreditPayment, handleCreditPayTripFinished, setStartSite }); @@ -1570,8 +1794,14 @@ handlePickupTimeoutAutoCancel, handleConfirmOverRoute, handleFinishCountdownTimeout, handlePay handlePay, stopCallSubmitting, clearActiveOrderRuntime, restartPlanningRoute } = orderActions; stopCallSubmittingRef.current = stopCallSubmitting; clearActiveOrderRuntimeRef.current = clearActiveOrderRuntime; restartPlanningRouteRef.current = restartPlanningRoute; const { authSuccess: bluetoothAuthSuccess, startAuth: startBluetoothAuth, @@ -1623,6 +1853,14 @@ useDidShow(() => { setPageDidShowVersion((v) => v + 1); setDoorUnlockedInCurrentPage(false); // 信用分结果页返回后,需要先保留“发布中”动画,不要立刻走页面恢复链路把状态顶到最新。 if ( isWaitingSignedCreditPayResultRef.current || isHoldingTripStatusDuringPublishingAnimationRef.current ) { updateMapViewportPadding(); return; } handleDidShow(router.params?.skipPageFlag, router.params?.fromModifyDestination); updateMapViewportPadding(); // 完成态下隐藏小程序再恢复时,受控中心点可能与原生地图实际中心脱钩,主动触发一次 @@ -1759,7 +1997,7 @@ // (causedBy='update') 的 end 不应清除标记,否则会在用户仍在操作时提前恢复补帧导致弹回。 userGestureActiveRef.current = false; } if(eventType === 'begin' && isUserGesture) { notifyUserMapGesture(); @@ -1784,7 +2022,7 @@ recordLongitude(lng); } } // useSpeedBasedScale.performFit 调用 includePoints 后会置 pendingFitSyncRef=true, // globalFitSyncRef.current = true 表示全局其他地方(如 PublishRoute 的 includePoints) // 等本次 fit 动画真正结束(type==='end')时直接从事件 detail 读取实际 scale/center @@ -2134,6 +2372,7 @@ startSite, endSite: effectiveEndSite, callImmediately: handleCallImmediately, isWaitingSignedCreditPayResult, handleEstimateSelectSite, fareEstimate, handleOpenCouponSelect, @@ -2144,6 +2383,7 @@ handleOpenCancelPopupWithType, handlePickupTimeoutAutoCancel, setTripStatus, handlePublishingAnimationFinish, finishCountdownEnabled, orderId, vehicleInfo, src/robotaxi/pages/order/index.tsx
@@ -225,6 +225,15 @@ if (status == 10) { return "已完成"; } if (status == 11) { return "退款中"; } if (status == 12) { return "已退款"; } if (status == 13) { return "退款失败"; } }; const getOrderStatusClassName = (status: number) => { @@ -302,7 +311,7 @@ </View> )} {/* 发票功能暂未上线,屏蔽入口 */} {false && ["已完成", "待评价"].includes( { ["已完成", "待评价"].includes( formatOrderStatus(item.status) ) && item.invoiceStatus != 2 && @@ -321,7 +330,7 @@ </View> </View> )} {["已完成", "已取消", "待评价"].includes( {["已完成", "已取消", "待评价", "退款中", "已退款", "退款失败"].includes( formatOrderStatus(item.status) ) && canAgainOrder(item) && ( src/robotaxi/pages/personal-center/index.tsx
@@ -33,8 +33,8 @@ const toolMenuList: ToolMenuItem[] = [ { key: "service", label: "联系客服" }, // { key: "invoice", label: "开发票" }, // 暂未上线,屏蔽入口 { key: "payment", label: "支付设置" }, { key: "invoice", label: "开发票" }, // 暂未上线,屏蔽入口 // { key: "payment", label: "支付设置" }, { key: "favorite", label: "收藏站点" } ];