zjk
2026-06-18 eacf8c6b658f83b0b149db22ccf0e6e5578daab6
Merge branch 'feature_v1.1' into feature_v2.0_zjk
6个文件已添加
16个文件已修改
1400 ■■■■■ 已修改文件
src/robotaxi/api/begin-route.ts 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/hooks/use-credit-pay-sign.ts 86 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/begin-route-view-context.tsx 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/begin-route-view.tsx 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car-modules/evaluation-card.tsx 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car-modules/trip-action-bar.tsx 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car.tsx 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/credit-pay-popup.tsx 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/css-folder/credit-pay-popup.less 112 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/css-folder/pickup-exception-popup.less 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/evaluate-popup.tsx 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/pickup-exception-popup.tsx 351 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-order-actions.shared.ts 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-order-distance-config.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-order-payment.ts 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-order-submit.ts 284 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-web-socket-events.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/web-socket-handlers/create-order-status-socket-handler.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/index.tsx 185 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/pickup-exception.ts 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/order/detail/index.tsx 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/api/begin-route.ts
@@ -99,6 +99,17 @@
    }, 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({
@@ -262,9 +273,10 @@
}
// 获取订单评价类型
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);
}
src/robotaxi/hooks/use-credit-pay-sign.ts
New file
@@ -0,0 +1,86 @@
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
    };
}
src/robotaxi/pages/begin-route/components/begin-route-view-context.tsx
@@ -123,6 +123,8 @@
  showEvaluatePopup: boolean;
  /** 订单 ID。 */
  orderId: string;
  /** 当前评价弹窗对应的评分。 */
  evaluateOrderRating: number;
  /** 关闭评价弹窗。 */
  handleCloseEvaluatePopup: () => void;
  /** 确认评价。 */
src/robotaxi/pages/begin-route/components/begin-route-view.tsx
@@ -172,6 +172,7 @@
    changeCheckedEnter,
    showEvaluatePopup,
    orderId,
    evaluateOrderRating,
    handleCloseEvaluatePopup,
    handleConfirmEvaluate,
    showAuthBluetoothPopup = false,
@@ -226,7 +227,7 @@
      <EvaluatePopup
        show={showEvaluatePopup}
        orderId={orderId}
        orderRating={1}
        orderRating={evaluateOrderRating}
        onClose={handleCloseEvaluatePopup}
        onConfirm={handleConfirmEvaluate}
      />
src/robotaxi/pages/begin-route/components/calling-car-modules/evaluation-card.tsx
@@ -8,9 +8,10 @@
type Props = {
    tripStatus: TripStatus;
    orderId?: string;
    handleOpenEvaluatePopup?: () => void;
    handleOpenEvaluatePopup?: (orderRating: number) => void;
    evaluateSubmitVersion?: number;
    evaluateCloseVersion?: number;
    evaluatePopupStatus?: number;
};
export default function EvaluationCard({
@@ -18,7 +19,8 @@
    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';
@@ -50,12 +52,12 @@
        }
    }, [evaluateCloseVersion]);
    // 点击评价项:不满意走弹窗,其余直接提交
    // 点击评价项:配置开启时任意评价都走弹窗,否则直接提交
    const changeEvaluate = (index: number) => {
        if (isEvaluated) return;
        setActiveIndex(index);
        if (index === 1) {
            handleOpenEvaluatePopup?.();
        if (evaluatePopupStatus === 1) {
            handleOpenEvaluatePopup?.(index);
            return;
        }
        const targetOrderId = getOrderId(orderId);
src/robotaxi/pages/begin-route/components/calling-car-modules/trip-action-bar.tsx
@@ -9,6 +9,7 @@
    handleShareTrip?: () => void;
    showModifyDestination?: boolean;
    showCancelTrip?: boolean;
    showShareTrip?: boolean;
};
export default function TripActionBar({
@@ -17,7 +18,8 @@
    handleModifyDestination,
    handleShareTrip,
    showModifyDestination = true,
    showCancelTrip = false
    showCancelTrip = false,
    showShareTrip = true
}: Props) {
    const canShowTripAction = [TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup, TripStatus.InService].includes(tripStatus);
    // 按行程状态控制按钮集合,避免在 JSX 中堆叠过多条件判断
@@ -41,7 +43,7 @@
            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);
src/robotaxi/pages/begin-route/components/calling-car.tsx
@@ -26,6 +26,8 @@
    evaluateSubmitVersion?: number;
    /** 外部触发“评价弹窗关闭”版本号 */
    evaluateCloseVersion?: number;
    /** 评价弹窗开关配置,0 不弹窗,1 点击任意评价都弹窗 */
    evaluatePopupStatus?: number;
    /** 打开结束行程弹窗 */
    handleOpenOverRoutePopup?: () => void;
    /** 费用明细展开/收起时同步地图视觉状态 */
@@ -33,13 +35,15 @@
    /** 立即支付回调 */
    handlePay?: () => void;
    /** 打开详细评价弹窗 */
    handleOpenEvaluatePopup?: () => void;
    handleOpenEvaluatePopup?: (orderRating: number) => void;
    /** 取消行程 */
    handleCancelTrip?: () => void;
    /** 修改目的地 */
    handleModifyDestination?: () => void;
    /** 是否展示修改目的地 */
    showModifyDestination?: boolean;
    /** 是否展示分享行程 */
    showShareTrip?: boolean;
    /** 分享行程 */
    handleShareTrip?: () => void;
    /** 预估行程秒数 */
@@ -98,6 +102,7 @@
    handleFinishTimeout,
    evaluateSubmitVersion,
    evaluateCloseVersion,
    evaluatePopupStatus = 0,
    handleOpenOverRoutePopup,
    onToggleDetailFade,
    handlePay,
@@ -105,6 +110,7 @@
    handleCancelTrip,
    handleModifyDestination,
    showModifyDestination = true,
    showShareTrip = true,
    handleShareTrip,
    routeDurationSeconds,
    finishCountdownEnabled = false,
@@ -248,6 +254,7 @@
                handleOpenEvaluatePopup={handleOpenEvaluatePopup}
                evaluateSubmitVersion={evaluateSubmitVersion}
                evaluateCloseVersion={evaluateCloseVersion}
                evaluatePopupStatus={evaluatePopupStatus}
            />
        </View>
    );
