hejianhao
2026-06-12 1c85b1916584bec91ad22cc10c8d3a531e04e23b
Merge branch 'feature_v1.0' of http://gitlab.xiaopeng.local:18080/robotaxi1/xp-robotaxi-miniprogram into feature_v1.0
1个文件已添加
10个文件已修改
506 ■■■■■ 已修改文件
src/robotaxi/api/begin-route.ts 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car-modules/bluetooth-operate-card.tsx 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car.tsx 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/debug-bluetooth-panel.tsx 296 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-bluetooth-access.ts 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-bluetooth-car-control.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/index.tsx 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/share-trip/hooks/use-share-trip-runtime.ts 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/share-trip/index.tsx 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/whitelist.ts 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/api/begin-route.ts
@@ -322,6 +322,19 @@
    }, true);
}
// 静默查询订单轨迹,供分享页自行处理业务异常,避免默认拦截 toast。
export const queryOrderTrajectorySilently = async (
    payload: QueryOrderRoutePayload
): Promise<any> => {
    return request({
        url: _ROBOTAXI_HOST + `/order/api/v1/mini-program/order/queryOrderTrajectory`,
        method: 'GET',
        payload,
        showToast: false,
        skipException: true
    }, true);
}
// 订单查询队列状态
export const queryOrderQueueStatus = async (
    payload: QueryOrderQueueStatusPayload
@@ -341,6 +354,16 @@
    }, true);
}
// 静默查询分享订单实时路线,供分享页自行处理业务异常,避免默认拦截 toast。
export const shareQueryOrderRouteSilently = async (orderId: OrderId): Promise<any> => {
    return request({
        url: _ROBOTAXI_HOST + `/order/base/v1/mini-program/orderActive/share/queryOrderRoute/${orderId}`,
        method: 'GET',
        showToast: false,
        skipException: true
    }, true);
}
// 分享订单查询订单详情
export const shareQueryOrderInfo = async (
    payload: ShareQueryOrderInfoPayload
@@ -351,3 +374,16 @@
        payload
    }, true);
}
// 静默查询分享订单详情,供分享页自行处理业务异常,避免默认拦截 toast。
export const shareQueryOrderInfoSilently = async (
    payload: ShareQueryOrderInfoPayload
): Promise<any> => {
    return request({
        url: _ROBOTAXI_HOST + '/order/base/v1/mini-program/orderActive/share/orderInfo',
        method: 'GET',
        payload,
        showToast: false,
        skipException: true
    }, true);
}
src/robotaxi/pages/begin-route/components/calling-car-modules/bluetooth-operate-card.tsx
@@ -4,24 +4,30 @@
    description: string;
    actionText?: string;
    onClick: () => void;
    authSuccess?: boolean;
    /** 蓝牙图标资源(点亮/熄灭由 useBluetoothAccess 统一控制) */
    iconSrc: string;
    /** 描述文字颜色(空串表示沿用默认色) */
    descriptionColor?: string;
    /** 是否允许点击(默认 true)。false 时不绑定 onClick */
    clickable?: boolean;
};
export default function BluetoothOperateCard({ description, actionText = '', onClick, authSuccess = false }: Props) {
    // 只要有动作需要执行,或者未达到成功授权的系统级要求,都展示关闭状态的图标
    const iconSrc = actionText === '去开启' || actionText === '去授权' || !authSuccess
        ? "https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/close-bluetooth.png"
        : "https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/bluetooth-icon.png";
export default function BluetoothOperateCard({ description, actionText = '', onClick, iconSrc, descriptionColor = '', clickable = true }: Props) {
    const descriptionStyle = descriptionColor ? { color: descriptionColor } : undefined;
    // 右侧区域渲染规则:
    // - 不可点击(如车端钥匙下发失败)→ 不渲染
    // - "去开启"(系统蓝牙未开)→ 不渲染(当前 UI 仅在描述区表达)
    // - 其他可点击场景("去授权" / 已授权)→ 渲染容器;文字仅在有 actionText 时显示,箭头始终显示
    const shouldRenderRight = clickable && actionText !== '去开启';
    return (
        <View className="bluetooth-operate-card" onClick={onClick}>
        <View className="bluetooth-operate-card" onClick={clickable ? onClick : undefined}>
            <View className="bluetooth-operate-card-left">
                <Image src={iconSrc} />
                <View>{description}</View>
                <View style={descriptionStyle}>{description}</View>
            </View>
            {(!!actionText && actionText !== '去开启') && (
            {shouldRenderRight && (
                <View className="bluetooth-operate-card-right">
                    <View>{actionText}</View>
                    {!!actionText && <View>{actionText}</View>}
                    <Image src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/blue-right-icon.png" />
                </View>
            )}
src/robotaxi/pages/begin-route/components/calling-car.tsx
@@ -68,6 +68,9 @@
    bluetoothStatusReady?: boolean;
    bluetoothCardDescription?: string;
    bluetoothCardActionText?: string;
    bluetoothCardIconSrc?: string;
    bluetoothCardDescriptionColor?: string;
    bluetoothCardClickable?: boolean;
    handleBluetoothCardClick?: () => void;
    showAuthBluetoothPopup?: boolean;
    showSuccessAuthPopup?: boolean;
@@ -108,6 +111,9 @@
    bluetoothStatusReady = false,
    bluetoothCardDescription = '授权并打开蓝牙链接,享受无感出行',
    bluetoothCardActionText = '去开启',
    bluetoothCardIconSrc = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/close-bluetooth.png',
    bluetoothCardDescriptionColor = '',
    bluetoothCardClickable = true,
    handleBluetoothCardClick,
    showAuthBluetoothPopup = false,
    showSuccessAuthPopup = false,
@@ -170,7 +176,9 @@
                    description={bluetoothCardDescription}
                    actionText={bluetoothCardActionText}
                    onClick={handleBluetoothCardClick || (() => { })}
                    authSuccess={authSuccess}
                    iconSrc={bluetoothCardIconSrc}
                    descriptionColor={bluetoothCardDescriptionColor}
                    clickable={bluetoothCardClickable}
                />
            }
src/robotaxi/pages/begin-route/components/debug-bluetooth-panel.tsx
New file
@@ -0,0 +1,296 @@
import { useRef, useState } from 'react';
import { View, Text } from '@tarojs/components';
import { TripStatus } from '../../../constants/trip-status';
type Props = {
  /** 当前行程状态。 */
  tripStatus: TripStatus;
  /** 设置行程状态(用于本地 mock 接驾中/已到达状态,方便调试蓝牙)。 */
  setTripStatus: (status: TripStatus) => void;
  /** 蓝牙首轮状态判定是否完成(系统蓝牙 + 小程序授权)。 */
  bluetoothStatusReady: boolean;
  /** 蓝牙卡片右侧动作文案('去开启' / '去授权' / ''),间接体现授权链路状态。 */
  bluetoothCardActionText: string;
  /** RtAuth 蓝牙鉴权是否成功(连上车并鉴权通过)。 */
  bluetoothAuthSuccess: boolean;
  /** 车端钥匙下发失败(getUnlocktToken 接口非 200)。 */
  unlockTokenFailed: boolean;
  /** 调试用:直接覆盖 unlockTokenFailed 状态。 */
  onMockUnlockTokenFailed: (failed: boolean) => void;
  /** 调试用:直接覆盖 authSuccess(蓝牙打开车锁的成功状态)。 */
  onMockAuthSuccess: (success: boolean) => void;
};
/** 行程状态显示名(仅展示用,不影响业务)。 */
const TRIP_STATUS_LABEL: Record<number, string> = {
  [TripStatus.PublishRoute]: '发布行程',
  [TripStatus.Publishing]: '发布中',
  [TripStatus.CallingCar]: '呼叫车中',
  [TripStatus.VehicleApproaching]: '接驾中',
  [TripStatus.VehicleArrivedPickup]: '车辆已到达',
  [TripStatus.InService]: '行程中',
  [TripStatus.ArrivedDestination]: '到达目的地',
  [TripStatus.TripFinished]: '行程结束',
  [TripStatus.PaymentSuccess]: '支付成功',
  [TripStatus.CancelOnlyPickup]: '已取消'
};
/**
 * 解析蓝牙授权链路阶段,便于调试时一目了然。
 */
function resolveBluetoothStage(actionText: string, ready: boolean): { label: string; color: string } {
  if (!ready) {
    return { label: '检测中', color: '#aaa' };
  }
  if (actionText === '去开启') {
    return { label: '系统蓝牙未开启', color: '#ff5630' };
  }
  if (actionText === '去授权') {
    return { label: '小程序未授权', color: '#ffab00' };
  }
  return { label: '已授权(图标点亮)', color: '#36b37e' };
}
/**
 * 蓝牙调试面板(仅 dev 构建可见)。
 *
 * ## 用途
 * 在没有真实订单/车辆的环境下,可视化观察并调试蓝牙授权链路与行程状态相关 UI;
 * 同时提供 mock 入口,可一键模拟「车端钥匙下发失败」「蓝牙开锁成功」等关键状态。
 *
 * ## 当前状态
 * 本组件已从 `begin-route/index.tsx` 中下线,**默认不会渲染**。
 * 文件保留以便下次需要复用调试能力时直接启用,无需重新实现。
 *
 * ## 下次启用步骤
 * 业务 hook 默认不暴露 mock setter(保持生产 API 干净)。启用时按以下三步接入:
 *
 * 1) 在 `use-bluetooth-car-control.ts` 的 `return` 对象中临时加上 mock setter:
 *
 * ```ts
 * return {
 *   // ...原有字段
 *   mockUnlockTokenFailed: setUnlockTokenFailed,
 *   mockAuthSuccess: setAuthSuccess,
 * };
 * ```
 *
 * 2) 在 `begin-route/index.tsx` 中解构 mock setter 并 import 本面板:
 *
 * ```tsx
 * import DebugBluetoothPanel from './components/debug-bluetooth-panel';
 *
 * const {
 *   authSuccess: bluetoothAuthSuccess,
 *   unlockTokenFailed,
 *   mockUnlockTokenFailed,
 *   mockAuthSuccess,
 *   // ...其他原本就有的字段
 * } = useBluetoothCarControl({ orderId, tripStatus });
 * ```
 *
 * 3) 在页面 JSX 末尾渲染面板,把状态与 mock setter 透传进来:
 *
 * ```tsx
 * <DebugBluetoothPanel
 *   tripStatus={tripStatus}
 *   setTripStatus={setTripStatus}
 *   bluetoothStatusReady={bluetoothStatusReady}
 *   bluetoothCardActionText={bluetoothCardActionText}
 *   bluetoothAuthSuccess={bluetoothAuthSuccess}
 *   unlockTokenFailed={unlockTokenFailed}
 *   onMockUnlockTokenFailed={mockUnlockTokenFailed}
 *   onMockAuthSuccess={mockAuthSuccess}
 * />
 * ```
 *
 * 调试结束后请把上述三步全部回滚,恢复生产代码干净。
 *
 * ## 使用方法
 * 1. 仅在非生产构建(`process.env.NODE_ENV !== 'production'`)时渲染,
 *    生产构建(`npm run build:weapp`)会自动剔除整段渲染逻辑,不影响线上用户。
 * 2. 启动 `npm run dev:weapp` 后进入「行程页(begin-route)」,
 *    页面左侧 60% 高度处会出现一个蓝色的「蓝牙调试」浮动入口;点击展开为完整面板。
 * 3. 面板分两块:
 *    - 状态信息:实时显示当前行程状态、授权链路阶段、车端鉴权结果与钥匙下发状态,
 *      值会随真实状态变化同步刷新,便于对照 UI 表现。
 *    - 操作按钮:
 *      - 行程切换:「车辆已到达」/「接驾中」/「还原」一键切到/恢复 `tripStatus`,
 *        用于不创建真实订单也能看到蓝牙卡片、授权弹窗等 UI。
 *      - Mock 触发:「模拟钥匙下发失败」「模拟蓝牙开锁」按钮,
 *        分别覆盖 `unlockTokenFailed` 和 `authSuccess` 两个状态,再点一次取消模拟。
 *
 * ## 注意
 * - mock 仅修改前端状态,不会创建真实订单或调用任何车控接口。
 * - 没有 `orderId` 时,车端 RtAuth 链路不会真正启动,但蓝牙开启/小程序授权链路仍可调试。
 * - mock 状态会被真实链路覆盖(`startAuth`/`retryAuth`/`markManualDoorUnlock` /
 *   行程切到非 4/5 / `onStatusChange(status===3)` / `onRtAuthResult` 等),与生产路径一致。
 * - 若要新增 mock 能力:在 `useBluetoothCarControl` 中暴露对应 setter(命名 `mockXxx`),
 *   然后在本面板 Props 与按钮中接入。
 */
export default function DebugBluetoothPanel(props: Props) {
  const [expanded, setExpanded] = useState(false);
  const originalTripStatusRef = useRef<TripStatus | null>(null); // 首次 mock 前的原状态,用于「还原」
  if (process.env.NODE_ENV === 'production') {
    return null;
  }
  /** Mock 行程状态:首次 mock 前先记录原状态,以便「还原」恢复。 */
  const mockTripStatus = (next: TripStatus) => {
    if (originalTripStatusRef.current === null) {
      originalTripStatusRef.current = props.tripStatus;
    }
    console.log('[debug-bluetooth] 🧪 mock 行程状态', { from: props.tripStatus, to: next });
    props.setTripStatus(next);
  };
  /** 还原到首次 mock 前的状态。 */
  const restoreTripStatus = () => {
    if (originalTripStatusRef.current === null) {
      return;
    }
    const target = originalTripStatusRef.current;
    originalTripStatusRef.current = null;
    console.log('[debug-bluetooth] 🧪 还原行程状态', { to: target });
    props.setTripStatus(target);
  };
  const buttonStyle = (background: string) => ({
    flex: 1,
    textAlign: 'center' as const,
    padding: '8px 0',
    margin: '0 4px',
    background,
    color: '#fff',
    fontSize: '13px',
    borderRadius: '8px'
  });
  // 收起态:仅显示一个浮动小按钮,位置避开 DebugAutoscalePanel(top:45%)
  if (!expanded) {
    return (
      <View
        onClick={() => setExpanded(true)}
        style={{
          position: 'fixed',
          left: '0',
          top: '60%',
          zIndex: 9999,
          padding: '8px 10px',
          background: 'rgba(0,80,160,0.85)',
          color: '#fff',
          fontSize: '12px',
          borderTopRightRadius: '16px',
          borderBottomRightRadius: '16px'
        }}
      >
        蓝牙调试
      </View>
    );
  }
  const stage = resolveBluetoothStage(props.bluetoothCardActionText, props.bluetoothStatusReady);
  const isMocked = originalTripStatusRef.current !== null;
  // 展开态:完整调试面板,置顶展示避开底部 DebugAutoscalePanel
  return (
    <View
      catchMove
      style={{
        position: 'fixed',
        left: '12px',
        right: '12px',
        top: '80px',
        zIndex: 9999,
        padding: '12px 14px',
        background: 'rgba(20,40,80,0.92)',
        color: '#fff',
        borderRadius: '12px',
        boxShadow: '0 4px 16px rgba(0,0,0,0.4)'
      }}
    >
      <View style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
        <Text style={{ fontSize: '14px', fontWeight: 'bold' }}>蓝牙调试面板</Text>
        <View
          onClick={() => setExpanded(false)}
          style={{ padding: '2px 10px', fontSize: '13px', background: 'rgba(255,255,255,0.15)', borderRadius: '8px' }}
        >
          收起
        </View>
      </View>
      <View style={{ fontSize: '12px', lineHeight: '1.7', marginBottom: '8px', opacity: 0.9 }}>
        <Text style={{ display: 'block' }}>
          {`行程状态:${TRIP_STATUS_LABEL[props.tripStatus] || props.tripStatus} (${props.tripStatus})`}
          {isMocked && <Text style={{ marginLeft: '6px', color: '#ffab00' }}>· 已 mock</Text>}
        </Text>
        <Text style={{ display: 'block' }}>
          {'授权链路:'}
          <Text style={{ color: stage.color }}>{stage.label}</Text>
        </Text>
        <Text style={{ display: 'block' }}>
          {'车端鉴权:'}
          <Text style={{ color: props.bluetoothAuthSuccess ? '#36b37e' : '#aaa' }}>
            {props.bluetoothAuthSuccess ? '已通过' : '未通过'}
          </Text>
        </Text>
        <Text style={{ display: 'block' }}>
          {'钥匙下发:'}
          <Text style={{ color: props.unlockTokenFailed ? '#ff4d4f' : '#36b37e' }}>
            {props.unlockTokenFailed ? '失败(已 mock)' : '正常'}
          </Text>
        </Text>
      </View>
      <View style={{ display: 'flex', marginBottom: '8px' }}>
        <View
          onClick={() => mockTripStatus(TripStatus.VehicleArrivedPickup)}
          style={buttonStyle(props.tripStatus === TripStatus.VehicleArrivedPickup ? '#4c9aff' : 'rgba(255,255,255,0.2)')}
        >
          车辆已到达
        </View>
        <View
          onClick={() => mockTripStatus(TripStatus.VehicleApproaching)}
          style={buttonStyle(props.tripStatus === TripStatus.VehicleApproaching ? '#4c9aff' : 'rgba(255,255,255,0.2)')}
        >
          接驾中
        </View>
        <View
          onClick={restoreTripStatus}
          style={buttonStyle(isMocked ? 'rgba(255,255,255,0.25)' : 'rgba(255,255,255,0.1)')}
        >
          还原
        </View>
      </View>
      <View style={{ display: 'flex', marginBottom: '8px' }}>
        <View
          onClick={() => {
            const next = !props.unlockTokenFailed;
            console.log('[debug-bluetooth] 🧪 mock 钥匙下发失败', { next });
            props.onMockUnlockTokenFailed(next);
          }}
          style={buttonStyle(props.unlockTokenFailed ? '#ff4d4f' : 'rgba(255,255,255,0.2)')}
        >
          {props.unlockTokenFailed ? '取消模拟钥匙失败' : '模拟钥匙下发失败'}
        </View>
        <View
          onClick={() => {
            const next = !props.bluetoothAuthSuccess;
            console.log('[debug-bluetooth] 🧪 mock 蓝牙开锁成功', { next });
            props.onMockAuthSuccess(next);
          }}
          style={buttonStyle(props.bluetoothAuthSuccess ? '#36b37e' : 'rgba(255,255,255,0.2)')}
        >
          {props.bluetoothAuthSuccess ? '取消模拟开锁' : '模拟蓝牙开锁'}
        </View>
      </View>
      <View style={{ fontSize: '11px', lineHeight: '1.5', opacity: 0.7 }}>
        <Text style={{ display: 'block' }}>提示:mock 仅修改前端状态,不会创建真实订单。</Text>
        <Text style={{ display: 'block' }}>无 orderId 时车端 RtAuth 不会启动,但蓝牙开启/授权链路仍可调试。</Text>
      </View>
    </View>
  );
}
src/robotaxi/pages/begin-route/hooks/use-bluetooth-access.ts
@@ -6,6 +6,8 @@
  tripStatus: TripStatus;
  bluetoothAuthSuccess: boolean;
  startBluetoothAuth: () => void;
  /** 车端钥匙下发失败(getUnlocktToken 接口非 200),蓝牙不可解锁 */
  unlockTokenFailed?: boolean;
};
type BluetoothAccessStatus = 'system_off' | 'unauthorized' | 'authorized';
@@ -24,6 +26,12 @@
const BLUETOOTH_ACCESS_LOG_PREFIX = '[蓝牙权限]';
const BLUETOOTH_AUTH_SYNC_DELAY_MS = 300; // 授权完成后首轮刷新延迟,给小程序权限态一点同步时间
const BLUETOOTH_AUTH_PROTECTION_WINDOW_MS = 5000; // 授权成功后的保护窗口,避免瞬时 permission_deny 覆盖 UI
// 蓝牙图标资源:点亮(已授权)与熄灭(未开/未授权)两态
const BLUETOOTH_ICON_ON = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/bluetooth-icon.png';
const BLUETOOTH_ICON_OFF = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/close-bluetooth.png';
// 车端钥匙下发失败时的描述文案与文字颜色
const UNLOCK_TOKEN_FAILED_TEXT = '蓝牙解锁失败, 请点击右下方按键解锁车门';
const UNLOCK_TOKEN_FAILED_COLOR = '#FF4D4F';
/**
 * 输出蓝牙权限链路日志,便于排查授权、系统蓝牙和环境问题。
@@ -83,7 +91,7 @@
 * 2. 系统蓝牙已开启,但小程序未授权
 * 3. 系统蓝牙已开启,且小程序已授权
 */
export default function useBluetoothAccess({ tripStatus, bluetoothAuthSuccess, startBluetoothAuth }: Deps) {
export default function useBluetoothAccess({ tripStatus, bluetoothAuthSuccess, startBluetoothAuth, unlockTokenFailed = false }: Deps) {
  const [status, setStatus] = useState<BluetoothAccessStatus>('system_off'); // 当前蓝牙访问态:系统未开 / 小程序未授权 / 已授权
  const [statusReady, setStatusReady] = useState(false); // 蓝牙状态是否已完成首轮判定
  const [showAuthBluetoothPopup, setShowAuthBluetoothPopup] = useState(false); // 蓝牙授权引导弹窗展示态
@@ -473,14 +481,17 @@
  // 蓝牙卡片主文案
  const cardDescription = useMemo(() => {
    if (unlockTokenFailed) {
      return UNLOCK_TOKEN_FAILED_TEXT;
    }
    if (status === 'system_off') {
      return '授权并打开蓝牙链接,享受无感出行';
      return '系统蓝牙未开启, 无法自动解锁车辆';
    }
    if (status === 'unauthorized') {
      return '授权蓝牙,车辆自动解锁';
      return '授权蓝牙连接, 解锁无感出行';
    }
    return '车辆到达后,手机靠近车门可自动解锁';
  }, [status]);
    return '手机靠近车辆,可自动解锁车门';
  }, [status, unlockTokenFailed]);
@@ -493,6 +504,10 @@
  // 蓝牙卡片右侧动作文案
  const cardActionText = useMemo(() => {
    // 失败态:无右侧动作(不展示按钮文字与箭头)
    if (unlockTokenFailed) {
      return '';
    }
    if (status === 'system_off') {
      return '去开启';
    }
@@ -500,7 +515,28 @@
      return '';
    }
    return '去授权';
  }, [status, unlockTokenFailed]);
  // 蓝牙卡片左侧图标资源:仅当 status === 'authorized'(系统蓝牙已开 + 小程序已授权)时点亮
  const cardIconSrc = useMemo(() => {
    return status === 'authorized' ? BLUETOOTH_ICON_ON : BLUETOOTH_ICON_OFF;
  }, [status]);
  // 蓝牙卡片描述文字颜色:仅在车端钥匙下发失败时为红色,其他情况沿用默认色(空串表示不覆盖)
  const cardDescriptionColor = useMemo(() => {
    return unlockTokenFailed ? UNLOCK_TOKEN_FAILED_COLOR : '';
  }, [unlockTokenFailed]);
  // 蓝牙卡片是否可点击:失败态、系统蓝牙未开启时不可点击;其他状态可点击
  const cardClickable = useMemo(() => {
    if (unlockTokenFailed) {
      return false;
    }
    if (status === 'system_off') {
      return false;
    }
    return true;
  }, [status, unlockTokenFailed]);
  // 对外暴露:状态 + 视图文案 + 交互动作
  return {
@@ -508,6 +544,9 @@
    bluetoothStatusReady: statusReady,
    bluetoothCardDescription: cardDescription,
    bluetoothCardActionText: cardActionText,
    bluetoothCardIconSrc: cardIconSrc,
    bluetoothCardDescriptionColor: cardDescriptionColor,
    bluetoothCardClickable: cardClickable,
    showAuthBluetoothPopup,
    showSuccessAuthPopup,
    handleBluetoothCardClick,
src/robotaxi/pages/begin-route/hooks/use-bluetooth-car-control.ts
@@ -65,6 +65,7 @@
  const [authSuccess, setAuthSuccess] = useState(false); // RtAuth 鉴权是否成功
  const [isAuthorizing, setIsAuthorizing] = useState(false); // 鉴权流程进行中
  const [authError, setAuthError] = useState<string>(''); // 鉴权流程错误信息
  const [unlockTokenFailed, setUnlockTokenFailed] = useState(false); // getUnlocktToken 接口非 200,车端钥匙下发失败
  const authSuccessRef = useRef(false); // 鉴权成功状态引用,避免并发期间闭包值滞后
  const isAuthorizingRef = useRef(false); // 鉴权中状态引用,避免并发期间闭包值滞后
  const ensureManagerRef = useRef<() => any>(() => null); // 蓝牙管理器初始化函数引用
@@ -243,6 +244,7 @@
    isAuthorizingRef.current = true;
    setIsAuthorizing(true);
    setAuthError('');
    setUnlockTokenFailed(false);
    const runTask = async () => {
      try {
        const tokenRes = await getBluetoothUnlockToken(orderId);
@@ -295,6 +297,8 @@
        console.log('[蓝牙] 鉴权流程异常:', error);
        isAuthorizingRef.current = false;
        setAuthError(error?.msg || '获取蓝牙鉴权凭证失败');
        // getUnlocktToken 接口非 200(含网络异常),车端钥匙下发失败,标记蓝牙不可解锁
        setUnlockTokenFailed(true);
        setIsAuthorizing(false);
      } finally {
        startAuthTaskRef.current = null;
@@ -322,6 +326,7 @@
    isAuthorizingRef.current = true;
    setIsAuthorizing(true);
    setAuthError('');
    setUnlockTokenFailed(false);
    managerRef.current.retryConnection();
  }, [startAuth]);
@@ -337,6 +342,7 @@
    }
    acceptBluetoothCallbacksRef.current = false;
    setAuthError('');
    setUnlockTokenFailed(false);
    closeConnectionRef.current();
  }, [orderId]);
