| | |
| | | } |
| | | |
| | | .marketing-popup-multi-panel { |
| | | margin-top: 408px; |
| | | margin-top: auto; |
| | | z-index: 100010; |
| | | width: 560px; |
| | | height: auto; |
| | |
| | | .marketing-popup-single-panel { |
| | | position: relative; |
| | | margin-top: 388px; |
| | | margin: auto; |
| | | z-index: 100010; |
| | | width: 540px; |
| | | height: auto; |
| | |
| | | } |
| | | } |
| | | .marketing-popup-coupon-title { |
| | | margin-top: 64px; |
| | | margin-top: 72px; |
| | | font-size: 56px; |
| | | line-height: 68px; |
| | | display: flex; |
| | |
| | | align-items: center; |
| | | justify-content: center; |
| | | margin: 0 auto; |
| | | margin-top: 112px; |
| | | margin-top: 120px; |
| | | font-size: 29.79px; |
| | | font-weight: 500; |
| | | line-height: 44.68px; |
| | |
| | | align-items: center; |
| | | color: #5b290e; |
| | | } |
| | | // .marketing-popup-close-single{ |
| | | // margin-top: 0 !important; |
| | | // } |
| | | // .marketing-popup-close-icon{ |
| | | // position: absolute; |
| | | // bottom: -120rpx; |
| | | // } |
| | | } |
| | | |
| | | .marketing-popup.is-active { |
| | |
| | | } |
| | | |
| | | const timer = setTimeout(() => { |
| | | props.onClose(); |
| | | // props.onClose(); |
| | | }, AUTO_CLOSE_DURATION); |
| | | |
| | | return () => { |
| | |
| | | import { FLOAT_ACTIVITY_DEFAULT_IMAGE } from '../constants'; |
| | | |
| | | type MarketingData = Record<string, any>; |
| | | const DEFAULT_MARKETING_POPUP_TITLE = '恭喜获得优惠券'; |
| | | const DEFAULT_MARKETING_POPUP_BUTTON_TEXT = '我知道了'; |
| | | |
| | | type Deps = { |
| | | /** 首次进入营销弹窗查询。 */ |
| | |
| | | }; |
| | | |
| | | /** |
| | | * 兼容旧 firstEnter 与新 exposureCoupon 两种返回结构,统一为首页营销弹窗可消费的数据。 |
| | | */ |
| | | const normalizeMarketingData = (rawData: any): MarketingData => { |
| | | if (Array.isArray(rawData)) { |
| | | const activities = rawData.filter(Boolean); |
| | | const selectedActivity = activities.find((item: any) => Array.isArray(item?.coupons) && item.coupons.length > 0) |
| | | || activities[0] |
| | | || {}; |
| | | const coupons = (Array.isArray(selectedActivity?.coupons) ? selectedActivity.coupons : []) |
| | | .filter(Boolean) |
| | | .map((item: any, index: number) => ({ |
| | | ...item, |
| | | id: item?.id ?? item?.couponId ?? index, |
| | | title: item?.title ?? item?.couponName ?? '', |
| | | amount: item?.amount ?? item?.money ?? item?.discountAmount ?? 0, |
| | | unit: item?.unit ?? (Number(item?.couponType) === 1 ? '折' : '元'), |
| | | endTime: item?.endTime ?? item?.expirationTime ?? item?.expireText ?? '', |
| | | quantity: item?.quantity ?? item?.count ?? 1, |
| | | conditionalAmount: item?.conditionalAmount ?? item?.fullMoney ?? 0 |
| | | })); |
| | | return { |
| | | ...selectedActivity, |
| | | isFirst: activities.length > 0, |
| | | coupons, |
| | | activityId: selectedActivity?.activityId ?? selectedActivity?.id, |
| | | popupId: selectedActivity?.popupId ?? selectedActivity?.id, |
| | | id: selectedActivity?.id ?? selectedActivity?.activityId, |
| | | miniProgramPopupTitle: selectedActivity?.miniProgramPopupTitle || selectedActivity?.acticityName || DEFAULT_MARKETING_POPUP_TITLE, |
| | | miniProgramBtnText: selectedActivity?.miniProgramBtnText || DEFAULT_MARKETING_POPUP_BUTTON_TEXT |
| | | }; |
| | | } |
| | | |
| | | if (rawData && typeof rawData === 'object') { |
| | | return { |
| | | miniProgramPopupTitle: rawData?.miniProgramPopupTitle || DEFAULT_MARKETING_POPUP_TITLE, |
| | | miniProgramBtnText: rawData?.miniProgramBtnText || DEFAULT_MARKETING_POPUP_BUTTON_TEXT, |
| | | ...rawData |
| | | }; |
| | | } |
| | | |
| | | return { |
| | | isFirst: false, |
| | | coupons: [], |
| | | miniProgramPopupTitle: DEFAULT_MARKETING_POPUP_TITLE, |
| | | miniProgramBtnText: DEFAULT_MARKETING_POPUP_BUTTON_TEXT |
| | | }; |
| | | }; |
| | | |
| | | /** |
| | | * 刷新首页营销弹窗与悬浮活动状态。 |
| | | */ |
| | | const syncMarketingState = async (hasLogin: boolean) => { |
| | |
| | | deps.firstEnter(), |
| | | deps.getFloatActicity() |
| | | ]); |
| | | const firstEnterData = firstEnterResponse?.data ?? firstEnterResponse ?? {}; |
| | | const firstEnterData = normalizeMarketingData(firstEnterResponse?.data ?? firstEnterResponse ?? {}); |
| | | const floatData = floatResponse?.data ?? floatResponse ?? {}; |
| | | const isFloatActivityEnabled = Number(floatData?.enableFloatWindow) === 1; |
| | | |
| | | setShowMarketing(Boolean(firstEnterData?.isFirst)); |
| | | setShowMarketing(Array.isArray(firstEnterData?.coupons) |
| | | ? firstEnterData.coupons.length > 0 |
| | | : Boolean(firstEnterData?.isFirst)); |
| | | setMarketingData(firstEnterData); |
| | | setShowFloatActivity(isFloatActivityEnabled); |
| | | setFloatActivityUrl(String(floatData?.floatWindowJumpUrl || '')); |
| | | setFloatActivityImg(String(floatData?.coverImage || '') || FLOAT_ACTIVITY_DEFAULT_IMAGE); |
| | | |
| | | let parsedCoverUrl = ''; |
| | | try { |
| | | const coverImage = floatData?.coverImage; |
| | | if (coverImage) { |
| | | const coverArray = typeof coverImage === 'string' ? JSON.parse(coverImage) : coverImage; |
| | | parsedCoverUrl = Array.isArray(coverArray) ? coverArray[0]?.url : ''; |
| | | } |
| | | } catch (e) { |
| | | console.error('解析悬浮窗图片失败', e); |
| | | } |
| | | setFloatActivityImg(parsedCoverUrl || FLOAT_ACTIVITY_DEFAULT_IMAGE); |
| | | } catch (error) { |
| | | resetMarketingState(); |
| | | } |
| | |
| | | right: 6px; |
| | | bottom: 234px; |
| | | z-index: 10020; |
| | | width: 96px; |
| | | height: 96px; |
| | | width: 108px; |
| | | height: 108px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | |
| | | .activity-float-window-img { |
| | | width: 96px; |
| | | height: 96px; |
| | | width: 108px; |
| | | height: 108px; |
| | | } |
| | | } |
| | | |
| | |
| | | UnlockVehiclePayload, |
| | | UnlockVehicleResponse, |
| | | OrderPaymentResponse, |
| | | QueryOrderPaymentResultResponse |
| | | QueryOrderPaymentResultResponse, |
| | | QueryLedgerCreditOrderResultResponse |
| | | } from './types/begin-route'; |
| | | |
| | | // 导航-获取路线和费用 |
| | |
| | | return request({ |
| | | url: _ROBOTAXI_HOST + `/order/api/v1/mini-program/order/creditPay/createOrder`, |
| | | method: 'POST', |
| | | payload |
| | | payload, |
| | | showToast: false |
| | | }, true); |
| | | } |
| | | |
| | | // 查询免密签约状态 |
| | | export const queryCreditPaySign = async (): Promise<any> => { |
| | | return request({ |
| | | url: _ROBOTAXI_HOST + '/order/api/v1/mini-program/order/creditPay/sign', |
| | | method: 'POST' |
| | | }, true); |
| | | } |
| | | |
| | |
| | | }, 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 |
| | |
| | | // 第一次进入应用 |
| | | export const firstEnter = async () => { |
| | | return request({ |
| | | url: _ROBOTAXI_HOST + '/operation/api/v1/mini-program/acticity/firstEnter', |
| | | // url: _ROBOTAXI_HOST + '/operation/api/v1/mini-program/acticity/firstEnter', |
| | | url: _ROBOTAXI_HOST + '/operation/api/v1/mini-program/acticity/exposureCoupon', |
| | | method: 'GET' |
| | | }, true); |
| | | } |
| | |
| | | }; |
| | | |
| | | /** |
| | | * 查询支付分开通情况。 |
| | | */ |
| | | export const getPayNoPwd = async (): Promise<GetPayPwdResponse> => { |
| | | return request({ |
| | | url: _ROBOTAXI_HOST + '/order/api/v1/mini-program/order/creditPay/sign', |
| | | method: 'POST', |
| | | }, true); |
| | | }; |
| | | /** |
| | | * 设置支付分开通状态。 |
| | | */ |
| | | export const setPayPwd = async (status: RobotaxiPaySettingStatus): Promise<SetPayPwdResponse> => { |
| | |
| | | |
| | | /** 订单支付结果查询返回值。 */ |
| | | export type QueryOrderPaymentResultResponse = { |
| | | status: RobotaxiOrderStatus | number; |
| | | orderId: string; |
| | | payStatus: number; |
| | | payType: number; |
| | | payTime: string; |
| | | }; |
| | | |
| | | // 信用分授权结果查询响应 |
| | | export type QueryLedgerCreditOrderResultResponse = { |
| | | creditOrderStatus: 'DOING' | 'REVOKED' | 'EXPIRED' | string; |
| | | }; |
| | |
| | | }: 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(() => { |
| | |
| | | : |
| | | <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> |
| | |
| | | font-size: 24px; |
| | | line-height: 40px; |
| | | letter-spacing: normal; |
| | | color: @black-text-main; |
| | | color: rgba(0, 0, 0, 0.9); |
| | | display: flex; |
| | | align-items: center; |
| | | |
| | | .unit { |
| | | margin-left: 4px; |
| | | font-size: 24px; |
| | | line-height: 40px; |
| | | letter-spacing: normal; |
| | | color: rgba(0, 0, 0, 0.9); |
| | | } |
| | | } |
| | | |
| | | &-btn-wrapper { |
| | | width: 296px; |
| | | height: 94px; |
| | | position: relative; |
| | | } |
| | | |
| | | &-btn { |
| | | width: 296px; |
| | | height: 94px; |
| | | display: flex; |
| | | justify-content: center; |
| | | align-items: center; |
| | | border-radius: 20px; |
| | | background: linear-gradient(102deg, #8475FF 0%, #7DD6FF 102%); |
| | | font-size: 28px; |
| | | 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); |
| | | } |
| | | } |
| | | .pay-box-btn { |
| New file |
| | |
| | | .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; |
| | | } |
| | | } |
| | |
| | | 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 [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 || '-'; |
| | |
| | | const step1 = setTimeout(() => setCompletedCount(1), 1000); |
| | | const step2 = setTimeout(() => setCompletedCount(2), 2000); |
| | | |
| | | // 3秒后进入呼叫车中状态 |
| | | // 3 秒后标记动画完成;是否真正切页由 finishBlocked 控制 |
| | | const step3 = setTimeout(() => { |
| | | onFinish?.(); |
| | | setFinishReady(true); |
| | | }, 3000); |
| | | |
| | | return () => { |
| | |
| | | }; |
| | | }, []); |
| | | |
| | | |
| | | useEffect(() => { |
| | | // 信用分下单时,需要等待用户从支付分确认页返回后再推进到“呼叫车中”。 |
| | | if (!finishReady || finishBlocked || finishTriggeredRef.current) { |
| | | return; |
| | | } |
| | | finishTriggeredRef.current = true; |
| | | onFinish?.(); |
| | | }, [finishBlocked, finishReady, onFinish]); |
| | | |
| | | const renderSiteItemMarker = (index, count) => { |
| | | /* 根据 completedCount 动态显示完成图标 */ |
| | | if (index < count) { |
| New file |
| | |
| | | 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); |
| | |
| | | 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'; |
| | |
| | | /** |
| | | * 拉起微信支付分订单确认页。 |
| | | */ |
| | | 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; |
| | | } |
| | |
| | | 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(); |
| | | Taro.showToast({ title: '授权成功', icon: 'none' }); |
| | | 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(); |
| | | Taro.showToast({ title: '授权成功', icon: 'none' }); |
| | | void deps.onAuthorizedReturn(queryId); |
| | | return; |
| | | } |
| | | |
| | | Taro.hideLoading(); |
| | | console.log('[信用分授权查询失败]', error); |
| | | void deps.onUnauthorizedReturn(queryOrderId); |
| | | } |
| | | }; |
| | | |
| | | await pollStatus(); |
| | | }; |
| | | |
| | | appShowListenerRef.current = handleAppShow; |
| | |
| | | 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]); |
| | | |
| | |
| | | /** 未签约态支付处理。 */ |
| | | 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; |
| | | }; |
| | |
| | | deps.setVehicleInfo({}); |
| | | clearOrderTripStorage(); |
| | | Taro.removeStorageSync('pickupEndTime'); |
| | | Taro.removeStorageSync('creditPayOrderId'); |
| | | Taro.removeStorageSync(WAITING_TIMER_START_STORAGE_KEY); |
| | | Taro.removeStorageSync(WAITING_TIMER_ELAPSED_STORAGE_KEY); |
| | | }; |
| | |
| | | } else { |
| | | syncTripFinishedStatus(); |
| | | } |
| | | return orderDetail; |
| | | return { |
| | | orderDetail, |
| | | nextTripStatus |
| | | }; |
| | | }; |
| | | |
| | | /** |
| | |
| | | |
| | | 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({ |
| | |
| | | await endOrder(targetOrderId); |
| | | } catch (error) { |
| | | } |
| | | await syncPostEndOrderTripStatus(targetOrderId); |
| | | const { nextTripStatus } = await syncPostEndOrderTripStatus(targetOrderId); |
| | | if (nextTripStatus === TripStatus.TripFinished) { |
| | | // 倒计时自动结束行程时也复用同一套信用扣款结果查询逻辑。 |
| | | await deps.handleCreditPayTripFinished?.(targetOrderId); |
| | | } |
| | | }; |
| | | |
| | | /** |
| | |
| | | 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 { |
| | |
| | | import Taro from '@tarojs/taro'; |
| | | import { useCallback, useRef } from 'react'; |
| | | import { formatDateTime, debounce, reportEvent } from '../../../../utils/Robotaxi-public-method'; |
| | | import { createCreditPayOrder, saveOrder, routeStaleCheck } from '../../../api/begin-route'; |
| | | import { createCreditPayOrder, saveOrder, routeStaleCheck, queryCreditPaySign } from '../../../api/begin-route'; |
| | | import type { SaveOrderPayload } from '../../../api/types/begin-route'; |
| | | import { getAgreement } from '../../../api/home'; |
| | | import { getPayPwd, RobotaxiPaySettingStatus, setPayPwd } from '../../../api/payment'; |
| | | import useCreditPaySign from '../../../hooks/use-credit-pay-sign'; |
| | | import { checkOtp } from '../../../../utils/tools'; |
| | | import { jumpTo } from '../../../../utils/jump'; |
| | |
| | | callImmediately: () => void; |
| | | }; |
| | | |
| | | /** 当前路线是否为载客测试免费。 */ |
| | | const isOperateFreeRoute = (selectedRoute: any) => Number(selectedRoute?.operateFree) === 1; |
| | | |
| | | /** 标准化文本,兼容普通空格与不可见空格差异。 */ |
| | | const normalizeText = (value: any) => String(value || '').replace(/\s+/g, ' ').trim().toLowerCase(); |
| | | |
| | |
| | | skipCreditPayCheck?: boolean; |
| | | /** 是否跳过下单起始埋点。 */ |
| | | skipPayStartReport?: boolean; |
| | | /** 跳过校验后强制指定信用下单模式。 */ |
| | | forcedCreditPaySubmitMode?: 'credit_pay_submit' | 'credit_pay_skip_auth_submit'; |
| | | }; |
| | | |
| | | type CreditPayCheckResult = 'normal_submit' | 'credit_pay_submit' | 'blocked'; |
| | | type CreditPayCheckResult = |
| | | | 'normal_submit' |
| | | | 'credit_pay_submit' |
| | | | 'credit_pay_skip_auth_submit' |
| | | | 'blocked'; |
| | | |
| | | /** 将签约状态转换为后续下单模式。 */ |
| | | const resolveCreditPaySubmitMode = ( |
| | | status: any |
| | | ): Extract<CreditPayCheckResult, 'credit_pay_submit' | 'credit_pay_skip_auth_submit'> | null => { |
| | | if (Number(status) === 2) { |
| | | return 'credit_pay_skip_auth_submit'; |
| | | } |
| | | if (Number(status) === 3) { |
| | | return 'credit_pay_submit'; |
| | | } |
| | | return null; |
| | | }; |
| | | |
| | | /** |
| | | * 管理下单前校验与提交动作。 |
| | |
| | | export default function useOrderSubmit(deps: OrderActionsDeps): UseOrderSubmitResult { |
| | | const waitingCreditPayReturnRef = useRef(false); // 是否正在等待微信支付分签约页返回 |
| | | const submitOrderFlowRef = useRef<(options?: SubmitOrderFlowOptions) => Promise<void>>(); // 始终指向最新的下单流程 |
| | | |
| | | /** |
| | | * 查询长期签约回跳后的最新支付分状态,并决定是否继续信用下单。 |
| | | */ |
| | | const resolveCreditPayModeAfterSignReturn = useCallback(async () => { |
| | | Taro.showLoading({ title: '授权结果查询中', mask: true }); |
| | | let shouldHideLoading = true; |
| | | try { |
| | | const signResult = await queryCreditPaySign(); |
| | | const latestStatus = signResult?.data?.status ?? signResult?.status; |
| | | const resolvedCreditPaySubmitMode = resolveCreditPaySubmitMode(latestStatus); |
| | | if (resolvedCreditPaySubmitMode) { |
| | | Taro.hideLoading(); |
| | | shouldHideLoading = false; |
| | | Taro.showToast({ title: '授权成功', icon: 'none' }); |
| | | return resolvedCreditPaySubmitMode; |
| | | } |
| | | return resolvedCreditPaySubmitMode; |
| | | } catch (error) { |
| | | return null; |
| | | } finally { |
| | | if (shouldHideLoading) { |
| | | Taro.hideLoading(); |
| | | } |
| | | } |
| | | }, []); |
| | | |
| | | /** |
| | | * 结束下单中状态。 |
| | |
| | | } |
| | | }; |
| | | |
| | | /** |
| | | * 继续执行签约页返回后的下单流程。 |
| | | */ |
| | | const continueSubmitAfterCreditPayReturn = useCallback(() => { |
| | | if (!waitingCreditPayReturnRef.current) { |
| | | return; |
| | | } |
| | | waitingCreditPayReturnRef.current = false; |
| | | void submitOrderFlowRef.current?.({ |
| | | skipCreditPayCheck: true, |
| | | skipPayStartReport: true |
| | | }); |
| | | }, []); |
| | | |
| | | /** |
| | | * 拉起签约失败后,降级为继续正常下单。 |
| | | */ |
| | | const continueSubmitAfterCreditPayLaunchFail = useCallback(() => { |
| | | if (!waitingCreditPayReturnRef.current) { |
| | | return; |
| | | } |
| | | waitingCreditPayReturnRef.current = false; |
| | | // Taro.showToast({ title: '拉起免密开通失败,已继续下单', icon: 'none' }); |
| | | void submitOrderFlowRef.current?.({ |
| | | skipCreditPayCheck: true, |
| | | skipPayStartReport: true |
| | | }); |
| | | }, []); |
| | | |
| | | const { openCreditPaySign } = useCreditPaySign({ |
| | | onReturn: continueSubmitAfterCreditPayReturn, |
| | | onLaunchFail: continueSubmitAfterCreditPayLaunchFail |
| | | onReturn: async () => { |
| | | // 长期签约页回跳后,先查询最新授权结果,再决定是否继续信用下单。 |
| | | waitingCreditPayReturnRef.current = false; |
| | | const nextCreditPaySubmitMode = await resolveCreditPayModeAfterSignReturn(); |
| | | if (!nextCreditPaySubmitMode) { |
| | | stopCallSubmitting(); |
| | | Taro.showToast({ title: '支付分授权未完成,请重试', icon: 'none' }); |
| | | return; |
| | | } |
| | | |
| | | void submitOrderFlowRef.current?.({ |
| | | skipCreditPayCheck: true, |
| | | forcedCreditPaySubmitMode: nextCreditPaySubmitMode |
| | | }); |
| | | }, |
| | | onLaunchFail: () => { |
| | | waitingCreditPayReturnRef.current = false; |
| | | stopCallSubmitting(); |
| | | Taro.showToast({ title: '拉起签约失败', icon: 'none' }); |
| | | } |
| | | }); |
| | | |
| | | /** |
| | | * 下单前校验免密状态。 |
| | | */ |
| | | const checkCreditPayReadyBeforeOrder = useCallback(async () => { |
| | | // 免密状态校验,先默认返回正常下单,后续根据校验结果再处理。 |
| | | return 'normal_submit' as CreditPayCheckResult; |
| | | const checkCreditPayReadyBeforeOrder = useCallback(async (selectedRoute: any) => { |
| | | if (isOperateFreeRoute(selectedRoute)) { |
| | | // 载客测试免费单直接走普通下单,不拉起微信信用下单弹窗。 |
| | | return 'normal_submit' as CreditPayCheckResult; |
| | | } |
| | | |
| | | try { |
| | | const hasEnabledCreditPay = await getPayPwd(); |
| | | if (hasEnabledCreditPay === true) { |
| | | return 'normal_submit' as CreditPayCheckResult; |
| | | const res = await queryCreditPaySign(); |
| | | const status = res?.data?.status ?? res?.status; // 兼容不同包裹层级 |
| | | const value = res?.data?.value ?? res?.value; |
| | | |
| | | const resolvedCreditPaySubmitMode = resolveCreditPaySubmitMode(status); |
| | | if (resolvedCreditPaySubmitMode) { |
| | | // status=2: 已确认签约,信用下单后无需再拉起微信支付分授权页。 |
| | | // status=3: 每次下单授权流程,信用下单后仍需拉起微信支付分确认页。 |
| | | return resolvedCreditPaySubmitMode; |
| | | } |
| | | |
| | | const isCanceled = await deps.handleOpenCreditPayPopup(); |
| | | if (isCanceled) { |
| | | // 用户取消开通,继续下单 |
| | | return 'normal_submit' as CreditPayCheckResult; |
| | | } |
| | | if (status === 1) { |
| | | // 需要拉起长期授权 |
| | | const isCanceled = await deps.handleOpenCreditPayPopup(); |
| | | if (isCanceled) { |
| | | // 用户取消开通,中断下单流程 |
| | | stopCallSubmitting(); |
| | | return 'blocked' as CreditPayCheckResult; |
| | | } |
| | | |
| | | if (Number(deps.distanceConfig?.creditPayNeedConfirm) !== 0) { |
| | | // 默认优先走信用下单,只有后端明确返回 0 时才继续旧的签约流程。 |
| | | return 'credit_pay_submit' as CreditPayCheckResult; |
| | | } |
| | | |
| | | try { |
| | | const response = await setPayPwd(RobotaxiPaySettingStatus.Enable); |
| | | const signStr = String(response?.signStr || '').trim(); |
| | | // value 为支付中心签约响应对象(CreditPaySignVo),需取其中的 signStr 字段作为预授权 token。 |
| | | const signStr = String(value?.signStr ?? value ?? '').trim(); |
| | | if (!signStr) { |
| | | // 获取免密签约信息失败,降级为继续下单 |
| | | return 'normal_submit' as CreditPayCheckResult; |
| | |
| | | // 等待微信签约页返回后再恢复下单,避免当前流程继续往下执行。 |
| | | stopCallSubmitting(); |
| | | return 'blocked' as CreditPayCheckResult; |
| | | } catch (e) { |
| | | // 免密状态校验失败,降级为继续下单 |
| | | return 'normal_submit' as CreditPayCheckResult; |
| | | } |
| | | |
| | | // 未知状态降级为继续下单 |
| | | return 'normal_submit' as CreditPayCheckResult; |
| | | |
| | | } catch (error) { |
| | | // 接口失败,降级为继续下单 |
| | | return 'normal_submit' as CreditPayCheckResult; |
| | | } |
| | | }, [openCreditPaySign, deps]); |
| | |
| | | return; |
| | | } |
| | | |
| | | let submitMode: CreditPayCheckResult = 'normal_submit'; |
| | | let submitMode: CreditPayCheckResult = options.forcedCreditPaySubmitMode || 'normal_submit'; |
| | | if (!options.skipCreditPayCheck) { |
| | | submitMode = await checkCreditPayReadyBeforeOrder(); |
| | | submitMode = await checkCreditPayReadyBeforeOrder(selectedRoute); |
| | | if (submitMode === 'blocked') { |
| | | return; |
| | | } |
| | |
| | | await waitForOrderSuccessSubscribePopup(); |
| | | |
| | | try { |
| | | const submitResult = submitMode === 'credit_pay_submit' |
| | | const isCreditPaySubmit = submitMode === 'credit_pay_submit' |
| | | || submitMode === 'credit_pay_skip_auth_submit'; |
| | | |
| | | if (isCreditPaySubmit) { |
| | | Taro.showLoading({ title: '加载中', mask: true }); |
| | | } |
| | | |
| | | const submitResult = isCreditPaySubmit |
| | | ? 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') { |
| | | |
| | | if (!isCreditPaySubmit) { |
| | | Taro.removeStorageSync('creditPayOrderId'); |
| | | } |
| | | |
| | | if (isCreditPaySubmit) { |
| | | Taro.setStorageSync('creditPayOrderId', submitResult.orderId); |
| | | if (submitMode === 'credit_pay_skip_auth_submit') { |
| | | Taro.hideLoading(); |
| | | // status=2 代表当前已是免确认模式,信用下单成功后直接进入发单态。 |
| | | deps.setTripStatus(TripStatus.Publishing); |
| | | return; |
| | | } |
| | | |
| | | 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' || submitMode === 'credit_pay_skip_auth_submit') { |
| | | Taro.showToast({ title: errorMessage, icon: 'none' }); |
| | | } |
| | | reportPayResultEvent(resolvePayResultStatus(errorMessage)); |
| | | stopCallSubmitting(); |
| | | } |
| | | }; |
| | |
| | | endSite: any; |
| | | /** 发单 */ |
| | | callImmediately: () => void; |
| | | /** 是否正在等待信用分确认页返回结果 */ |
| | | isWaitingSignedCreditPayResult?: boolean; |
| | | /** 预估态选择上下车点 */ |
| | | handleEstimateSelectSite: (pageType: "start" | "end") => void; |
| | | /** 费用信息 */ |
| | |
| | | handlePickupTimeoutAutoCancel: () => void; |
| | | /** 行程状态设置 */ |
| | | setTripStatus: (value: TripStatus) => void; |
| | | /** 发布中动画结束 */ |
| | | handlePublishingAnimationFinish?: () => void; |
| | | /** 完成倒计时启用 */ |
| | | finishCountdownEnabled: boolean; |
| | | /** 订单 ID */ |
| | |
| | | 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); |
| | | }} |
| | |
| | | |
| | | 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>; |
| | |
| | | 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); |
| | |
| | | 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'; |
| | |
| | | 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); // 当前评价弹窗对应评分 |
| | |
| | | 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 |
| | |
| | | }, []); |
| | | |
| | | /** |
| | | * 统一维护“等待信用分结果”状态,同时同步 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(() => { |
| | |
| | | |
| | | const { bindSocketListeners, ensureSocketListeners } = useWebSocketEvents({ |
| | | tripStatusRef, |
| | | isWaitingSignedCreditPayResultRef, |
| | | pendingSignedCreditPayTripStatusRef, |
| | | isHoldingTripStatusDuringPublishingAnimationRef, |
| | | pendingTripStatusAfterPublishingAnimationRef, |
| | | pickupArrivedReportedOrderIdRef, |
| | | tripInProgressReportedOrderIdRef, |
| | | arrivedDestinationReportedOrderIdRef, |
| | |
| | | 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({ |
| | |
| | | /** |
| | | * 处理已签约态支付分支付。 |
| | | */ |
| | | 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, |
| | |
| | | routeRequestRouteAndFare, |
| | | handleUnsignedPayment, |
| | | handleSignedCreditPayment, |
| | | handleCreditPayTripFinished, |
| | | setStartSite |
| | | }); |
| | | |
| | |
| | | handlePickupTimeoutAutoCancel, |
| | | handleConfirmOverRoute, |
| | | handleFinishCountdownTimeout, |
| | | handlePay |
| | | handlePay, |
| | | stopCallSubmitting, |
| | | clearActiveOrderRuntime, |
| | | restartPlanningRoute |
| | | } = orderActions; |
| | | stopCallSubmittingRef.current = stopCallSubmitting; |
| | | clearActiveOrderRuntimeRef.current = clearActiveOrderRuntime; |
| | | restartPlanningRouteRef.current = restartPlanningRoute; |
| | | const { |
| | | authSuccess: bluetoothAuthSuccess, |
| | | startAuth: startBluetoothAuth, |
| | |
| | | useDidShow(() => { |
| | | setPageDidShowVersion((v) => v + 1); |
| | | setDoorUnlockedInCurrentPage(false); |
| | | // 信用分结果页返回后,需要先保留“发布中”动画,不要立刻走页面恢复链路把状态顶到最新。 |
| | | if ( |
| | | isWaitingSignedCreditPayResultRef.current |
| | | || isHoldingTripStatusDuringPublishingAnimationRef.current |
| | | ) { |
| | | updateMapViewportPadding(); |
| | | return; |
| | | } |
| | | handleDidShow(router.params?.skipPageFlag, router.params?.fromModifyDestination); |
| | | updateMapViewportPadding(); |
| | | // 完成态下隐藏小程序再恢复时,受控中心点可能与原生地图实际中心脱钩,主动触发一次 |
| | |
| | | elapsedSeconds, |
| | | endSite: effectiveEndSite, |
| | | callImmediately: handleCallImmediately, |
| | | isWaitingSignedCreditPayResult, |
| | | handleEstimateSelectSite, |
| | | fareEstimate, |
| | | handleOpenCouponSelect, |
| | |
| | | handleOpenCancelPopupWithType, |
| | | handlePickupTimeoutAutoCancel, |
| | | setTripStatus, |
| | | handlePublishingAnimationFinish, |
| | | finishCountdownEnabled, |
| | | orderId, |
| | | vehicleInfo, |
| | |
| | | if (status == 10) { |
| | | return "已完成"; |
| | | } |
| | | if (status == 11) { |
| | | return "退款中"; |
| | | } |
| | | if (status == 12) { |
| | | return "已退款"; |
| | | } |
| | | if (status == 13) { |
| | | return "退款失败"; |
| | | } |
| | | }; |
| | | |
| | | const getOrderStatusClassName = (status: number) => { |
| | |
| | | </View> |
| | | )} |
| | | {/* 发票功能暂未上线,屏蔽入口 */} |
| | | {false && ["已完成", "待评价"].includes( |
| | | { ["已完成", "待评价"].includes( |
| | | formatOrderStatus(item.status) |
| | | ) && |
| | | item.invoiceStatus != 2 && |
| | |
| | | </View> |
| | | </View> |
| | | )} |
| | | {["已完成", "已取消", "待评价"].includes( |
| | | {["已完成", "已取消", "待评价", "退款中", "已退款", "退款失败"].includes( |
| | | formatOrderStatus(item.status) |
| | | ) && |
| | | canAgainOrder(item) && ( |
| | |
| | | import { View, Text, Image } from '@tarojs/components'; |
| | | import { useCallback, useState } from 'react'; |
| | | import { getPayPwd, RobotaxiPaySettingStatus, setPayPwd } from '../../api/payment'; |
| | | import { getOrderActive } from '../../api/home'; |
| | | import { withRobotaxiWhitelist } from '../../../utils/whitelist'; |
| | | |
| | | import './index.less'; |
| | |
| | | }, []); |
| | | |
| | | /** |
| | | * 关闭免密支付前,先确认当前没有进行中的订单。 |
| | | */ |
| | | const ensureNoActiveOrderBeforeClose = useCallback(async () => { |
| | | try { |
| | | const activeOrder = await getOrderActive(); |
| | | if (activeOrder) { |
| | | Taro.showToast({ |
| | | title: '有进行中的订单,暂无法关闭服务', |
| | | icon: 'none' |
| | | }); |
| | | return false; |
| | | } |
| | | return true; |
| | | } catch (error) { |
| | | Taro.showToast({ |
| | | title: '查询订单状态失败,请稍后重试', |
| | | icon: 'none' |
| | | }); |
| | | return false; |
| | | } |
| | | }, []); |
| | | |
| | | /** |
| | | * 关闭微信免密支付。 |
| | | */ |
| | | const handleClosePayment = async () => { |
| | |
| | | return; |
| | | } |
| | | |
| | | setIsOperating(true); |
| | | const canClosePayment = await ensureNoActiveOrderBeforeClose(); |
| | | if (!canClosePayment) { |
| | | setIsOperating(false); |
| | | return; |
| | | } |
| | | |
| | | try { |
| | | setIsOperating(true); |
| | | await setPayPwd(RobotaxiPaySettingStatus.Disable); |
| | | Taro.showToast({ |
| | | title: '微信免密支付已关闭', |
| | |
| | | |
| | | const toolMenuList: ToolMenuItem[] = [ |
| | | { key: "service", label: "联系客服" }, |
| | | // { key: "invoice", label: "开发票" }, // 暂未上线,屏蔽入口 |
| | | { key: "invoice", label: "开发票" }, // 暂未上线,屏蔽入口 |
| | | { key: "payment", label: "支付设置" }, |
| | | { key: "favorite", label: "收藏站点" } |
| | | ]; |