Merge branch 'feature_v1.1' into feature_v2.0_zjk
| | |
| | | }, true); |
| | | } |
| | | |
| | | // 信用下单 |
| | | export const createCreditPayOrder = async ( |
| | | payload: SaveOrderPayload |
| | | ): Promise<SaveOrderResponse> => { |
| | | return request({ |
| | | url: _ROBOTAXI_HOST + `/order/api/v1/mini-program/order/creditPay/createOrder`, |
| | | method: 'POST', |
| | | payload |
| | | }, true); |
| | | } |
| | | |
| | | // 获取等待时间 |
| | | export const getWaitTime = async (payload: GetWaitTimePayload): Promise<NumericLike> => { |
| | | return request({ |
| | |
| | | } |
| | | |
| | | // 获取订单评价类型 |
| | | export const getOrderCommentType = async (): Promise<OrderCommentTypeItem[]> => { |
| | | export const getOrderCommentType = async (evaluateDimension: number): Promise<OrderCommentTypeItem[]> => { |
| | | return request({ |
| | | url: _ROBOTAXI_HOST + `/operation/base/v1/mini-program/feedback-type/getList`, |
| | | // url: _ROBOTAXI_HOST + `/operation/base/v1/mini-program/feedback-type/getList`, |
| | | url: _ROBOTAXI_HOST + `/operation/base/v1/mini-program/evaluateTag/list?evaluateDimension=${evaluateDimension}`, |
| | | method: 'GET' |
| | | }, true); |
| | | } |
| New file |
| | |
| | | import Taro from '@tarojs/taro'; |
| | | import { useCallback, useEffect, useRef } from 'react'; |
| | | |
| | | /** 支付分小程序回跳 appId。 */ |
| | | const CREDIT_PAY_MINIPROGRAM_APP_ID = 'wxd8f3793ea3b935b8'; |
| | | |
| | | type UseCreditPaySignDeps = { |
| | | /** 支付分签约页返回后的回调。 */ |
| | | onReturn?: () => void; |
| | | /** 拉起支付分签约失败后的回调。 */ |
| | | onLaunchFail?: () => void; |
| | | }; |
| | | |
| | | /** |
| | | * 统一处理微信支付分签约页的拉起与返回识别。 |
| | | */ |
| | | export default function useCreditPaySign(deps: UseCreditPaySignDeps = {}) { |
| | | /** 当前 AppShow 监听函数。 */ |
| | | const appShowListenerRef = useRef<any>(null); |
| | | |
| | | /** |
| | | * 清理签约回跳监听,避免重复绑定。 |
| | | */ |
| | | const clearCreditPaySignListener = useCallback(() => { |
| | | if (!appShowListenerRef.current) { |
| | | return; |
| | | } |
| | | Taro.offAppShow(appShowListenerRef.current); |
| | | appShowListenerRef.current = null; |
| | | }, []); |
| | | |
| | | /** |
| | | * 拉起微信支付分签约页。 |
| | | */ |
| | | const openCreditPaySign = useCallback((signStr: string) => { |
| | | const normalizedSignStr = String(signStr || '').trim(); |
| | | if (!normalizedSignStr) { |
| | | return false; |
| | | } |
| | | |
| | | // @ts-ignore |
| | | if (!wx.openBusinessView) { |
| | | return false; |
| | | } |
| | | |
| | | clearCreditPaySignListener(); |
| | | |
| | | /** 支付分签约页返回监听函数。 */ |
| | | const handleAppShow = (payload) => { |
| | | if (payload?.scene !== 1038) { |
| | | return; |
| | | } |
| | | |
| | | if (payload?.referrerInfo?.appId !== CREDIT_PAY_MINIPROGRAM_APP_ID) { |
| | | return; |
| | | } |
| | | |
| | | clearCreditPaySignListener(); |
| | | deps.onReturn?.(); |
| | | }; |
| | | |
| | | appShowListenerRef.current = handleAppShow; |
| | | Taro.onAppShow(handleAppShow); |
| | | |
| | | // @ts-ignore |
| | | wx.openBusinessView({ |
| | | businessType: 'wxpayScoreEnable', |
| | | extraData: { |
| | | apply_permissions_token: normalizedSignStr, |
| | | }, |
| | | fail: () => { |
| | | clearCreditPaySignListener(); |
| | | deps.onLaunchFail?.(); |
| | | } |
| | | }); |
| | | |
| | | return true; |
| | | }, [clearCreditPaySignListener, deps]); |
| | | |
| | | useEffect(() => clearCreditPaySignListener, [clearCreditPaySignListener]); |
| | | |
| | | return { |
| | | openCreditPaySign, |
| | | clearCreditPaySignListener |
| | | }; |
| | | } |
| | |
| | | showEvaluatePopup: boolean; |
| | | /** 订单 ID。 */ |
| | | orderId: string; |
| | | /** 当前评价弹窗对应的评分。 */ |
| | | evaluateOrderRating: number; |
| | | /** 关闭评价弹窗。 */ |
| | | handleCloseEvaluatePopup: () => void; |
| | | /** 确认评价。 */ |
| | |
| | | changeCheckedEnter, |
| | | showEvaluatePopup, |
| | | orderId, |
| | | evaluateOrderRating, |
| | | handleCloseEvaluatePopup, |
| | | handleConfirmEvaluate, |
| | | showAuthBluetoothPopup = false, |
| | |
| | | <EvaluatePopup |
| | | show={showEvaluatePopup} |
| | | orderId={orderId} |
| | | orderRating={1} |
| | | orderRating={evaluateOrderRating} |
| | | onClose={handleCloseEvaluatePopup} |
| | | onConfirm={handleConfirmEvaluate} |
| | | /> |
| | |
| | | type Props = { |
| | | tripStatus: TripStatus; |
| | | orderId?: string; |
| | | handleOpenEvaluatePopup?: () => void; |
| | | handleOpenEvaluatePopup?: (orderRating: number) => void; |
| | | evaluateSubmitVersion?: number; |
| | | evaluateCloseVersion?: number; |
| | | evaluatePopupStatus?: number; |
| | | }; |
| | | |
| | | export default function EvaluationCard({ |
| | |
| | | orderId, |
| | | handleOpenEvaluatePopup, |
| | | evaluateSubmitVersion, |
| | | evaluateCloseVersion |
| | | evaluateCloseVersion, |
| | | evaluatePopupStatus = 0 |
| | | }: Props) { |
| | | const good = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/good.png'; |
| | | const normal = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/normal.png'; |
| | |
| | | } |
| | | }, [evaluateCloseVersion]); |
| | | |
| | | // 点击评价项:不满意走弹窗,其余直接提交 |
| | | // 点击评价项:配置开启时任意评价都走弹窗,否则直接提交 |
| | | const changeEvaluate = (index: number) => { |
| | | if (isEvaluated) return; |
| | | setActiveIndex(index); |
| | | if (index === 1) { |
| | | handleOpenEvaluatePopup?.(); |
| | | if (evaluatePopupStatus === 1) { |
| | | handleOpenEvaluatePopup?.(index); |
| | | return; |
| | | } |
| | | const targetOrderId = getOrderId(orderId); |
| | |
| | | handleShareTrip?: () => void; |
| | | showModifyDestination?: boolean; |
| | | showCancelTrip?: boolean; |
| | | showShareTrip?: boolean; |
| | | }; |
| | | |
| | | export default function TripActionBar({ |
| | |
| | | handleModifyDestination, |
| | | handleShareTrip, |
| | | showModifyDestination = true, |
| | | showCancelTrip = false |
| | | showCancelTrip = false, |
| | | showShareTrip = true |
| | | }: Props) { |
| | | const canShowTripAction = [TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup, TripStatus.InService].includes(tripStatus); |
| | | // 按行程状态控制按钮集合,避免在 JSX 中堆叠过多条件判断 |
| | |
| | | key: "share", |
| | | label: "分享行程", |
| | | icon: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/share-trip.png', |
| | | visible: canShowTripAction, |
| | | visible: canShowTripAction && showShareTrip, |
| | | onClick: handleShareTrip |
| | | } |
| | | ].filter(item => item.visible); |
| | |
| | | evaluateSubmitVersion?: number; |
| | | /** 外部触发“评价弹窗关闭”版本号 */ |
| | | evaluateCloseVersion?: number; |
| | | /** 评价弹窗开关配置,0 不弹窗,1 点击任意评价都弹窗 */ |
| | | evaluatePopupStatus?: number; |
| | | /** 打开结束行程弹窗 */ |
| | | handleOpenOverRoutePopup?: () => void; |
| | | /** 费用明细展开/收起时同步地图视觉状态 */ |
| | |
| | | /** 立即支付回调 */ |
| | | handlePay?: () => void; |
| | | /** 打开详细评价弹窗 */ |
| | | handleOpenEvaluatePopup?: () => void; |
| | | handleOpenEvaluatePopup?: (orderRating: number) => void; |
| | | /** 取消行程 */ |
| | | handleCancelTrip?: () => void; |
| | | /** 修改目的地 */ |
| | | handleModifyDestination?: () => void; |
| | | /** 是否展示修改目的地 */ |
| | | showModifyDestination?: boolean; |
| | | /** 是否展示分享行程 */ |
| | | showShareTrip?: boolean; |
| | | /** 分享行程 */ |
| | | handleShareTrip?: () => void; |
| | | /** 预估行程秒数 */ |
| | |
| | | handleFinishTimeout, |
| | | evaluateSubmitVersion, |
| | | evaluateCloseVersion, |
| | | evaluatePopupStatus = 0, |
| | | handleOpenOverRoutePopup, |
| | | onToggleDetailFade, |
| | | handlePay, |
| | |
| | | handleCancelTrip, |
| | | handleModifyDestination, |
| | | showModifyDestination = true, |
| | | showShareTrip = true, |
| | | handleShareTrip, |
| | | routeDurationSeconds, |
| | | finishCountdownEnabled = false, |
| | |
| | | handleOpenEvaluatePopup={handleOpenEvaluatePopup} |
| | | evaluateSubmitVersion={evaluateSubmitVersion} |
| | | evaluateCloseVersion={evaluateCloseVersion} |
| | | evaluatePopupStatus={evaluatePopupStatus} |
| | | /> |
| | | </View> |
| | | ); |
| New file |
| | |
| | | import { Image, View } from '@tarojs/components'; |
| | | import './css-folder/credit-pay-popup.less'; |
| | | |
| | | type Props = { |
| | | show?: boolean; |
| | | onClose?: () => void; |
| | | onConfirm?: () => void; |
| | | }; |
| | | |
| | | export default function CreditPayPopup({ |
| | | show = false, |
| | | onClose, |
| | | onConfirm |
| | | }: Props) { |
| | | if (!show) { |
| | | return null; |
| | | } |
| | | |
| | | return ( |
| | | <View className='credit-pay-popup' catchMove> |
| | | <View className='credit-pay-mask' onClick={onClose}></View> |
| | | <View className='credit-pay-content'> |
| | | <View className='credit-pay-close' onClick={onClose}> |
| | | <Image |
| | | className='credit-pay-close-icon' |
| | | src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/close-icon.png' |
| | | /> |
| | | </View> |
| | | <View className='credit-pay-header'> |
| | | <Image |
| | | className='credit-pay-header-icon' |
| | | src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/pay-mini-icon.png' |
| | | /> |
| | | <View className='credit-pay-title'>微信支付分 | 先乘后付</View> |
| | | </View> |
| | | <View className='credit-pay-list'> |
| | | <View className='credit-pay-item'> |
| | | <Image |
| | | className='credit-pay-check-icon' |
| | | src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/credit-pay-popup.png' |
| | | /> |
| | | <View className='credit-pay-item-text'>银行级安全保障,资金更安全</View> |
| | | </View> |
| | | <View className='credit-pay-item'> |
| | | <Image |
| | | className='credit-pay-check-icon' |
| | | src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/credit-pay-popup.png' |
| | | /> |
| | | <View className='credit-pay-item-text'>随时可关闭,灵活管理</View> |
| | | </View> |
| | | </View> |
| | | <View className='credit-pay-confirm' onClick={onConfirm}> |
| | | 立即开通 |
| | | </View> |
| | | <View className='credit-pay-footer'> |
| | | <Image |
| | | className='credit-pay-footer-icon' |
| | | src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/pay-mini-icon.png' |
| | | /> |
| | | <View className='credit-pay-footer-text'>微信支付分 | 550分及以上优享</View> |
| | | </View> |
| | | </View> |
| | | </View> |
| | | ); |
| | | } |
| New file |
| | |
| | | .credit-pay-popup { |
| | | position: fixed; |
| | | inset: 0; |
| | | z-index: 1001; |
| | | } |
| | | |
| | | .credit-pay-mask { |
| | | position: absolute; |
| | | inset: 0; |
| | | background: rgba(0, 0, 0, 0.45); |
| | | } |
| | | |
| | | .credit-pay-content { |
| | | position: absolute; |
| | | left: 0; |
| | | right: 0; |
| | | bottom: 0; |
| | | background: #ffffff; |
| | | border-radius: 40px 40px 0 0; |
| | | padding: 60px 66px 72px; |
| | | box-sizing: border-box; |
| | | } |
| | | |
| | | .credit-pay-close { |
| | | position: absolute; |
| | | top: 24px; |
| | | right: 24px; |
| | | z-index: 10; |
| | | } |
| | | |
| | | .credit-pay-close-icon { |
| | | width: 40px; |
| | | height: 40px; |
| | | } |
| | | |
| | | .credit-pay-header { |
| | | display: flex; |
| | | align-items: center; |
| | | margin-bottom: 46px; |
| | | } |
| | | |
| | | .credit-pay-header-icon { |
| | | width: 48px; |
| | | height: 48px; |
| | | margin-right: 12px; |
| | | } |
| | | |
| | | .credit-pay-title { |
| | | font-size: 36px; |
| | | font-weight: 600; |
| | | color: rgba(0, 0, 0, 0.9); |
| | | } |
| | | |
| | | .credit-pay-list { |
| | | margin-bottom: 64px; |
| | | padding-left: 8px; |
| | | box-sizing: border-box; |
| | | } |
| | | |
| | | .credit-pay-item { |
| | | display: flex; |
| | | align-items: center; |
| | | margin-bottom: 8px; |
| | | |
| | | &:last-child { |
| | | margin-bottom: 0; |
| | | } |
| | | } |
| | | |
| | | .credit-pay-check-icon { |
| | | width: 26px; |
| | | height: 26px; |
| | | margin-right: 14px; |
| | | } |
| | | |
| | | .credit-pay-item-text { |
| | | font-size: 28px; |
| | | line-height: 46px; |
| | | color: rgba(0, 0, 0, 0.6); |
| | | } |
| | | |
| | | .credit-pay-confirm { |
| | | width: 586px; |
| | | height: 92px; |
| | | line-height: 92px; |
| | | text-align: center; |
| | | color: #ffffff; |
| | | font-size: 28px; |
| | | font-weight: 500; |
| | | background: #19c261; |
| | | border-radius: 46px; |
| | | margin-bottom: 24px; |
| | | margin-left: 18px; |
| | | } |
| | | |
| | | .credit-pay-footer { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .credit-pay-footer-icon { |
| | | width: 24px; |
| | | height: 24px; |
| | | margin-right: 8px; |
| | | } |
| | | |
| | | .credit-pay-footer-text { |
| | | font-size: 24px; |
| | | line-height: 46px; |
| | | color: rgba(0, 0, 0, 0.6); |
| | | } |
| New file |
| | |
| | | .pickup-exception-popup { |
| | | position: fixed; |
| | | inset: 0; |
| | | z-index: 1001; |
| | | } |
| | | |
| | | .pickup-exception-mask { |
| | | position: absolute; |
| | | inset: 0; |
| | | background: rgba(0, 0, 0, 0.6); |
| | | } |
| | | |
| | | .pickup-exception-content { |
| | | position: absolute; |
| | | left: 0; |
| | | right: 0; |
| | | bottom: 0; |
| | | background: #ffffff; |
| | | border-radius: 16px 16px 0 0; |
| | | padding: 120px 72px 50px; |
| | | box-sizing: border-box; |
| | | } |
| | | |
| | | .pickup-exception-close { |
| | | position: absolute; |
| | | top: 52px; |
| | | right: 32px; |
| | | z-index: 10; |
| | | } |
| | | |
| | | .pickup-exception-close-icon { |
| | | width: 40px; |
| | | height: 40px; |
| | | } |
| | | |
| | | .pickup-exception-map-wrapper { |
| | | position: relative; |
| | | width: 100%; |
| | | height: 378px; |
| | | overflow: hidden; |
| | | background: #f5f7fa; |
| | | } |
| | | |
| | | .pickup-exception-map { |
| | | width: 100%; |
| | | height: calc(100% + 100px); |
| | | } |
| | | |
| | | .pickup-exception-distance { |
| | | position: absolute; |
| | | left: 50%; |
| | | bottom: 28px; |
| | | transform: translateX(-50%); |
| | | background: rgba(0, 0, 0, 0.7); |
| | | color: #ffffff; |
| | | font-size: 20px; |
| | | font-weight: normal; |
| | | letter-spacing: normal; |
| | | line-height: 40px; |
| | | padding: 12px 20px; |
| | | border-radius: 8px; |
| | | white-space: nowrap; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .pickup-exception-message { |
| | | margin-top: 32px; |
| | | color: rgba(0, 0, 0, 0.9); |
| | | font-size: 28px; |
| | | line-height: 44px; |
| | | text-align: center; |
| | | } |
| | | |
| | | .pickup-exception-confirm { |
| | | margin-top: 48px; |
| | | height: 96px; |
| | | line-height: 96px; |
| | | text-align: center; |
| | | color: #ffffff; |
| | | font-size: 30px; |
| | | font-weight: 500; |
| | | background: #181916; |
| | | border-radius: 16px; |
| | | } |
| | |
| | | |
| | | export default function EvaluatePopup({ show = false, onClose, onConfirm, orderId, orderRating, submitMode = 'comment' }: Props) { |
| | | const activeBad = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/active-bad.png'; |
| | | const activeNormal = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/active-normal.png'; |
| | | const activeGood = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/active-good.png'; |
| | | const [visible, setVisible] = useState(false); // 弹窗显示状态 |
| | | const [animationData, setAnimationData] = useState<any>(); // 弹窗进出场动画 |
| | | const [selectedTypeId, setSelectedTypeId] = useState<number | null>(null); // 当前选择的问题类型 |
| | |
| | | compress: true, |
| | | uploadUrl: _ROBOTAXI_HOST + '/operation/base/v1/mini-program/oss/upload' |
| | | }); // 图片上传状态与操作 |
| | | |
| | | // 根据外部点击的评分,动态展示评价弹窗的 icon 和文案 |
| | | const currentOrderRating = Number(orderRating) || 1; |
| | | const popupDisplayConfig = { |
| | | 1: { |
| | | icon: activeBad, |
| | | title: '“不满意,我要吐槽”', |
| | | placeholder: '请详描述您遇到的问题(可添加相关截图辅助说明)' |
| | | }, |
| | | 2: { |
| | | icon: activeNormal, |
| | | title: '“一般般,还需努力”', |
| | | placeholder: '告诉我们哪些方面还可以继续改进' |
| | | }, |
| | | 3: { |
| | | icon: activeGood, |
| | | title: '“很满意,表扬一下”', |
| | | placeholder: '告诉我哪里做的不错,我们继续保持!' |
| | | } |
| | | }[currentOrderRating] || { |
| | | icon: activeBad, |
| | | title: '“不满意,我要吐槽”', |
| | | placeholder: '请详描述您遇到的问题(可添加相关截图辅助说明)' |
| | | }; |
| | | const evaluateDimensionMap: Record<number, number> = { |
| | | 3: 1, |
| | | 2: 2, |
| | | 1: 3 |
| | | }; |
| | | const evaluateDimension = evaluateDimensionMap[currentOrderRating] || 3; // 接口维度:1 满意,2 一般,3 不满意 |
| | | |
| | | // 控制弹窗开合动画 |
| | | useEffect(() => { |
| | |
| | | setSelectedTypeId(null); |
| | | setFeedbackContent(''); |
| | | resetImages(); |
| | | getOrderCommentType().then((res: any) => { |
| | | getOrderCommentType(evaluateDimension).then((res: any) => { |
| | | const list = res; |
| | | if (Array.isArray(list)) { |
| | | setTypeOptions(list); |
| | |
| | | }).catch(() => { |
| | | setTypeOptions([]); |
| | | }); |
| | | }, [resetImages, show]); |
| | | }, [evaluateDimension, resetImages, show]); |
| | | |
| | | if (!visible) return null; |
| | | |
| | |
| | | <Image onClick={onClose} src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/close-icon.png" /> |
| | | </View> |
| | | <View className='evaluate-content-title'> |
| | | <Image src={activeBad} /> |
| | | <View>“不满意,我要吐槽”</View> |
| | | <Image src={popupDisplayConfig.icon} /> |
| | | <View>{popupDisplayConfig.title}</View> |
| | | </View> |
| | | <View className='content-box'> |
| | | <View className='evaluate-problem-type'> |
| | | <View className='evaluate-problem-type-title'>遇到的问题类型:</View> |
| | | <View className='evaluate-problem-type-title'>反馈的问题类型:</View> |
| | | <View className='evaluate-problem-type-content'> |
| | | {typeOptions.map((item) => ( |
| | | <View key={item?.id} className={`evaluate-problem-type-content-item ${selectedTypeId === item?.id && 'active-tag'}`} onClick={() => setSelectedTypeId(item?.id)}>{item?.feedbackType}</View> |
| | |
| | | className='evaluate-problem-desc' |
| | | maxlength={500} |
| | | placeholderClass='placeholder-class' |
| | | placeholder='请描述您的建议或问题(可添加相关截图)' |
| | | placeholder={popupDisplayConfig.placeholder} |
| | | value={feedbackContent} |
| | | onInput={(e) => setFeedbackContent(e.detail.value)} /> |
| | | <View className='evaluate-problem-desc-count'>{feedbackContent.length}/500</View> |
| New file |
| | |
| | | import Taro from '@tarojs/taro'; |
| | | import { Image, Map, View } from '@tarojs/components'; |
| | | import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; |
| | | import type { PickupExceptionInfo } from '../pickup-exception'; |
| | | import { getDistance, getWalkingRoute } from '../../../../utils/Robotaxi-public-method'; |
| | | import './css-folder/pickup-exception-popup.less'; |
| | | |
| | | type Props = { |
| | | show?: boolean; |
| | | pickupExceptionInfo?: PickupExceptionInfo | null; |
| | | onClose?: () => void; |
| | | onConfirm?: () => void; |
| | | }; |
| | | |
| | | const POPUP_ANIMATION_DURATION_MS = 300; |
| | | const DEFAULT_MAP_SCALE = 16; |
| | | const LOCATION_REFRESH_INTERVAL_MS = 5000; |
| | | const USER_MARKER_ICON_URL = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/site-poi.png'; |
| | | const PICKUP_MARKER_ICON_URL = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/start-icon.png'; |
| | | const VEHICLE_MARKER_ICON_URL = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/marker-car.png'; |
| | | |
| | | type LocationPoint = { |
| | | latitude: number; |
| | | longitude: number; |
| | | }; |
| | | |
| | | /** |
| | | * 接驾异常提醒弹窗。 |
| | | */ |
| | | export default function PickupExceptionPopup({ |
| | | show = false, |
| | | pickupExceptionInfo = null, |
| | | onClose, |
| | | onConfirm |
| | | }: Props) { |
| | | const [visible, setVisible] = useState(false); |
| | | const [animationData, setAnimationData] = useState<any>(); |
| | | const [currentUserLocation, setCurrentUserLocation] = useState<LocationPoint | null>(null); |
| | | const [walkingPoints, setWalkingPoints] = useState<LocationPoint[]>([]); |
| | | const [distanceToVehicle, setDistanceToVehicle] = useState<number | null>(null); |
| | | const closeTimerRef = useRef<any>(null); |
| | | const locationTimerRef = useRef<any>(null); |
| | | const shouldRender = show || visible; |
| | | |
| | | const mapCenter = useMemo(() => { |
| | | const candidatePoints: LocationPoint[] = []; |
| | | if (currentUserLocation) { |
| | | candidatePoints.push(currentUserLocation); |
| | | } |
| | | const pickupLat = Number(pickupExceptionInfo?.pickupLat); |
| | | const pickupLon = Number(pickupExceptionInfo?.pickupLon); |
| | | const vehicleLat = Number(pickupExceptionInfo?.vehicleLat); |
| | | const vehicleLon = Number(pickupExceptionInfo?.vehicleLon); |
| | | const hasPickup = Number.isFinite(pickupLat) && Number.isFinite(pickupLon); |
| | | const hasVehicle = Number.isFinite(vehicleLat) && Number.isFinite(vehicleLon); |
| | | if (hasPickup) { |
| | | candidatePoints.push({ |
| | | latitude: pickupLat, |
| | | longitude: pickupLon |
| | | }); |
| | | } |
| | | if (hasVehicle) { |
| | | candidatePoints.push({ |
| | | latitude: vehicleLat, |
| | | longitude: vehicleLon |
| | | }); |
| | | } |
| | | if (Array.isArray(walkingPoints) && walkingPoints.length > 0) { |
| | | candidatePoints.push(...walkingPoints); |
| | | } |
| | | if (candidatePoints.length === 0) { |
| | | return null; |
| | | } |
| | | const total = candidatePoints.reduce((accumulator, point) => ({ |
| | | latitude: accumulator.latitude + point.latitude, |
| | | longitude: accumulator.longitude + point.longitude |
| | | }), { |
| | | latitude: 0, |
| | | longitude: 0 |
| | | }); |
| | | return { |
| | | latitude: total.latitude / candidatePoints.length, |
| | | longitude: total.longitude / candidatePoints.length |
| | | }; |
| | | }, [ |
| | | currentUserLocation, |
| | | walkingPoints, |
| | | pickupExceptionInfo?.pickupLat, |
| | | pickupExceptionInfo?.pickupLon, |
| | | pickupExceptionInfo?.vehicleLat, |
| | | pickupExceptionInfo?.vehicleLon |
| | | ]); |
| | | |
| | | const markers = useMemo(() => { |
| | | const nextMarkers: any[] = []; |
| | | if (currentUserLocation) { |
| | | nextMarkers.push({ |
| | | id: 1, |
| | | latitude: currentUserLocation.latitude, |
| | | longitude: currentUserLocation.longitude, |
| | | iconPath: USER_MARKER_ICON_URL, |
| | | width: 18, |
| | | height: 18, |
| | | anchor: { x: 0.5, y: 0.5 } |
| | | }); |
| | | } |
| | | if (Number.isFinite(Number(pickupExceptionInfo?.pickupLat)) && Number.isFinite(Number(pickupExceptionInfo?.pickupLon))) { |
| | | nextMarkers.push({ |
| | | id: 2, |
| | | latitude: Number(pickupExceptionInfo?.pickupLat), |
| | | longitude: Number(pickupExceptionInfo?.pickupLon), |
| | | iconPath: PICKUP_MARKER_ICON_URL, |
| | | width: 21, |
| | | height: 28, |
| | | anchor: { x: 0.5, y: 1 } |
| | | }); |
| | | } |
| | | if (Number.isFinite(Number(pickupExceptionInfo?.vehicleLat)) && Number.isFinite(Number(pickupExceptionInfo?.vehicleLon))) { |
| | | nextMarkers.push({ |
| | | id: 3, |
| | | latitude: Number(pickupExceptionInfo?.vehicleLat), |
| | | longitude: Number(pickupExceptionInfo?.vehicleLon), |
| | | iconPath: VEHICLE_MARKER_ICON_URL, |
| | | width: 20, |
| | | height: 40, |
| | | anchor: { x: 0.5, y: 0.5 } |
| | | }); |
| | | } |
| | | return nextMarkers; |
| | | }, [ |
| | | currentUserLocation, |
| | | pickupExceptionInfo?.pickupLat, |
| | | pickupExceptionInfo?.pickupLon, |
| | | pickupExceptionInfo?.vehicleLat, |
| | | pickupExceptionInfo?.vehicleLon |
| | | ]); |
| | | |
| | | const polyline = useMemo(() => { |
| | | const nextPolylines: any[] = []; |
| | | if (Array.isArray(walkingPoints) && walkingPoints.length > 1) { |
| | | nextPolylines.push({ |
| | | points: walkingPoints, |
| | | color: '#2f7cff', |
| | | width: 6, |
| | | dottedLine: false, |
| | | arrowLine: true |
| | | }); |
| | | } else if ( |
| | | currentUserLocation |
| | | && Number.isFinite(Number(pickupExceptionInfo?.vehicleLat)) |
| | | && Number.isFinite(Number(pickupExceptionInfo?.vehicleLon)) |
| | | ) { |
| | | nextPolylines.push({ |
| | | points: [ |
| | | currentUserLocation, |
| | | { |
| | | latitude: Number(pickupExceptionInfo?.vehicleLat), |
| | | longitude: Number(pickupExceptionInfo?.vehicleLon) |
| | | } |
| | | ], |
| | | color: '#2f7cff', |
| | | width: 6, |
| | | dottedLine: true |
| | | }); |
| | | } |
| | | return nextPolylines; |
| | | }, [ |
| | | currentUserLocation, |
| | | walkingPoints, |
| | | pickupExceptionInfo?.pickupLat, |
| | | pickupExceptionInfo?.pickupLon, |
| | | pickupExceptionInfo?.vehicleLat, |
| | | pickupExceptionInfo?.vehicleLon |
| | | ]); |
| | | |
| | | /** |
| | | * 拉取当前位置、实时距离和步行路线。 |
| | | */ |
| | | const syncLiveLocationAndWalkingRoute = useCallback(async () => { |
| | | const vehicleLatitude = Number(pickupExceptionInfo?.vehicleLat); |
| | | const vehicleLongitude = Number(pickupExceptionInfo?.vehicleLon); |
| | | const hasValidVehiclePoint = Number.isFinite(vehicleLatitude) && Number.isFinite(vehicleLongitude); |
| | | if (!hasValidVehiclePoint) { |
| | | setCurrentUserLocation(null); |
| | | setWalkingPoints([]); |
| | | setDistanceToVehicle(null); |
| | | return; |
| | | } |
| | | |
| | | try { |
| | | const location = await Taro.getLocation({ |
| | | type: 'gcj02', |
| | | isHighAccuracy: true, |
| | | highAccuracyExpireTime: LOCATION_REFRESH_INTERVAL_MS |
| | | } as any); |
| | | const currentLatitude = Number(location?.latitude); |
| | | const currentLongitude = Number(location?.longitude); |
| | | if (!Number.isFinite(currentLatitude) || !Number.isFinite(currentLongitude)) { |
| | | return; |
| | | } |
| | | |
| | | const nextUserLocation = { |
| | | latitude: currentLatitude, |
| | | longitude: currentLongitude |
| | | }; |
| | | setCurrentUserLocation(nextUserLocation); |
| | | setDistanceToVehicle(Math.round(getDistance( |
| | | currentLatitude, |
| | | currentLongitude, |
| | | vehicleLatitude, |
| | | vehicleLongitude |
| | | ))); |
| | | |
| | | try { |
| | | const { points } = await getWalkingRoute( |
| | | `${currentLongitude},${currentLatitude}`, |
| | | `${vehicleLongitude},${vehicleLatitude}` |
| | | ); |
| | | if (Array.isArray(points) && points.length > 1) { |
| | | setWalkingPoints(points.map((point: any) => ({ |
| | | latitude: Number(point.latitude), |
| | | longitude: Number(point.longitude) |
| | | })).filter((point: LocationPoint) => Number.isFinite(point.latitude) && Number.isFinite(point.longitude))); |
| | | return; |
| | | } |
| | | } catch (error) { |
| | | } |
| | | |
| | | setWalkingPoints([ |
| | | nextUserLocation, |
| | | { |
| | | latitude: vehicleLatitude, |
| | | longitude: vehicleLongitude |
| | | } |
| | | ]); |
| | | } catch (error) { |
| | | } |
| | | }, [pickupExceptionInfo?.vehicleLat, pickupExceptionInfo?.vehicleLon]); |
| | | |
| | | useEffect(() => { |
| | | if (closeTimerRef.current) { |
| | | clearTimeout(closeTimerRef.current); |
| | | closeTimerRef.current = null; |
| | | } |
| | | |
| | | if (show) { |
| | | setVisible(true); |
| | | setAnimationData(undefined); |
| | | return; |
| | | } |
| | | |
| | | if (!visible) { |
| | | return; |
| | | } |
| | | |
| | | const animation = Taro.createAnimation({ |
| | | duration: POPUP_ANIMATION_DURATION_MS, |
| | | timingFunction: 'ease-in' |
| | | }); |
| | | animation.translateY(500).step(); |
| | | setAnimationData(animation.export()); |
| | | closeTimerRef.current = setTimeout(() => { |
| | | setVisible(false); |
| | | setAnimationData(undefined); |
| | | closeTimerRef.current = null; |
| | | }, POPUP_ANIMATION_DURATION_MS); |
| | | |
| | | return () => { |
| | | if (closeTimerRef.current) { |
| | | clearTimeout(closeTimerRef.current); |
| | | closeTimerRef.current = null; |
| | | } |
| | | }; |
| | | }, [show, visible]); |
| | | |
| | | useEffect(() => { |
| | | if (!show) { |
| | | if (locationTimerRef.current) { |
| | | clearInterval(locationTimerRef.current); |
| | | locationTimerRef.current = null; |
| | | } |
| | | setCurrentUserLocation(null); |
| | | setWalkingPoints([]); |
| | | setDistanceToVehicle(null); |
| | | return; |
| | | } |
| | | |
| | | syncLiveLocationAndWalkingRoute(); |
| | | locationTimerRef.current = setInterval(() => { |
| | | syncLiveLocationAndWalkingRoute(); |
| | | }, LOCATION_REFRESH_INTERVAL_MS); |
| | | |
| | | return () => { |
| | | if (locationTimerRef.current) { |
| | | clearInterval(locationTimerRef.current); |
| | | locationTimerRef.current = null; |
| | | } |
| | | }; |
| | | }, [show, syncLiveLocationAndWalkingRoute]); |
| | | |
| | | if (!shouldRender) { |
| | | return null; |
| | | } |
| | | |
| | | return ( |
| | | <View className='pickup-exception-popup' catchMove> |
| | | <View className='pickup-exception-mask'></View> |
| | | <View className='pickup-exception-content' animation={show ? undefined : animationData}> |
| | | <View className='pickup-exception-close' onClick={onClose}> |
| | | <Image |
| | | className='pickup-exception-close-icon' |
| | | src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/close-icon.png' |
| | | /> |
| | | </View> |
| | | {mapCenter && ( |
| | | <View className='pickup-exception-map-wrapper'> |
| | | <Map |
| | | className='pickup-exception-map' |
| | | longitude={mapCenter.longitude} |
| | | latitude={mapCenter.latitude} |
| | | scale={DEFAULT_MAP_SCALE} |
| | | markers={markers} |
| | | polyline={polyline} |
| | | includePoints={markers.map((marker) => ({ |
| | | latitude: marker.latitude, |
| | | longitude: marker.longitude |
| | | }))} |
| | | enableScroll={false} |
| | | enableZoom={false} |
| | | enableRotate={false} |
| | | enableSatellite={false} |
| | | enableTraffic={false} |
| | | showLocation={false} |
| | | /> |
| | | {typeof distanceToVehicle === 'number' && ( |
| | | <View className='pickup-exception-distance'> |
| | | 当前距离车辆位置 {distanceToVehicle} 米 |
| | | </View> |
| | | )} |
| | | </View> |
| | | )} |
| | | <View className='pickup-exception-message'> |
| | | {pickupExceptionInfo?.message || '为确保行车安全,已为您调整新的上车点,请步行导航至车辆位置'} |
| | | </View> |
| | | <View className='pickup-exception-confirm' onClick={onConfirm}> |
| | | 开始步行导航 |
| | | </View> |
| | | </View> |
| | | </View> |
| | | ); |
| | | } |
| | |
| | | import { SignedOrderPaymentValue } from '../../../api/types/begin-route'; |
| | | import type { OrderDistanceConfig } from './use-order-distance-config'; |
| | | import { TripStatus } from '../constants/trip-status'; |
| | | |
| | | /** 下单防抖时长,单位毫秒。 */ |
| | |
| | | selectedRouteIndex?: number; |
| | | /** 路线错误标记。 */ |
| | | routeAndFareError: boolean; |
| | | /** 下单配置。 */ |
| | | distanceConfig?: OrderDistanceConfig; |
| | | /** 下单提交中。 */ |
| | | callSubmitting: boolean; |
| | | /** 取消类型。 */ |
| | |
| | | setShowOverRoutePopup: (value: boolean) => void; |
| | | /** 打开协议弹窗。 */ |
| | | handleOpenPrivacyPopup: () => void; |
| | | /** 打开免密支付开通提醒弹窗。 */ |
| | | handleOpenCreditPayPopup: () => Promise<boolean>; |
| | | /** 距离与城市校验。 */ |
| | | checkDistanceAndCityReady: (startSite: any) => Promise<number | null>; |
| | | /** 绑定 socket 监听。 */ |
| | |
| | | ORDER_DISTANCE_TIP_TYPE_MIN_DISTANCE |
| | | } from '../../../constants/order-distance'; |
| | | |
| | | type OrderDistanceConfig = { |
| | | export type OrderDistanceConfig = { |
| | | maxDistance?: number; |
| | | minDistance?: number; |
| | | adsorptionDistance?: number; |
| | | queuePollingTime?: number; |
| | | tripShareStatus?: number; |
| | | evaluatePopupStatus?: number; |
| | | creditPayNeedConfirm?: number; |
| | | walkingNavigationDistance?: number; |
| | | maxStandingTiming?: number; |
| | | xczSyDistance?: number; |
| | |
| | | minDistance: Number(res.minDistance), |
| | | adsorptionDistance: Number(res.adsorptionDistance), |
| | | queuePollingTime: Number(res.queuePollingTime), |
| | | tripShareStatus: Number(res.tripShareStatus), |
| | | evaluatePopupStatus: Number(res.evaluatePopupStatus), |
| | | creditPayNeedConfirm: Number(res.creditPayNeedConfirm), |
| | | walkingNavigationDistance: Number(res.walkingNavigationDistance), |
| | | maxStandingTiming: Number(res.maxStandingTiming), |
| | | xczSyDistance: Number(res.xczSyDistance), |
| | |
| | | return; |
| | | } |
| | | paySubmittingRef.current = true; |
| | | Taro.showLoading({ |
| | | title: '支付中', |
| | | 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; |
| | |
| | | // return; |
| | | // } |
| | | |
| | | // Taro.showToast({ title: '暂不支持当前支付方式', icon: 'none' }); |
| | | Taro.showToast({ title: '暂不支持当前支付方式', icon: 'none' }); |
| | | |
| | | } catch (error) { |
| | | Taro.showToast({ title: '支付失败,请重试', icon: 'none' }); |
| | | } finally { |
| | | Taro.hideLoading(); |
| | | paySubmittingRef.current = false; |
| | | } |
| | | }; |
| | |
| | | import Taro from '@tarojs/taro'; |
| | | import { useCallback, useRef } from 'react'; |
| | | import { formatDateTime, debounce, reportEvent } from '../../../../utils/Robotaxi-public-method'; |
| | | import { saveOrder, routeStaleCheck } from '../../../api/begin-route'; |
| | | import { createCreditPayOrder, saveOrder, routeStaleCheck } 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'; |
| | | import { requestOrderSuccessSubscribeOnTap, subscribeAfterOrderSuccess } from '../../../../utils/subscribe'; |
| | | import { |
| | | requestOrderSuccessSubscribeOnTap, |
| | | subscribeAfterOrderSuccess, |
| | | waitForOrderSuccessSubscribePopup |
| | | } from '../../../../utils/subscribe'; |
| | | import { TripStatus } from '../../../constants/trip-status'; |
| | | import { |
| | | CONFIRM_RIDE_EXPIRE_BUTTON_EVENT_ID, |
| | |
| | | type SubmitOrderResult = { |
| | | /** 新订单 ID。 */ |
| | | orderId: string; |
| | | /** 支付分确认页拉起参数。 */ |
| | | creditPayPackageValue?: string; |
| | | }; |
| | | |
| | | /** 下单 Hook 返回值。 */ |
| | |
| | | return 'network_error'; |
| | | }; |
| | | |
| | | type SubmitOrderFlowOptions = { |
| | | /** 是否跳过下单前的免密状态校验。 */ |
| | | skipCreditPayCheck?: boolean; |
| | | /** 是否跳过下单起始埋点。 */ |
| | | skipPayStartReport?: boolean; |
| | | }; |
| | | |
| | | type CreditPayCheckResult = 'normal_submit' | 'credit_pay_submit' | 'blocked'; |
| | | |
| | | /** |
| | | * 管理下单前校验与提交动作。 |
| | | */ |
| | | export default function useOrderSubmit(deps: OrderActionsDeps): UseOrderSubmitResult { |
| | | const waitingCreditPayReturnRef = useRef(false); // 是否正在等待微信支付分签约页返回 |
| | | const submitOrderFlowRef = useRef<(options?: SubmitOrderFlowOptions) => Promise<void>>(); // 始终指向最新的下单流程 |
| | | |
| | | /** |
| | | * 结束下单中状态。 |
| | | */ |
| | |
| | | }; |
| | | |
| | | /** |
| | | * 上报下单埋点。 |
| | | */ |
| | | const reportOrderSubmitEvent = (resolvedCouponId: string, hasOrderId: boolean) => { |
| | | reportEvent(ORDER_SUBMIT_EVENT_ID, { |
| | | total_amount: deps.fareEstimate?.originalFare, |
| | | payable_amount: deps.fareEstimate?.finalFare, |
| | | coupon_id: resolvedCouponId, |
| | | status_code: hasOrderId ? 1 : 0 |
| | | }); |
| | | }; |
| | | |
| | | /** |
| | | * 刷新路线并终止本次下单。 |
| | | */ |
| | | const refreshRouteAndStopSubmitting = (startSite: any, message: string) => { |
| | |
| | | }; |
| | | |
| | | /** |
| | | * 提交订单。 |
| | | * 统一提取接口返回中的订单 ID。 |
| | | */ |
| | | const submitOrder = async (startSite: any, openCityId: number): Promise<SubmitOrderResult | null> => { |
| | | const waitingTimeMinutes = Number(deps.fareEstimate?.waitingTime || 0); |
| | | const estimatedTimestamp = Date.now() + waitingTimeMinutes * MILLISECONDS_PER_MINUTE; |
| | | const estimatedTime = formatDateTime(new Date(estimatedTimestamp)); |
| | | const selectedRoute = Array.isArray(deps.routeOptions) && typeof deps.selectedRouteIndex === 'number' |
| | | ? deps.routeOptions[deps.selectedRouteIndex] |
| | | : null; |
| | | const resolvedCouponId = resolveSubmitCouponId(selectedRoute); |
| | | const payload: any = { |
| | | endPointId: deps.endSite.id, |
| | | openCityId, |
| | | placementAddress: startSite?.name, |
| | | placementLat: startSite?.latitude, |
| | | placementLon: startSite?.longitude, |
| | | startPointId: startSite?.id, |
| | | xpElectronicFenceAreaId: startSite?.electronicAreaId || deps.endSite?.electronicAreaId || '', |
| | | estimatedTime, |
| | | }; |
| | | const resolveResponseOrderId = (response: any) => String( |
| | | response?.orderId |
| | | || response?.data?.orderId |
| | | || response?.data?.businessOrderId |
| | | || '' |
| | | ).trim(); |
| | | |
| | | if (selectedRoute?.route?.routeID) { |
| | | payload.selectedRouteId = selectedRoute.route.routeID; |
| | | payload.navigationSnapshotId = selectedRoute.snapshotId; |
| | | } |
| | | /** |
| | | * 提取信用下单返回的支付分 package 参数。 |
| | | */ |
| | | const resolveCreditPayPackageValue = (response: any) => String( |
| | | response?.data?.creditPayValue?.package |
| | | || response?.creditPayValue?.package |
| | | || '' |
| | | ).trim(); |
| | | |
| | | if (resolvedCouponId) { |
| | | payload.userCouponId = resolvedCouponId; |
| | | } |
| | | |
| | | const response = await saveOrder(payload); |
| | | const statusCode = response?.orderId ? 1 : 0; |
| | | reportEvent(ORDER_SUBMIT_EVENT_ID, { |
| | | total_amount: deps.fareEstimate.originalFare, |
| | | payable_amount: deps.fareEstimate.finalFare, |
| | | coupon_id: resolvedCouponId, |
| | | status_code: statusCode |
| | | }); |
| | | /** |
| | | * 统一处理下单接口响应,兼容普通下单与信用下单。 |
| | | */ |
| | | const resolveSubmitOrderResult = (response: any, resolvedCouponId: string): SubmitOrderResult | null => { |
| | | const resolvedOrderId = resolveResponseOrderId(response); |
| | | reportOrderSubmitEvent(resolvedCouponId, Boolean(resolvedOrderId)); |
| | | |
| | | if ( |
| | | isNotAgreedMessage(response) |
| | |
| | | return null; |
| | | } |
| | | |
| | | if (!response?.orderId) { |
| | | if (!resolvedOrderId) { |
| | | reportPayResultEvent(resolvePayResultStatus(response?.msg || response?.data || response)); |
| | | Taro.showToast({ title: '下单失败,请重试', icon: 'none' }); |
| | | return null; |
| | | } |
| | | |
| | | reportPayResultEvent('success', response.orderId); |
| | | reportPayResultEvent('success', resolvedOrderId); |
| | | resetConfirmRideSelectionContext(); |
| | | Taro.removeStorageSync('selectedCouponId'); |
| | | Taro.setStorageSync('orderId', response.orderId); |
| | | return { orderId: response.orderId }; |
| | | Taro.setStorageSync('orderId', resolvedOrderId); |
| | | return { |
| | | orderId: resolvedOrderId, |
| | | creditPayPackageValue: resolveCreditPayPackageValue(response) |
| | | }; |
| | | }; |
| | | |
| | | /** |
| | | * 构建普通下单与信用下单共用的请求参数。 |
| | | */ |
| | | const buildSubmitOrderPayload = (startSite: any, openCityId: number, selectedRoute: any) => { |
| | | const waitingTimeMinutes = Number(deps.fareEstimate?.waitingTime || 0); |
| | | const estimatedTimestamp = Date.now() + waitingTimeMinutes * MILLISECONDS_PER_MINUTE; |
| | | const estimatedTime = formatDateTime(new Date(estimatedTimestamp)); |
| | | const resolvedCouponId = resolveSubmitCouponId(selectedRoute); |
| | | const payload: SaveOrderPayload = { |
| | | endPointId: deps.endSite.id, |
| | | openCityId, |
| | | placementAddress: startSite?.name, |
| | | placementLat: startSite?.latitude, |
| | | placementLon: startSite?.longitude, |
| | | startPointId: startSite?.id, |
| | | xpElectronicFenceAreaId: startSite?.electronicAreaId || deps.endSite?.electronicAreaId || '', |
| | | estimatedTime, |
| | | }; |
| | | |
| | | if (selectedRoute?.route?.routeID) { |
| | | payload.selectedRouteId = selectedRoute.route.routeID; |
| | | payload.navigationSnapshotId = selectedRoute.snapshotId; |
| | | } |
| | | |
| | | if (resolvedCouponId) { |
| | | payload.userCouponId = resolvedCouponId; |
| | | } |
| | | |
| | | return { |
| | | payload, |
| | | resolvedCouponId |
| | | }; |
| | | }; |
| | | |
| | | /** |
| | | * 提交订单。 |
| | | */ |
| | | const submitOrder = async (startSite: any, openCityId: number): Promise<SubmitOrderResult | null> => { |
| | | const selectedRoute = Array.isArray(deps.routeOptions) && typeof deps.selectedRouteIndex === 'number' |
| | | ? deps.routeOptions[deps.selectedRouteIndex] |
| | | : null; |
| | | const { payload, resolvedCouponId } = buildSubmitOrderPayload(startSite, openCityId, selectedRoute); |
| | | const response = await saveOrder(payload); |
| | | return resolveSubmitOrderResult(response, resolvedCouponId); |
| | | }; |
| | | |
| | | /** |
| | | * 命中配置后直接走信用下单,不再调用普通下单接口。 |
| | | */ |
| | | const submitCreditPayOrder = async (startSite: any, openCityId: number, selectedRoute: any): Promise<SubmitOrderResult | null> => { |
| | | const { payload, resolvedCouponId } = buildSubmitOrderPayload(startSite, openCityId, selectedRoute); |
| | | const response = await createCreditPayOrder(payload); |
| | | return resolveSubmitOrderResult(response, resolvedCouponId); |
| | | }; |
| | | |
| | | /** |
| | |
| | | }; |
| | | |
| | | /** |
| | | * 继续执行签约页返回后的下单流程。 |
| | | */ |
| | | 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 |
| | | }); |
| | | |
| | | /** |
| | | * 下单前校验免密状态。 |
| | | */ |
| | | const checkCreditPayReadyBeforeOrder = useCallback(async () => { |
| | | try { |
| | | const hasEnabledCreditPay = await getPayPwd(); |
| | | if (hasEnabledCreditPay === true) { |
| | | return 'normal_submit' as CreditPayCheckResult; |
| | | } |
| | | |
| | | const isCanceled = await deps.handleOpenCreditPayPopup(); |
| | | if (isCanceled) { |
| | | // 用户取消开通,继续下单 |
| | | return 'normal_submit' 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(); |
| | | if (!signStr) { |
| | | // 获取免密签约信息失败,降级为继续下单 |
| | | return 'normal_submit' as CreditPayCheckResult; |
| | | } |
| | | |
| | | // 先记录等待状态,避免拉起失败回调过快时丢失后续续单逻辑。 |
| | | waitingCreditPayReturnRef.current = true; |
| | | const launched = openCreditPaySign(signStr); |
| | | if (!launched) { |
| | | waitingCreditPayReturnRef.current = false; |
| | | // 拉起免密开通失败,降级为继续下单 |
| | | return 'normal_submit' as CreditPayCheckResult; |
| | | } |
| | | |
| | | // 等待微信签约页返回后再恢复下单,避免当前流程继续往下执行。 |
| | | stopCallSubmitting(); |
| | | return 'blocked' as CreditPayCheckResult; |
| | | } catch (e) { |
| | | // 免密状态校验失败,降级为继续下单 |
| | | return 'normal_submit' as CreditPayCheckResult; |
| | | } |
| | | } catch (error) { |
| | | return 'normal_submit' as CreditPayCheckResult; |
| | | } |
| | | }, [openCreditPaySign, deps]); |
| | | |
| | | /** |
| | | * 执行下单流程。 |
| | | */ |
| | | const submitOrderFlow = async () => { |
| | | const submitOrderFlow = async (options: SubmitOrderFlowOptions = {}) => { |
| | | if (deps.callSubmitting) { |
| | | return; |
| | | } |
| | | deps.setCallSubmitting(true); |
| | | reportPayStartEvent(); |
| | | if (!options.skipPayStartReport) { |
| | | reportPayStartEvent(); |
| | | } |
| | | |
| | | const authReady = await checkCallAuth(); |
| | | if (!authReady) { |
| | |
| | | return; |
| | | } |
| | | |
| | | let submitMode: CreditPayCheckResult = 'normal_submit'; |
| | | if (!options.skipCreditPayCheck) { |
| | | submitMode = await checkCreditPayReadyBeforeOrder(); |
| | | if (submitMode === 'blocked') { |
| | | return; |
| | | } |
| | | } |
| | | |
| | | requestOrderSuccessSubscribeOnTap(); |
| | | await waitForOrderSuccessSubscribePopup(); |
| | | |
| | | try { |
| | | const submitResult = await submitOrder(startSite, openCityId); |
| | | const submitResult = submitMode === 'credit_pay_submit' |
| | | ? await submitCreditPayOrder(startSite, openCityId, selectedRoute) |
| | | : await submitOrder(startSite, openCityId); |
| | | if (!submitResult?.orderId) { |
| | | stopCallSubmitting(); |
| | | return; |
| | |
| | | deps.setTripStatus(TripStatus.Publishing); |
| | | deps.bindSocketListeners(); |
| | | void subscribeAfterOrderSuccess(submitResult.orderId); |
| | | if (submitMode === 'credit_pay_submit') { |
| | | const creditPayPackageValue = String(submitResult.creditPayPackageValue || '').trim(); |
| | | if (!creditPayPackageValue) { |
| | | Taro.showToast({ title: '支付参数缺失,请稍后重试', icon: 'none' }); |
| | | return; |
| | | } |
| | | // 复用现有支付分确认页拉起与回跳查询链路。 |
| | | void deps.handleSignedCreditPayment({ |
| | | businessOrderId: '', |
| | | authOrderId: '', |
| | | channelOrderId: '', |
| | | payType: 13, |
| | | packageValue: creditPayPackageValue |
| | | }); |
| | | } |
| | | } catch (error) { |
| | | reportPayResultEvent(resolvePayResultStatus((error as any)?.msg || (error as any)?.message || error)); |
| | | stopCallSubmitting(); |
| | | } |
| | | }; |
| | | submitOrderFlowRef.current = submitOrderFlow; |
| | | |
| | | /** |
| | | * 防抖下单入口。 |
| | |
| | | const callImmediatelyWithDebounce = debounce(submitOrderFlow, ORDER_SUBMIT_DEBOUNCE_MS); |
| | | |
| | | /** |
| | | * 呼叫入口:先在点击手势阶段预拉起订阅,再进入防抖后的下单流程。 |
| | | * 呼叫入口:进入防抖后的下单流程。 |
| | | */ |
| | | const callImmediately = () => { |
| | | requestOrderSuccessSubscribeOnTap(); |
| | | callImmediatelyWithDebounce(); |
| | | }; |
| | | |
| | |
| | | evaluateSubmitVersion: number; |
| | | /** 评价关闭版本 */ |
| | | evaluateCloseVersion: number; |
| | | /** 评价弹窗开关配置 */ |
| | | evaluatePopupStatus: number; |
| | | /** 打开结束弹窗 */ |
| | | handleOpenOverRoutePopup: () => void; |
| | | /** 支付 */ |
| | | handlePay: () => void; |
| | | /** 打开评价 */ |
| | | handleOpenEvaluatePopup: () => void; |
| | | handleOpenEvaluatePopup: (orderRating: number) => void; |
| | | /** 详情展开回调 */ |
| | | handleToggleDetailFade: (expanded: boolean, hasFare: boolean) => void; |
| | | /** 修改目的地 */ |
| | | handleModifyDestination: () => void; |
| | | /** 是否展示修改目的地 */ |
| | | showModifyDestination: boolean; |
| | | /** 是否展示分享行程 */ |
| | | showShareTrip: boolean; |
| | | /** 分享行程 */ |
| | | handleShareTrip: () => void; |
| | | /** 路线选择展开状态 */ |
| | |
| | | handleCancelTrip={() => deps.handleOpenCancelPopupByTripStatus(deps.tripStatus, true)} |
| | | handleModifyDestination={deps.handleModifyDestination} |
| | | showModifyDestination={deps.showModifyDestination} |
| | | showShareTrip={deps.showShareTrip} |
| | | handleShareTrip={deps.handleShareTrip} |
| | | resetDetailVersion={deps.resetDetailVersion} |
| | | waitMinutes={waitMinutes} |
| | |
| | | fareBreakdown={fareBreakdown} |
| | | evaluateSubmitVersion={deps.evaluateSubmitVersion} |
| | | evaluateCloseVersion={deps.evaluateCloseVersion} |
| | | evaluatePopupStatus={deps.evaluatePopupStatus} |
| | | handleOpenOverRoutePopup={deps.handleOpenOverRoutePopup} |
| | | handlePay={deps.handlePay} |
| | | onOpenCouponSelect={deps.handleOpenCouponSelect} |
| | |
| | | import { type WebSocketDepsRef } from './web-socket-handlers/web-socket-order-guard'; |
| | | import { TripStatus } from '../../../constants/trip-status'; |
| | | import { type PendingCancelContext } from './use-order-actions.shared'; |
| | | import type { PickupExceptionInfo } from '../pickup-exception'; |
| | | |
| | | export type UseWebSocketEventsDeps = { |
| | | tripStatusRef: React.MutableRefObject<TripStatus>; |
| | |
| | | setCancelType: (value: number) => void; |
| | | setShowCancelPopup: (value: boolean) => void; |
| | | checkAndShowPassengerNotice: () => void; |
| | | handlePickupException?: (pickupException: PickupExceptionInfo | null) => void; |
| | | setVehicleDirection: (value: number) => void; |
| | | lastSocketDirectionAtRef?: React.MutableRefObject<number>; |
| | | setVehicleMoveAlongPoints: (points: any[]) => void; |
| | |
| | | import { PENDING_CANCEL_CONTEXT_TTL_MS, type PendingCancelContext } from '../use-order-actions.shared'; |
| | | import type { UseWebSocketEventsDeps } from '../use-web-socket-events'; |
| | | import { isSameOrder, resolveCurrentOrderId, type WebSocketDepsRef } from './web-socket-order-guard'; |
| | | import { normalizePickupException } from '../../pickup-exception'; |
| | | |
| | | /** |
| | | * 解析 payload 中用于同步订单的有效 orderId。 |
| | |
| | | reportTripInProgressEventIfNeeded(effectiveTripStatus, changeStatusOld, effectiveOrderId, currentDeps); |
| | | reportTripCompleteEventIfNeeded(effectiveTripStatus, changeStatusOld, effectiveOrderId, currentDeps); |
| | | handleStatusSideEffects(effectiveTripStatus, changeStatusOld, currentDeps); |
| | | currentDeps.handlePickupException?.(normalizePickupException(payload?.pickupException || payload?.content?.pickupException)); |
| | | handleCancelPopupPush(payload, changeStatusOld, currentDeps, pendingCancelContext); |
| | | if (payload?.showPassengerNotice) { |
| | | currentDeps.checkAndShowPassengerNotice(); |
| | |
| | | import { BeginRouteViewProvider } from './components/begin-route-view-context'; |
| | | // 修改目的地失败弹窗 |
| | | import ModifyDestinationFailPopup from './components/modify-destination-fail-popup'; |
| | | import PickupExceptionPopup from './components/pickup-exception-popup'; |
| | | import CreditPayPopup from './components/credit-pay-popup'; |
| | | import CouponSelectPopup from '../../components/coupon-select-popup'; |
| | | // 弹窗状态管理 |
| | | import usePopupManager from './hooks/use-popup-manager'; |
| | |
| | | resetConfirmRideSelectionContext |
| | | } from '../../constants/confirm-ride-report'; |
| | | import { withRobotaxiWhitelist } from '../../../utils/whitelist'; |
| | | import type { PickupExceptionInfo } from './pickup-exception'; |
| | | |
| | | const ROUTE_SUMMARY_MARKER_ID = 999001; |
| | | const MODIFY_DESTINATION_PICKUP_STATUS_GUARD_MS = 5000; |
| | |
| | | routeDuration: number | null; |
| | | orderRouteMetrics: OrderRouteMetrics; |
| | | }; |
| | | |
| | | /** |
| | | * 生成接驾异常签名,避免同一异常消息重复弹窗。 |
| | | */ |
| | | function createPickupExceptionSignature(pickupException: PickupExceptionInfo | null) { |
| | | if (!pickupException) { |
| | | return ''; |
| | | } |
| | | return [ |
| | | pickupException.type ?? '', |
| | | pickupException.vehicleLon ?? '', |
| | | pickupException.vehicleLat ?? '', |
| | | pickupException.pickupLon ?? '', |
| | | pickupException.pickupLat ?? '', |
| | | pickupException.offsetDistance ?? '', |
| | | pickupException.freeCancelDeadline || '', |
| | | pickupException.message || '' |
| | | ].join('|'); |
| | | } |
| | | |
| | | /** |
| | | * 生成确认行程类埋点公共参数。 |
| | |
| | | handleOpenPassengerNotice, |
| | | showEvaluatePopup, |
| | | setShowEvaluatePopup, |
| | | handleOpenEvaluatePopup, |
| | | handleOpenEvaluatePopup: _handleOpenEvaluatePopup, |
| | | showTipPopup, |
| | | setShowTipPopup, |
| | | handleCloseTipPopup, |
| | |
| | | const [priceUpdateVersion, setPriceUpdateVersion] = useState(0); // 价格更新版本号 |
| | | const [evaluateSubmitVersion, setEvaluateSubmitVersion] = useState(0); // 评价提交版本号 |
| | | const [evaluateCloseVersion, setEvaluateCloseVersion] = useState(0); // 评价关闭版本号 |
| | | const [evaluateOrderRating, setEvaluateOrderRating] = useState(1); // 当前评价弹窗对应评分 |
| | | const [showCouponSelectPopup, setShowCouponSelectPopup] = useState(false); // 优惠券选择弹窗展示状态 |
| | | const [orderDetailViewportRefreshVersion, setOrderDetailViewportRefreshVersion] = useState(0); // 完成态视口刷新版本,关闭优惠券弹窗后触发 includePoints 纠偏 |
| | | const [showModifyDestinationFailPopup, setShowModifyDestinationFailPopup] = useState(false); // 修改目的地失败弹窗 |
| | | const [modifyDestinationFailMessage, setModifyDestinationFailMessage] = useState(''); // 修改目的地失败文案(默认空,避免闪文案) |
| | | const [pickupExceptionInfo, setPickupExceptionInfo] = useState<PickupExceptionInfo | null>(null); // 接驾异常信息 |
| | | const [showPickupExceptionPopup, setShowPickupExceptionPopup] = useState(false); // 接驾异常弹窗展示状态 |
| | | const [showCreditPayPopup, setShowCreditPayPopup] = useState(false); // 免密支付开通弹窗 |
| | | const creditPayResolverRef = useRef<((value: boolean) => void) | null>(null); // 免密弹窗的 Promise resolve 回调 |
| | | const [orderRouteMetrics, setOrderRouteMetrics] = useState<OrderRouteMetrics>({ |
| | | distanceMeters: null, |
| | | durationSeconds: null |
| | |
| | | const lastPreviewPageViewDidShowVersionRef = useRef(0); // 确认行程预览态页面浏览埋点最近一次上报的页面展示版本 |
| | | const lastWaitingForDispatchPageViewOrderIdRef = useRef(''); // 等待派单页页面浏览埋点最近一次上报的订单 ID |
| | | const lastWaitingForPickupPageViewOrderIdRef = useRef(''); // 等待接驾页页面浏览埋点最近一次上报的订单 ID |
| | | const handledPickupExceptionSignatureRef = useRef(''); // 已处理接驾异常签名,避免同一异常重复弹窗 |
| | | const previousOrderIdRef = useRef(''); // 上一个订单 ID,用于跨单清理异常弹窗状态 |
| | | const confirmRideEventSnapshotRef = useRef({ |
| | | startSite: null as any, |
| | | endSite: null as any, |
| | |
| | | const queuePollingTimeSeconds = Number.isFinite(Number(distanceConfig?.queuePollingTime)) && Number(distanceConfig?.queuePollingTime) > 0 |
| | | ? Number(distanceConfig?.queuePollingTime) |
| | | : DEFAULT_QUEUE_POLLING_TIME; // 排队位次轮询频率(秒) |
| | | const showShareTrip = Number(distanceConfig?.tripShareStatus) === 1; // 分享行程入口展示开关 |
| | | const evaluatePopupStatus = Number(distanceConfig?.evaluatePopupStatus) === 1 ? 1 : 0; // 评价弹窗开关 |
| | | const walkingNavigationDistance = Number.isFinite(Number(distanceConfig?.walkingNavigationDistance)) |
| | | ? Number(distanceConfig?.walkingNavigationDistance) |
| | | : 0; // 步行导航按钮展示阈值(米) |
| | |
| | | ensureDistanceConfig(); |
| | | }, [ensureDistanceConfig]); |
| | | |
| | | /** |
| | | * 打开免密支付弹窗。 |
| | | */ |
| | | const handleOpenCreditPayPopup = useCallback(() => { |
| | | return new Promise<boolean>((resolve) => { |
| | | creditPayResolverRef.current = resolve; |
| | | setShowCreditPayPopup(true); |
| | | }); |
| | | }, []); |
| | | |
| | | /** |
| | | * 关闭免密支付弹窗。 |
| | | */ |
| | | const handleCloseCreditPayPopup = useCallback(() => { |
| | | setShowCreditPayPopup(false); |
| | | if (creditPayResolverRef.current) { |
| | | creditPayResolverRef.current(true); // 取消相当于降级为普通下单 |
| | | creditPayResolverRef.current = null; |
| | | } |
| | | }, []); |
| | | |
| | | /** |
| | | * 确认开通免密支付。 |
| | | */ |
| | | const handleConfirmCreditPayPopup = useCallback(() => { |
| | | setShowCreditPayPopup(false); |
| | | if (creditPayResolverRef.current) { |
| | | creditPayResolverRef.current(false); // 确认开通则交由下单流程继续走签约页逻辑 |
| | | creditPayResolverRef.current = null; |
| | | } |
| | | }, []); |
| | | |
| | | /** |
| | | * 处理后端推送的接驾异常: |
| | | * 1. 保留原上车点展示,仅记录异常导航目标 |
| | | * 2. 同一异常只弹一次提醒弹窗 |
| | | */ |
| | | const handlePickupException = useCallback((nextPickupException: PickupExceptionInfo | null) => { |
| | | const isPickupTripStatus = [TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup].includes(tripStatusRef.current); |
| | | if (!nextPickupException?.enabled || nextPickupException.type !== 1 || !isPickupTripStatus) { |
| | | setPickupExceptionInfo(null); |
| | | setShowPickupExceptionPopup(false); |
| | | return; |
| | | } |
| | | |
| | | setPickupExceptionInfo(nextPickupException); |
| | | const nextSignature = createPickupExceptionSignature(nextPickupException); |
| | | if (nextSignature && handledPickupExceptionSignatureRef.current !== nextSignature) { |
| | | handledPickupExceptionSignatureRef.current = nextSignature; |
| | | setShowPickupExceptionPopup(true); |
| | | } |
| | | }, []); |
| | | |
| | | const { bindSocketListeners, ensureSocketListeners } = useWebSocketEvents({ |
| | | tripStatusRef, |
| | | pickupArrivedReportedOrderIdRef, |
| | |
| | | setCancelType, |
| | | setShowCancelPopup, |
| | | checkAndShowPassengerNotice: tripRuntime.checkAndShowPassengerNotice, |
| | | handlePickupException, |
| | | setVehicleDirection, |
| | | lastSocketDirectionAtRef, |
| | | setVehicleMoveAlongPoints, |
| | |
| | | }); |
| | | |
| | | /** |
| | | * 订单切换后清理上一单的接驾异常弹窗状态,避免串单。 |
| | | */ |
| | | useEffect(() => { |
| | | const previousOrderId = previousOrderIdRef.current; |
| | | if (previousOrderId && previousOrderId !== orderId) { |
| | | setPickupExceptionInfo(null); |
| | | setShowPickupExceptionPopup(false); |
| | | handledPickupExceptionSignatureRef.current = ''; |
| | | } |
| | | previousOrderIdRef.current = orderId; |
| | | }, [orderId]); |
| | | |
| | | /** |
| | | * 离开接驾态后收起异常弹窗,避免影响后续阶段。 |
| | | */ |
| | | useEffect(() => { |
| | | if ([TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup].includes(tripStatus)) { |
| | | return; |
| | | } |
| | | setPickupExceptionInfo(null); |
| | | setShowPickupExceptionPopup(false); |
| | | }, [tripStatus]); |
| | | |
| | | /** |
| | | * 支付成功后的统一收口。 |
| | | */ |
| | | const handlePaymentSuccess = useCallback(() => { |
| | |
| | | }); |
| | | |
| | | const { openCreditPayScoreUse } = useCreditPayReturn({ |
| | | onReturn: queryOrderPaymentResult |
| | | // 信用下单链路回跳后仅关闭监听,不再主动查询支付结果。 |
| | | onReturn: () => {} |
| | | }); |
| | | |
| | | useCashierPayListener({ |
| | |
| | | routeOptions, |
| | | selectedRouteIndex, |
| | | routeAndFareError, |
| | | distanceConfig, |
| | | callSubmitting, |
| | | cancelType, |
| | | pendingCancelContextRef, |
| | |
| | | setLongitude, |
| | | setShowOverRoutePopup, |
| | | handleOpenPrivacyPopup, |
| | | handleOpenCreditPayPopup, |
| | | checkDistanceAndCityReady, |
| | | bindSocketListeners, |
| | | routeUpdateWalkingRouteForStart, |
| | |
| | | vehicleLocation |
| | | ]); |
| | | |
| | | // 打开评价弹窗 |
| | | // 打开评价弹窗,并记录当前点击的评分 |
| | | const handleOpenEvaluatePopup = (orderRating: number) => { |
| | | setEvaluateOrderRating(orderRating); |
| | | setShowEvaluatePopup(true); |
| | | }; |
| | | |
| | | const handleCloseEvaluatePopup = () => { |
| | | setEvaluateCloseVersion(v => v + 1); |
| | | setShowEvaluatePopup(false); |
| | |
| | | const handleShareTrip = () => { |
| | | isCheckedEnter.current = true; |
| | | }; |
| | | const handleClosePickupExceptionPopup = useCallback(() => { |
| | | setShowPickupExceptionPopup(false); |
| | | }, []); |
| | | const handleConfirmPickupExceptionNavigation = useCallback(() => { |
| | | setShowPickupExceptionPopup(false); |
| | | const targetLatitude = Number(pickupExceptionInfo?.vehicleLat); |
| | | const targetLongitude = Number(pickupExceptionInfo?.vehicleLon); |
| | | if (!Number.isFinite(targetLatitude) || !Number.isFinite(targetLongitude)) { |
| | | return; |
| | | } |
| | | isCheckedEnter.current = true; |
| | | Taro.openLocation({ |
| | | latitude: targetLatitude, |
| | | longitude: targetLongitude, |
| | | name: '车辆位置', |
| | | address: pickupExceptionInfo?.message || '请步行前往车辆停靠位置', |
| | | scale: 18 |
| | | }); |
| | | }, [pickupExceptionInfo]); |
| | | // 打开优惠券选择弹窗 |
| | | const handleOpenCouponSelect = () => { |
| | | if (![TripStatus.PublishRoute, TripStatus.TripFinished].includes(tripStatus)) { |
| | |
| | | priceUpdateVersion, |
| | | evaluateSubmitVersion, |
| | | evaluateCloseVersion, |
| | | evaluatePopupStatus, |
| | | handleOpenOverRoutePopup, |
| | | handlePay, |
| | | handleOpenEvaluatePopup, |
| | | handleToggleDetailFade, |
| | | handleModifyDestination, |
| | | showModifyDestination, |
| | | showShareTrip, |
| | | handleShareTrip, |
| | | trunkStatus, |
| | | doorUnlockedInCurrentPage, |
| | |
| | | toPersonalCenter(); |
| | | }; |
| | | const handleWalkNavigation = () => { |
| | | if (!canWalkNavigateToPickup || !startSite?.latitude || !startSite?.longitude) { |
| | | if (!canWalkNavigateToPickup) { |
| | | return; |
| | | } |
| | | |
| | | const exceptionVehicleLatitude = Number(pickupExceptionInfo?.vehicleLat); |
| | | const exceptionVehicleLongitude = Number(pickupExceptionInfo?.vehicleLon); |
| | | const hasPickupExceptionTarget = pickupExceptionInfo?.enabled |
| | | && pickupExceptionInfo?.type === 1 |
| | | && Number.isFinite(exceptionVehicleLatitude) |
| | | && Number.isFinite(exceptionVehicleLongitude); |
| | | |
| | | const targetLatitude = hasPickupExceptionTarget |
| | | ? exceptionVehicleLatitude |
| | | : Number(startSite?.latitude); |
| | | const targetLongitude = hasPickupExceptionTarget |
| | | ? exceptionVehicleLongitude |
| | | : Number(startSite?.longitude); |
| | | if (!Number.isFinite(targetLatitude) || !Number.isFinite(targetLongitude)) { |
| | | return; |
| | | } |
| | | |
| | | Taro.openLocation({ |
| | | latitude: Number(startSite.latitude), |
| | | longitude: Number(startSite.longitude), |
| | | name: startSite.name || '上车点', |
| | | address: startSite.address || startSite.siteAddress || '', |
| | | latitude: targetLatitude, |
| | | longitude: targetLongitude, |
| | | name: hasPickupExceptionTarget ? '车辆位置' : (startSite.name || '上车点'), |
| | | address: hasPickupExceptionTarget |
| | | ? (pickupExceptionInfo?.message || '请步行前往车辆停靠位置') |
| | | : (startSite.address || startSite.siteAddress || ''), |
| | | scale: 18 |
| | | }); |
| | | }; |
| | |
| | | changeCheckedEnter, |
| | | showEvaluatePopup, |
| | | orderId, |
| | | evaluateOrderRating, |
| | | handleCloseEvaluatePopup, |
| | | handleConfirmEvaluate |
| | | }; |
| | |
| | | content={modifyDestinationFailMessage} |
| | | onConfirm={handleAcknowledgeModifyDestinationFail} |
| | | /> |
| | | <PickupExceptionPopup |
| | | show={showPickupExceptionPopup} |
| | | pickupExceptionInfo={pickupExceptionInfo} |
| | | onClose={handleClosePickupExceptionPopup} |
| | | onConfirm={handleConfirmPickupExceptionNavigation} |
| | | /> |
| | | <CreditPayPopup |
| | | show={showCreditPayPopup} |
| | | onClose={handleCloseCreditPayPopup} |
| | | onConfirm={handleConfirmCreditPayPopup} |
| | | /> |
| | | </> |
| | | ) |
| | | } |
| New file |
| | |
| | | export type PickupExceptionInfo = { |
| | | enabled: boolean; |
| | | type: number | null; |
| | | vehicleLon: number | null; |
| | | vehicleLat: number | null; |
| | | pickupLon: number | null; |
| | | pickupLat: number | null; |
| | | offsetDistance: number | null; |
| | | freeCancelExtendMinutes: number | null; |
| | | freeCancelDeadline: string; |
| | | message: string; |
| | | }; |
| | | |
| | | /** |
| | | * 归一化接驾异常对象,统一兜底空值和数值类型。 |
| | | */ |
| | | export function normalizePickupException(rawPickupException: any): PickupExceptionInfo | null { |
| | | if (!rawPickupException) { |
| | | return null; |
| | | } |
| | | const resolveNumber = (value: unknown) => { |
| | | const parsed = Number(value); |
| | | return Number.isFinite(parsed) ? parsed : null; |
| | | }; |
| | | |
| | | return { |
| | | enabled: Boolean(rawPickupException?.enabled), |
| | | type: resolveNumber(rawPickupException?.type), |
| | | vehicleLon: resolveNumber(rawPickupException?.vehicleLon), |
| | | vehicleLat: resolveNumber(rawPickupException?.vehicleLat), |
| | | pickupLon: resolveNumber(rawPickupException?.pickupLon), |
| | | pickupLat: resolveNumber(rawPickupException?.pickupLat), |
| | | offsetDistance: resolveNumber(rawPickupException?.offsetDistance), |
| | | freeCancelExtendMinutes: resolveNumber(rawPickupException?.freeCancelExtendMinutes), |
| | | freeCancelDeadline: String(rawPickupException?.freeCancelDeadline || ''), |
| | | message: String(rawPickupException?.message || '') |
| | | }; |
| | | } |
| | |
| | | import { getOrderDetail } from '../../../api/order'; |
| | | import EvaluatePopup from '../../begin-route/components/evaluate-popup'; |
| | | import { submitOrderComment } from '../../../api/begin-route'; |
| | | import { getOrderDistance } from '../../../api/select-map-site'; |
| | | import { formatTripDistance, formatTripDurationFromSeconds, safeParse } from '../../../../utils/Robotaxi-public-method'; |
| | | import { getOrderId } from '../../../utils/order'; |
| | | import { withRobotaxiWhitelist } from '../../../../utils/whitelist'; |
| | |
| | | const [activeIndex, setActiveIndex] = useState<number | null>(null) // 默认选中"非常满意" |
| | | const [orderData, setOrderData] = useState<any>(null); |
| | | const [showEvaluatePopup, setShowEvaluatePopup] = useState(false); |
| | | const [selectedOrderRating, setSelectedOrderRating] = useState(1); // 当前准备提交的评价分值 |
| | | const [evaluatePopupStatus, setEvaluatePopupStatus] = useState(0); // 0 不弹评价弹窗,1 点击任意评价都弹窗 |
| | | const showEvaluatePopupRef = useRef(false); |
| | | |
| | | // 地图相关状态 |
| | |
| | | fetchOrderDetail(orderId); |
| | | } |
| | | }, [orderId]); |
| | | |
| | | useEffect(() => { |
| | | const fetchEvaluatePopupConfig = async () => { |
| | | try { |
| | | const res: any = await getOrderDistance(); |
| | | // setEvaluatePopupStatus(Number(res?.evaluatePopupStatus) === 1 ? 1 : 0); |
| | | setEvaluatePopupStatus(1); |
| | | } catch (error) { |
| | | setEvaluatePopupStatus(0); |
| | | } |
| | | }; |
| | | fetchEvaluatePopupConfig(); |
| | | }, []); |
| | | |
| | | useEffect(() => { |
| | | showEvaluatePopupRef.current = showEvaluatePopup; |
| | |
| | | setActiveIndex(null); |
| | | } |
| | | const handleConfirmEvaluate = () => { |
| | | setActiveIndex(1); |
| | | setActiveIndex(selectedOrderRating); |
| | | setShowEvaluatePopup(false); |
| | | } |
| | | |
| | | const changeEvaluate = (index: number) => { |
| | | if (activeIndex !== null) return; |
| | | if (index === 1) { |
| | | handleOpenEvaluatePopup(); |
| | | return; |
| | | } |
| | | const submitSimpleEvaluate = (index: number) => { |
| | | const targetOrderId = getOrderId(orderId); |
| | | if (!targetOrderId) { |
| | | Taro.showToast({ title: '订单信息缺失,无法提交评价', icon: 'none' }); |
| | | return; |
| | | } |
| | | const successToastMap: Record<number, string> = { |
| | | 1: '收到您的评价,我们将努力提升体验', |
| | | 2: '收到您的评价,我们将努力提升体验', |
| | | 3: '感谢您的反馈,欢迎下次乘坐' |
| | | }; |
| | | submitOrderComment({ orderRating: index, orderId: targetOrderId }) |
| | | .then(() => { |
| | | setActiveIndex(index); |
| | | Taro.showToast({ |
| | | title: ['收到您的评价,我们将努力提升体验', '感谢您的反馈,欢迎下次乘坐'][index - 2], |
| | | title: successToastMap[index] || '评价提交成功', |
| | | icon: 'none' |
| | | }); |
| | | }) |
| | | .catch(() => { |
| | | Taro.showToast({ title: '评价提交失败,请稍后重试', icon: 'none' }); |
| | | }); |
| | | } |
| | | |
| | | const changeEvaluate = (index: number) => { |
| | | if (activeIndex !== null) return; |
| | | if (evaluatePopupStatus === 1) { |
| | | // 配置开启时,任意评价项都走弹窗反馈链路。 |
| | | setSelectedOrderRating(index); |
| | | handleOpenEvaluatePopup(); |
| | | return; |
| | | } |
| | | submitSimpleEvaluate(index); |
| | | } |
| | | |
| | | if (!orderData) return null; |
| | |
| | | <EvaluatePopup |
| | | show={showEvaluatePopup} |
| | | orderId={orderId} |
| | | orderRating={1} |
| | | orderRating={selectedOrderRating} |
| | | onClose={handleCloseEvaluatePopup} |
| | | onConfirm={handleConfirmEvaluate} |
| | | /> |