@@ -354,6 +360,7 @@
      unlockFinishedRef.current = false;
      manualDoorUnlockRef.current = false; // 重置手动解锁状态,供下一次使用
      setAuthError('');
      setUnlockTokenFailed(false);
      
      // 行程进入 6(InService 行驶中)或以上,或者是回退/取消等情况时,清除该订单已弹出过成功解锁的标识缓存
      if (orderId) {
@@ -367,6 +374,7 @@
    authSuccess,
    authError,
    isAuthorizing,
    unlockTokenFailed,
    startAuth,
    markManualDoorUnlock,
    retryAuth,
src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx
@@ -141,6 +141,12 @@
  bluetoothCardDescription: string;
  /** 蓝牙卡片右侧动作文案 */
  bluetoothCardActionText: string;
  /** 蓝牙卡片左侧图标资源 */
  bluetoothCardIconSrc: string;
  /** 蓝牙卡片描述文字颜色(空串表示沿用默认色) */
  bluetoothCardDescriptionColor: string;
  /** 蓝牙卡片是否允许点击 */
  bluetoothCardClickable: boolean;
  /** 点击蓝牙卡片 */
  handleBluetoothCardClick: () => void;
  /** 蓝牙授权弹窗是否展示 */
@@ -457,6 +463,9 @@
          bluetoothStatusReady={deps.bluetoothStatusReady}
          bluetoothCardDescription={deps.bluetoothCardDescription}
          bluetoothCardActionText={deps.bluetoothCardActionText}
          bluetoothCardIconSrc={deps.bluetoothCardIconSrc}
          bluetoothCardDescriptionColor={deps.bluetoothCardDescriptionColor}
          bluetoothCardClickable={deps.bluetoothCardClickable}
          handleBluetoothCardClick={deps.handleBluetoothCardClick}
          showAuthBluetoothPopup={deps.showAuthBluetoothPopup}
          showSuccessAuthPopup={deps.showSuccessAuthPopup}
src/robotaxi/pages/begin-route/index.tsx
@@ -1054,7 +1054,8 @@
  const {
    authSuccess: bluetoothAuthSuccess,
    startAuth: startBluetoothAuth,
    markManualDoorUnlock
    markManualDoorUnlock,
    unlockTokenFailed
  } = useBluetoothCarControl({
    orderId,
    tripStatus,
@@ -1071,6 +1072,9 @@
    bluetoothStatusReady,
    bluetoothCardDescription,
    bluetoothCardActionText,
    bluetoothCardIconSrc,
    bluetoothCardDescriptionColor,
    bluetoothCardClickable,
    showAuthBluetoothPopup,
    showSuccessAuthPopup,
    handleBluetoothCardClick,
@@ -1080,7 +1084,8 @@
  } = useBluetoothAccess({
    tripStatus,
    bluetoothAuthSuccess,
    startBluetoothAuth
    startBluetoothAuth,
    unlockTokenFailed
  });
  const { handleConfirmRequoteIfNeeded } = useModifyDestinationPreviewRoute({
    tripStatus,
@@ -1489,6 +1494,9 @@
    bluetoothStatusReady,
    bluetoothCardDescription,
    bluetoothCardActionText,
    bluetoothCardIconSrc,
    bluetoothCardDescriptionColor,
    bluetoothCardClickable,
    handleBluetoothCardClick,
    showAuthBluetoothPopup,
    showSuccessAuthPopup,
src/robotaxi/pages/share-trip/hooks/use-share-trip-runtime.ts
@@ -1,13 +1,23 @@
import Taro from '@tarojs/taro';
import { useEffect, useRef } from 'react';
import { mapOrderStatusToTripStatus } from '../../../../utils/Robotaxi-public-method';
import { queryOrderTrajectory, shareQueryOrderInfo, shareQueryOrderRoute } from '../../../api/begin-route';
import {
  queryOrderTrajectorySilently,
  shareQueryOrderInfoSilently,
  shareQueryOrderRouteSilently
} from '../../../api/begin-route';
import { normalizeDistanceKm, normalizeDurationSeconds } from '../../../utils/format';
import { TripStatus } from '../../../constants/trip-status';
type LocationPoint = { latitude: number; longitude: number };
type RouteDistance = { value: number; unit: 'km' | 'm' } | null;
type SilentApiResponse<T> = {
  code?: number;
  status?: number;
  msg?: string;
  data?: T;
};
const SHARE_TRIP_POLL_INTERVAL_MS = 5000; // 分享页轮询间隔
const MAX_SHARE_TRIP_POLL_COUNT = 6000; // 分享页最多轮询 10 分钟
@@ -81,6 +91,18 @@
  const toFiniteNumber = (value: any) => {
    const num = Number(value);
    return Number.isFinite(num) ? num : null;
  };
  /**
   * 分享页静默接口统一解包。
   * 这里不走全局默认异常 toast,由页面自行决定如何处理。
   */
  const unwrapSilentResponse = <T>(response: SilentApiResponse<T>) => {
    const code = Number(response?.code ?? response?.status ?? 200);
    if (code === 200) {
      return response?.data ?? response;
    }
    throw response;
  };
  /**
@@ -329,12 +351,12 @@
   */
  const requestShareRouteByStatus = (targetStatus: TripStatus) => {
    if ([TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(targetStatus)) {
      return queryOrderTrajectory({
      return queryOrderTrajectorySilently({
        orderId: deps.orderId,
        routeType: 2,
      });
    }
    return shareQueryOrderRoute(deps.orderId);
    return shareQueryOrderRouteSilently(deps.orderId);
  };
  useEffect(() => {
@@ -446,7 +468,8 @@
    };
    const fetchInitialShareInfo = async () => {
      const info = await shareQueryOrderInfo({ orderId: deps.orderId });
      const infoResponse = await shareQueryOrderInfoSilently({ orderId: deps.orderId });
      const info = unwrapSilentResponse(infoResponse);
      if (cancelled) return null;
      return applyShareInfo(info || {});
    };
@@ -473,7 +496,9 @@
          stopPolling();
          return false;
        }
        let routeResponse = await requestShareRouteByStatus(tripStatusRef.current);
        let routeResponse = unwrapSilentResponse(
          await requestShareRouteByStatus(tripStatusRef.current)
        );
        if (cancelled) return false;
        const routeStatus = resolveRouteOrderStatus(routeResponse);
        const shouldFetchInfo = routeStatus !== null && routeStatus !== infoStatusRef.current;
@@ -483,7 +508,8 @@
        let nextEndSite = endSiteRef.current;
        if (shouldFetchInfo) {
          const info = await shareQueryOrderInfo({ orderId: deps.orderId });
          const infoResponse = await shareQueryOrderInfoSilently({ orderId: deps.orderId });
          const info = unwrapSilentResponse(infoResponse);
          if (cancelled) return false;
          const infoState = applyShareInfo(info || {});
          mappedStatus = infoState.mappedStatus;
@@ -494,7 +520,9 @@
          // TripFinished 继续走分享实时路线接口,完单态走完整轨迹接口,
          // 这样地图才能基于最新路线按 order_detail 视口完整展示。
          if (mappedStatus >= TripStatus.TripFinished) {
            routeResponse = await requestShareRouteByStatus(mappedStatus);
            routeResponse = unwrapSilentResponse(
              await requestShareRouteByStatus(mappedStatus)
            );
            if (cancelled) return false;
          }
        }
src/robotaxi/pages/share-trip/index.tsx
@@ -201,7 +201,10 @@
      return;
    }
    // 非白名单用户先回到小鹏汽车首页,页面内容后续再补充
    jumpToHome();
    // jumpToHome();
    Taro.switchTab({
      url: '/pages/home/index',
    });
  }; // 返回首页
  const handleContactService = () => {
src/utils/whitelist.ts
@@ -17,7 +17,11 @@
const WHITELIST_EXEMPT_ROUTES = [
  'pages/not-on-whitelist/index',
  'pages/user/login',
  'robotaxi/pages/share-trip/index',
];
/** 分享行程页路由片段。 */
const SHARE_TRIP_ROUTE_SEGMENT = 'pages/share-trip/index';
/** 白名单请求并发锁。 */
let whitelistCheckTask: Promise<WhitelistAccessResult> | null = null;
@@ -29,6 +33,13 @@
 * 判断当前页面是否跳过白名单守卫。
 */
export function isWhitelistExemptPage(route: string) {
  if (!route) {
    return false;
  }
  // 分享行程允许非白名单用户访问,路由命中时直接跳过白名单拦截。
  if (route === SHARE_TRIP_ROUTE_SEGMENT || route.endsWith(`/${SHARE_TRIP_ROUTE_SEGMENT}`)) {
    return true;
  }
  return WHITELIST_EXEMPT_ROUTES.includes(route);
}