src/robotaxi/pages/begin-route/components/credit-pay-popup.tsx
New file
@@ -0,0 +1,65 @@
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>
  );
}
src/robotaxi/pages/begin-route/components/css-folder/credit-pay-popup.less
New file
@@ -0,0 +1,112 @@
.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);
}
src/robotaxi/pages/begin-route/components/css-folder/pickup-exception-popup.less
New file
@@ -0,0 +1,84 @@
.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;
}
src/robotaxi/pages/begin-route/components/evaluate-popup.tsx
@@ -18,6 +18,8 @@
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); // 当前选择的问题类型
@@ -36,6 +38,36 @@
        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(() => {
@@ -141,7 +173,7 @@
        setSelectedTypeId(null);
        setFeedbackContent('');
        resetImages();
        getOrderCommentType().then((res: any) => {
        getOrderCommentType(evaluateDimension).then((res: any) => {
            const list = res;
            if (Array.isArray(list)) {
                setTypeOptions(list);
@@ -151,7 +183,7 @@
        }).catch(() => {
            setTypeOptions([]);
        });
    }, [resetImages, show]);
    }, [evaluateDimension, resetImages, show]);
    if (!visible) return null;
@@ -163,12 +195,12 @@
                    <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>
@@ -180,7 +212,7 @@
                            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>
src/robotaxi/pages/begin-route/components/pickup-exception-popup.tsx
New file
@@ -0,0 +1,351 @@
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>
  );
}
src/robotaxi/pages/begin-route/hooks/use-order-actions.shared.ts
@@ -1,4 +1,5 @@
import { SignedOrderPaymentValue } from '../../../api/types/begin-route';
import type { OrderDistanceConfig } from './use-order-distance-config';
import { TripStatus } from '../constants/trip-status';
/** 下单防抖时长,单位毫秒。 */
@@ -62,6 +63,8 @@
  selectedRouteIndex?: number;
  /** 路线错误标记。 */
  routeAndFareError: boolean;
  /** 下单配置。 */
  distanceConfig?: OrderDistanceConfig;
  /** 下单提交中。 */
  callSubmitting: boolean;
  /** 取消类型。 */
@@ -103,6 +106,8 @@
  setShowOverRoutePopup: (value: boolean) => void;
  /** 打开协议弹窗。 */
  handleOpenPrivacyPopup: () => void;
  /** 打开免密支付开通提醒弹窗。 */
  handleOpenCreditPayPopup: () => Promise<boolean>;
  /** 距离与城市校验。 */
  checkDistanceAndCityReady: (startSite: any) => Promise<number | null>;
  /** 绑定 socket 监听。 */
src/robotaxi/pages/begin-route/hooks/use-order-distance-config.ts
@@ -8,11 +8,14 @@
  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;
@@ -51,6 +54,9 @@
          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),
src/robotaxi/pages/begin-route/hooks/use-order-payment.ts
@@ -187,23 +187,27 @@
      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;
@@ -215,11 +219,12 @@
      //   return;
      // }
      // Taro.showToast({ title: '暂不支持当前支付方式', icon: 'none' });
      Taro.showToast({ title: '暂不支持当前支付方式', icon: 'none' });
    } catch (error) {
      Taro.showToast({ title: '支付失败,请重试', icon: 'none' });
    } finally {
      Taro.hideLoading();
      paySubmittingRef.current = false;
    }
  };
src/robotaxi/pages/begin-route/hooks/use-order-submit.ts
@@ -1,10 +1,18 @@
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,
@@ -31,6 +39,8 @@
type SubmitOrderResult = {
  /** 新订单 ID。 */
  orderId: string;
  /** 支付分确认页拉起参数。 */
  creditPayPackageValue?: string;
};
/** 下单 Hook 返回值。 */
@@ -79,10 +89,22 @@
  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>>(); // 始终指向最新的下单流程
  /**
   * 结束下单中状态。
   */
@@ -131,6 +153,18 @@
  };
  /**
   * 上报下单埋点。
   */
  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) => {
@@ -163,44 +197,30 @@
  };
  /**
   * 提交订单。
   * 统一提取接口返回中的订单 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)
@@ -222,17 +242,75 @@
      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);
  };
  /**
@@ -351,14 +429,100 @@
  };
  /**
   * 继续执行签约页返回后的下单流程。
   */
  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) {
@@ -397,8 +561,21 @@
      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;
@@ -407,11 +584,27 @@
      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;
  /**
   * 防抖下单入口。
@@ -419,10 +612,9 @@
  const callImmediatelyWithDebounce = debounce(submitOrderFlow, ORDER_SUBMIT_DEBOUNCE_MS);
  /**
   * 呼叫入口:先在点击手势阶段预拉起订阅,再进入防抖后的下单流程。
   * 呼叫入口:进入防抖后的下单流程。
   */
  const callImmediately = () => {
    requestOrderSuccessSubscribeOnTap();
    callImmediatelyWithDebounce();
  };
src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx
@@ -97,18 +97,22 @@
  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;
  /** 路线选择展开状态 */
@@ -487,6 +491,7 @@
          handleCancelTrip={() => deps.handleOpenCancelPopupByTripStatus(deps.tripStatus, true)}
          handleModifyDestination={deps.handleModifyDestination}
          showModifyDestination={deps.showModifyDestination}
          showShareTrip={deps.showShareTrip}
          handleShareTrip={deps.handleShareTrip}
          resetDetailVersion={deps.resetDetailVersion}
          waitMinutes={waitMinutes}
@@ -496,6 +501,7 @@
          fareBreakdown={fareBreakdown}
          evaluateSubmitVersion={deps.evaluateSubmitVersion}
          evaluateCloseVersion={deps.evaluateCloseVersion}
          evaluatePopupStatus={deps.evaluatePopupStatus}
          handleOpenOverRoutePopup={deps.handleOpenOverRoutePopup}
          handlePay={deps.handlePay}
          onOpenCouponSelect={deps.handleOpenCouponSelect}
src/robotaxi/pages/begin-route/hooks/use-web-socket-events.ts
@@ -12,6 +12,7 @@
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>;
@@ -56,6 +57,7 @@
    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;
src/robotaxi/pages/begin-route/hooks/web-socket-handlers/create-order-status-socket-handler.ts
@@ -10,6 +10,7 @@
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。
@@ -356,6 +357,7 @@
  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();
src/robotaxi/pages/begin-route/index.tsx
@@ -6,6 +6,8 @@
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';
@@ -81,6 +83,7 @@
  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;
@@ -118,6 +121,25 @@
  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('|');
}
/**
 * 生成确认行程类埋点公共参数。
@@ -186,7 +208,7 @@
    handleOpenPassengerNotice,
    showEvaluatePopup,
    setShowEvaluatePopup,
    handleOpenEvaluatePopup,
    handleOpenEvaluatePopup: _handleOpenEvaluatePopup,
    showTipPopup,
    setShowTipPopup,
    handleCloseTipPopup,
@@ -226,10 +248,15 @@
  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
@@ -249,6 +276,8 @@
  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,
@@ -975,6 +1004,8 @@
  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; // 步行导航按钮展示阈值(米)
@@ -1171,6 +1202,59 @@
    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,
@@ -1206,6 +1290,7 @@
    setCancelType,
    setShowCancelPopup,
    checkAndShowPassengerNotice: tripRuntime.checkAndShowPassengerNotice,
    handlePickupException,
    setVehicleDirection,
    lastSocketDirectionAtRef,
    setVehicleMoveAlongPoints,
@@ -1229,6 +1314,30 @@
  });
  /**
   * 订单切换后清理上一单的接驾异常弹窗状态,避免串单。
   */
  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(() => {
@@ -1250,7 +1359,8 @@
  });
  const { openCreditPayScoreUse } = useCreditPayReturn({
    onReturn: queryOrderPaymentResult
    // 信用下单链路回跳后仅关闭监听,不再主动查询支付结果。
    onReturn: () => {}
  });
  useCashierPayListener({
@@ -1290,6 +1400,7 @@
    routeOptions,
    selectedRouteIndex,
    routeAndFareError,
    distanceConfig,
    callSubmitting,
    cancelType,
    pendingCancelContextRef,
@@ -1308,6 +1419,7 @@
    setLongitude,
    setShowOverRoutePopup,
    handleOpenPrivacyPopup,
    handleOpenCreditPayPopup,
    checkDistanceAndCityReady,
    bindSocketListeners,
    routeUpdateWalkingRouteForStart,
@@ -1445,7 +1557,12 @@
    vehicleLocation
  ]);
  // 打开评价弹窗
  // 打开评价弹窗,并记录当前点击的评分
  const handleOpenEvaluatePopup = (orderRating: number) => {
    setEvaluateOrderRating(orderRating);
    setShowEvaluatePopup(true);
  };
  const handleCloseEvaluatePopup = () => {
    setEvaluateCloseVersion(v => v + 1);
    setShowEvaluatePopup(false);
@@ -1603,6 +1720,25 @@
  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)) {
@@ -1785,12 +1921,14 @@
    priceUpdateVersion,
    evaluateSubmitVersion,
    evaluateCloseVersion,
    evaluatePopupStatus,
    handleOpenOverRoutePopup,
    handlePay,
    handleOpenEvaluatePopup,
    handleToggleDetailFade,
    handleModifyDestination,
    showModifyDestination,
    showShareTrip,
    handleShareTrip,
    trunkStatus,
    doorUnlockedInCurrentPage,
@@ -1849,15 +1987,34 @@
    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
    });
  };
@@ -1932,6 +2089,7 @@
    changeCheckedEnter,
    showEvaluatePopup,
    orderId,
    evaluateOrderRating,
    handleCloseEvaluatePopup,
    handleConfirmEvaluate
  };
@@ -1953,6 +2111,17 @@
        content={modifyDestinationFailMessage}
        onConfirm={handleAcknowledgeModifyDestinationFail}
      />
      <PickupExceptionPopup
        show={showPickupExceptionPopup}
        pickupExceptionInfo={pickupExceptionInfo}
        onClose={handleClosePickupExceptionPopup}
        onConfirm={handleConfirmPickupExceptionNavigation}
      />
      <CreditPayPopup
        show={showCreditPayPopup}
        onClose={handleCloseCreditPayPopup}
        onConfirm={handleConfirmCreditPayPopup}
      />
    </>
  )
}
src/robotaxi/pages/begin-route/pickup-exception.ts
New file
@@ -0,0 +1,38 @@
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 || '')
  };
}
src/robotaxi/pages/order/detail/index.tsx
@@ -5,6 +5,7 @@
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';
@@ -23,6 +24,8 @@
  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);
  // 地图相关状态
@@ -63,6 +66,19 @@
      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;
@@ -221,32 +237,43 @@
    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;
@@ -345,7 +372,7 @@
      <EvaluatePopup
        show={showEvaluatePopup}
        orderId={orderId}
        orderRating={1}
        orderRating={selectedOrderRating}
        onClose={handleCloseEvaluatePopup}
        onConfirm={handleConfirmEvaluate}
      />