liaof
2026-07-15 dbb323adc7c1114282733600df9dfe3e686d901d
Merge branch 'feature_v2.3.1' into 'feature_v2.3'

几个bug修复

See merge request robotaxi1/xp-robotaxi-miniprogram!155
4个文件已修改
270 ■■■■■ 已修改文件
src/robotaxi/pages/begin-route/hooks/use-bluetooth-access.ts 212 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/index.tsx 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/select-map-site/hooks/use-select-map-site.ts 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/site-search/index.tsx 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-bluetooth-access.ts
@@ -19,6 +19,12 @@
  errorType: BluetoothErrorType;
  error: any;
};
type BluetoothPermissionLayerState = 'authorized' | 'denied' | 'not determined' | 'unknown';
type BluetoothPermissionSnapshot = {
  appBluetoothAuthorized: BluetoothPermissionLayerState;
  miniappBluetoothScope: MiniappBluetoothAuthState;
  systemInfo: Record<string, unknown>;
};
const BLUETOOTH_AUTH_KEY = 'ROBOTAXI_BLUETOOTH_AUTHORIZED';
const BLUETOOTH_SCOPE = 'scope.bluetooth' as any;
@@ -48,7 +54,7 @@
      if (bluetoothAuthorized === 'authorized') {
        return 'authorized';
      }
      if (bluetoothAuthorized === 'denied' || bluetoothAuthorized === 'not determined') {
      if (bluetoothAuthorized === 'denied') {
        return 'denied';
      }
    }
@@ -76,6 +82,13 @@
    return;
  }
  console.log(BLUETOOTH_ACCESS_LOG_PREFIX, message);
}
function getBluetoothPermissionDeniedMessage(snapshot?: Partial<BluetoothPermissionSnapshot>) {
  if (snapshot?.appBluetoothAuthorized === 'denied') {
    return '请先在微信设置中授权蓝牙';
  }
  return '请先授权蓝牙权限';
}
/**
@@ -141,50 +154,109 @@
  const startBluetoothStateWatchRef = useRef<() => void>(() => {});
  const stopBluetoothStateWatchRef = useRef<() => void>(() => {});
  /**
   * 读取当前小程序蓝牙授权状态。
   */
  const getMiniappBluetoothAuthState = useCallback(async () => {
  const getBluetoothSystemInfoSnapshot = useCallback(() => {
    try {
      const systemInfo = Taro.getSystemInfoSync() as any;
      return {
        platform: systemInfo?.platform,
        brand: systemInfo?.brand,
        model: systemInfo?.model,
        system: systemInfo?.system,
        version: systemInfo?.version,
        SDKVersion: systemInfo?.SDKVersion,
        bluetoothEnabled: systemInfo?.bluetoothEnabled,
        locationEnabled: systemInfo?.locationEnabled,
        locationAuthorized: systemInfo?.locationAuthorized
      };
    } catch (error: any) {
      return {
        systemInfoError: error?.message || String(error || '')
      };
    }
  }, []);
  const readBluetoothPermissionSnapshot = useCallback(async (): Promise<BluetoothPermissionSnapshot> => {
    let appBluetoothAuthorized: BluetoothPermissionLayerState = 'unknown';
    let miniappBluetoothScope: MiniappBluetoothAuthState = 'unknown';
    const systemInfo = getBluetoothSystemInfoSnapshot();
    try {
      const appAuthorizeSetting = await Promise.resolve((Taro as any).getAppAuthorizeSetting?.());
      const bluetoothAuthorized = appAuthorizeSetting?.bluetoothAuthorized;
      logBluetoothAccess('读取小程序权限设置', {
        bluetoothAuthorized,
        appAuthorizeSetting
      });
      if (typeof bluetoothAuthorized === 'string') {
        if (bluetoothAuthorized === 'authorized') {
          return 'authorized';
        }
        if (bluetoothAuthorized === 'denied' || bluetoothAuthorized === 'not determined') {
          return 'denied';
        }
        appBluetoothAuthorized = bluetoothAuthorized as BluetoothPermissionLayerState;
      }
      logBluetoothAccess('读取微信 App 蓝牙授权状态', {
        bluetoothAuthorized,
        appAuthorizeSetting,
        systemInfo
      });
    } catch (error) {
      logBluetoothAccess('读取 App 级蓝牙权限设置失败', {
        error
      logBluetoothAccess('读取微信 App 蓝牙授权状态失败', {
        error,
        systemInfo
      });
    }
    try {
      const setting = await Taro.getSetting();
      const authSetting = setting?.authSetting || {};
      logBluetoothAccess('回退读取小程序权限设置', {
        bluetoothScope: authSetting?.[BLUETOOTH_SCOPE],
        authSetting
      });
      if (Object.prototype.hasOwnProperty.call(authSetting, BLUETOOTH_SCOPE)) {
        return authSetting[BLUETOOTH_SCOPE] === true ? 'authorized' : 'denied';
        miniappBluetoothScope = authSetting[BLUETOOTH_SCOPE] === true ? 'authorized' : 'denied';
      }
      logBluetoothAccess('读取小程序蓝牙 scope 状态', {
        bluetoothScope: authSetting?.[BLUETOOTH_SCOPE],
        authSetting,
        systemInfo
      });
    } catch (error) {
      logBluetoothAccess('回退读取小程序权限设置失败', {
        error
      logBluetoothAccess('读取小程序蓝牙 scope 状态失败', {
        error,
        systemInfo
      });
    }
    logBluetoothAccess('小程序蓝牙权限状态未知');
    return {
      appBluetoothAuthorized,
      miniappBluetoothScope,
      systemInfo
    };
  }, [getBluetoothSystemInfoSnapshot]);
  /**
   * 读取当前小程序蓝牙授权状态。
   */
  const getMiniappBluetoothAuthState = useCallback(async () => {
    const snapshot = await readBluetoothPermissionSnapshot();
    logBluetoothAccess('聚合蓝牙权限快照', snapshot);
    // 优先判断拒绝态:微信 App 没权限 或 小程序没授权,都视为拒绝
    if (snapshot.appBluetoothAuthorized === 'denied' || snapshot.miniappBluetoothScope === 'denied') {
      return 'denied';
    }
    // 其次判断授权态:必须小程序 scope 已授权(App 层即使是 unknown 只要没拒绝就放行)
    if (snapshot.miniappBluetoothScope === 'authorized') {
      return 'authorized';
    }
    logBluetoothAccess('蓝牙权限状态未知,等待用户触发授权或系统回传', snapshot);
    return 'unknown';
  }, []);
  }, [readBluetoothPermissionSnapshot]);
  const openBluetoothPermissionSetting = useCallback(async (scene: string, snapshot?: Partial<BluetoothPermissionSnapshot>) => {
    const nextSnapshot = snapshot || await readBluetoothPermissionSnapshot();
    const openAppAuthorizeSetting = (Taro as any).openAppAuthorizeSetting;
    const shouldOpenWechatAppSetting = nextSnapshot.appBluetoothAuthorized === 'denied' && typeof openAppAuthorizeSetting === 'function';
    logBluetoothAccess('准备打开蓝牙相关设置页', {
      scene,
      shouldOpenWechatAppSetting,
      ...nextSnapshot
    });
    if (shouldOpenWechatAppSetting) {
      await openAppAuthorizeSetting();
      return 'wechat_app_setting';
    }
    await Taro.openSetting();
    return 'miniapp_setting';
  }, [readBluetoothPermissionSnapshot]);
  /**
   * 已确认拿到小程序蓝牙权限后,统一收口后续初始化与兜底提示。
@@ -259,17 +331,21 @@
  const refreshBluetoothStateImpl = async () => {
    let hasMiniappAuth = Boolean(Taro.getStorageSync(BLUETOOTH_AUTH_KEY));
    const isAuthorizeProtectionActive = (Date.now() - recentAuthorizedAtRef.current) < BLUETOOTH_AUTH_PROTECTION_WINDOW_MS;
    const systemInfoSnapshot = getBluetoothSystemInfoSnapshot();
    logBluetoothAccess('开始刷新蓝牙状态', {
      cachedMiniappAuth: hasMiniappAuth,
      tripStatus,
      isAuthorizeProtectionActive
      isAuthorizeProtectionActive,
      bluetoothAuthSuccess,
      systemInfo: systemInfoSnapshot
    });
    try {
      const appAuthorizeSetting = await Promise.resolve((Taro as any).getAppAuthorizeSetting?.());
      const bluetoothAuthorized = appAuthorizeSetting?.bluetoothAuthorized;
      logBluetoothAccess('刷新蓝牙状态时读取设置', {
        bluetoothAuthorized,
        appAuthorizeSetting
        appAuthorizeSetting,
        systemInfo: systemInfoSnapshot
      });
      if (bluetoothAuthorized === 'authorized') {
        hasMiniappAuth = true;
@@ -277,7 +353,8 @@
      }
    } catch (error) {
      logBluetoothAccess('刷新蓝牙状态时读取 App 级权限失败', {
        error
        error,
        systemInfo: systemInfoSnapshot
      });
    }
@@ -296,7 +373,8 @@
    const syncMiniappAuthAfterSystemPrompt = async (scene: string) => {
      const latestMiniappAuthState = await getMiniappBluetoothAuthState();
      logBluetoothAccess(`${scene}后再次确认小程序蓝牙权限`, {
        latestMiniappAuthState
        latestMiniappAuthState,
        systemInfo: systemInfoSnapshot
      });
      if (latestMiniappAuthState === 'authorized') {
        hasMiniappAuth = true;
@@ -309,6 +387,13 @@
    let systemBluetoothEnabled = false;
    const openResult = await openBluetoothAdapterSafe();
    logBluetoothAccess('openBluetoothAdapter 结果补充诊断', {
      opened: openResult.opened,
      errorType: openResult.errorType,
      errCode: openResult.error?.errCode,
      errMsg: openResult.error?.errMsg,
      systemInfo: systemInfoSnapshot
    });
    if (openResult.opened) {
      systemBluetoothEnabled = true;
      if (!hasMiniappAuth) {
@@ -322,10 +407,17 @@
      const authGrantedAfterPrompt = await syncMiniappAuthAfterSystemPrompt('蓝牙权限弹窗处理');
      if (!authGrantedAfterPrompt && !isAuthorizeProtectionActive) {
        hasMiniappAuth = false;
        logBluetoothAccess('openBluetoothAdapter 返回 permission_deny,当前按未授权态处理', {
          appPermissionLikelyDenied: true,
          systemInfo: systemInfoSnapshot,
          errCode: openResult.error?.errCode,
          errMsg: openResult.error?.errMsg
        });
      } else if (!authGrantedAfterPrompt) {
        logBluetoothAccess('授权保护窗口内忽略瞬时 permission_deny', {
          errCode: openResult.error?.errCode,
          errMsg: openResult.error?.errMsg
          errMsg: openResult.error?.errMsg,
          systemInfo: systemInfoSnapshot
        });
      }
    } else {
@@ -338,7 +430,8 @@
      opened: openResult.opened,
      errorType: openResult.errorType,
      errCode: openResult.error?.errCode,
      errMsg: openResult.error?.errMsg
      errMsg: openResult.error?.errMsg,
      systemInfo: systemInfoSnapshot
    });
    if (!systemBluetoothEnabled) {
@@ -470,7 +563,10 @@
  // 跳转系统蓝牙设置,并在返回后触发一次状态刷新
  const handleOpenSystemBluetoothSetting = useCallback(async () => {
    logBluetoothAccess('用户点击去开启系统蓝牙');
    const systemInfoSnapshot = getBluetoothSystemInfoSnapshot();
    logBluetoothAccess('用户点击去开启系统蓝牙', {
      systemInfo: systemInfoSnapshot
    });
    try {
      const openSystemBluetoothSetting = (Taro as any).openSystemBluetoothSetting;
      if (typeof openSystemBluetoothSetting === 'function') {
@@ -480,17 +576,20 @@
      }
    } catch (error) {
      logBluetoothAccess('打开系统蓝牙设置失败', {
        error
        error,
        systemInfo: systemInfoSnapshot
      });
      Taro.showToast({ title: '请手动开启系统蓝牙', icon: 'none' });
    } finally {
      logBluetoothAccess('系统蓝牙设置流程结束,准备刷新状态');
      logBluetoothAccess('系统蓝牙设置流程结束,准备刷新状态', {
        systemInfo: systemInfoSnapshot
      });
      startOpenSettingPollingRef.current();
      setTimeout(() => {
        refreshBluetoothState();
      }, 300);
    }
  }, [refreshBluetoothState]);
  }, [getBluetoothSystemInfoSnapshot, refreshBluetoothState]);
  // 打开蓝牙授权弹窗
  const handleOpenAuthBluetoothPopup = () => setShowAuthBluetoothPopup(true);
@@ -504,26 +603,34 @@
  // 执行小程序蓝牙授权,成功后展示成功弹窗
  const handleAuthorizeBluetooth = useCallback(async () => {
    const miniappBluetoothAuthState = await getMiniappBluetoothAuthState();
    const permissionSnapshot = await readBluetoothPermissionSnapshot();
    logBluetoothAccess('用户触发蓝牙授权', {
      miniappBluetoothAuthState
      miniappBluetoothAuthState,
      ...permissionSnapshot
    });
    if (miniappBluetoothAuthState === 'denied') {
      try {
        await Taro.openSetting();
        const openedSettingType = await openBluetoothPermissionSetting('bluetooth_denied', permissionSnapshot);
        logBluetoothAccess('已从拒绝态打开权限设置页', {
          openedSettingType
        });
      } catch (openSettingError) {
        console.error('打开设置失败:', openSettingError);
        logBluetoothAccess('蓝牙权限已拒绝,打开设置失败', {
          openSettingError
          openSettingError,
          ...permissionSnapshot
        });
        Taro.showToast({ title: '请先授权小程序蓝牙权限', icon: 'none' });
        Taro.showToast({ title: getBluetoothPermissionDeniedMessage(permissionSnapshot), icon: 'none' });
        return;
      }
      const latestAuthState = await getMiniappBluetoothAuthState();
      const latestSnapshot = await readBluetoothPermissionSnapshot();
      logBluetoothAccess('从设置页返回后重新读取蓝牙权限', {
        latestAuthState
        latestAuthState,
        ...latestSnapshot
      });
      if (latestAuthState !== 'authorized') {
        Taro.showToast({ title: '请先授权小程序蓝牙权限', icon: 'none' });
        Taro.showToast({ title: getBluetoothPermissionDeniedMessage(latestSnapshot), icon: 'none' });
        return;
      }
      await finishBluetoothAuthorization();
@@ -536,31 +643,38 @@
    } catch (authorizeError) {
      console.log('授权蓝牙失败:', authorizeError);
      logBluetoothAccess('调用 authorize(scope.bluetooth) 失败', {
        authorizeError
        authorizeError,
        ...permissionSnapshot
      });
      try {
        await Taro.openSetting();
        const openedSettingType = await openBluetoothPermissionSetting('authorize_failed', permissionSnapshot);
        logBluetoothAccess('authorize 失败后已打开权限设置页', {
          openedSettingType
        });
      } catch (openSettingError) {
        console.log('打开设置失败:', openSettingError);
        logBluetoothAccess('authorize 失败后打开设置失败', {
          openSettingError
          openSettingError,
          ...permissionSnapshot
        });
        Taro.showToast({ title: '请先授权小程序蓝牙权限', icon: 'none' });
        Taro.showToast({ title: getBluetoothPermissionDeniedMessage(permissionSnapshot), icon: 'none' });
        return;
      }
      const latestAuthState = await getMiniappBluetoothAuthState();
      const latestSnapshot = await readBluetoothPermissionSnapshot();
      logBluetoothAccess('authorize 失败后从设置页返回,重新读取蓝牙权限', {
        latestAuthState
        latestAuthState,
        ...latestSnapshot
      });
      if (latestAuthState !== 'authorized') {
        Taro.showToast({ title: '请先授权小程序蓝牙权限', icon: 'none' });
        Taro.showToast({ title: getBluetoothPermissionDeniedMessage(latestSnapshot), icon: 'none' });
        return;
      }
      await finishBluetoothAuthorization();
      return;
    }
    await finishBluetoothAuthorization();
  }, [finishBluetoothAuthorization, getMiniappBluetoothAuthState]);
  }, [finishBluetoothAuthorization, getMiniappBluetoothAuthState, openBluetoothPermissionSetting, readBluetoothPermissionSnapshot]);
  // 蓝牙卡片点击分发:未开蓝牙->去开启,未授权->弹授权,已授权->弹成功态说明
  const handleBluetoothCardClick = useCallback(() => {
src/robotaxi/pages/begin-route/index.tsx
@@ -593,7 +593,8 @@
      return {
        ...baseRule,
        showPickup: baseRule.showPickup, // 行程中(车辆已过上车点)即使在修改目的地态也不展示上车点,沿用基础规则
        showDropoff: true,
        // 预览态强制展示下车点;确认修改后(本地覆盖态)回退至基础规则,避免接驾态错误渲染目的地,且不影响送驾态的正常显示
        showDropoff: isModifyDestinationReviewVisible ? true : baseRule.showDropoff,
        showCar: !isTripBeforePickupArrived && baseRule.showCar,
        showRoute: true,
        showWalking: false,
@@ -2201,8 +2202,14 @@
    ? (Number(fareBreakdown?.orderMoney) || 0)
    : (Number(fareEstimate?.orderMoney) || 0);
  // 渲染时优先使用 record*/set* 记录的最新值(ref),ref 为空再回退到 state,保证下次渲染拿到最新值
  const mapLongitude = longitudeRef.current ?? longitude ?? 116.397128;
  const mapLatitude = latitudeRef.current ?? latitude ?? 39.916527;
  // 【修复频繁切换目的地后地图闪现北京故宫的问题】
  // 当页面重新挂载且定位接口(getLocation)还未返回时,longitude/latitude 状态为 null。
  // 若直接使用 116.397128(故宫) 兜底,地图会先飞到北京,等定位回来后又瞬间拉回,造成视觉跳跃。
  // 解决方案:在定位空窗期,优先复用本地缓存已恢复的业务点位(上车点 -> 下车点 -> 车辆位置)作为过渡中心点。
  const fallbackLongitude = startSite?.longitude ?? effectiveEndSite?.longitude ?? vehicleLocation?.longitude ?? 116.397128;
  const fallbackLatitude = startSite?.latitude ?? effectiveEndSite?.latitude ?? vehicleLocation?.latitude ?? 39.916527;
  const mapLongitude = longitudeRef.current ?? longitude ?? fallbackLongitude;
  const mapLatitude = latitudeRef.current ?? latitude ?? fallbackLatitude;
  const canToggleRouteSelect = isPickupRouteChangeMode
    ? isPickupRouteChangeEligible
    : allowSelectableRoutes && routeOptions.length > 1;
src/robotaxi/pages/select-map-site/hooks/use-select-map-site.ts
@@ -721,6 +721,26 @@
    }
    reportConfirmMapSiteEvent();
    const modifySession = Taro.getStorageSync(MODIFY_DESTINATION_SESSION_KEY); // 修改目的地会话中将终点写入候选缓存,返回行程页进入预览态
    // 【修改目的地拦截补充】接驾中选点相同或距离过近拦截
    if (modifySession?.active) {
      const isPickupTripStatus = [4, 5].includes(modifySession.tripStatus); // 4: VehicleApproaching, 5: VehicleArrivedPickup
      if (isPickupTripStatus) {
        const isSameSite = isSameModifyDestinationSite(startSiteCache, state.selectedSite);
        const isTooClose = !isWithinStartEndMinDistance({
          startSite: startSiteCache,
          endSite: state.selectedSite,
          minDistance: distanceConfig?.minDistance
        });
        if (isSameSite || isTooClose) {
          state.setTipType(ORDER_DISTANCE_TIP_TYPE_MIN_DISTANCE);
          state.setShowTipPopup(true);
          return;
        }
      }
    }
    if (modifySession?.active && isSameModifyDestinationSite(modifySession?.previousEndSite, state.selectedSite)) {
      Taro.setStorageSync(MODIFY_DESTINATION_PENDING_SAME_SITE_TOAST_KEY, 1);
    }
src/robotaxi/pages/site-search/index.tsx
@@ -64,6 +64,7 @@
} from '../../constants/confirm-ride-report';
import { hasActiveTripOrderInStorage } from '../../utils/order';
import { withRobotaxiWhitelist } from '../../../utils/whitelist';
import { TripStatus } from '../../constants/trip-status';
function SiteSearchPage() {
  const [searchValue, setSearchValue] = useState('') //搜索值
@@ -160,6 +161,30 @@
        return;
      }
    } catch { }
    // 【修改目的地拦截补充】
    // 正常下单流程在上方已由 isWithinStartEndMinDistance 处理。
    // 但在“接驾中修改目的地”时,如果后台距离配置(minDistance)未下发或为 0,底层判断会被放行。
    // 为了防止用户在接驾阶段将目的地改得和原上车点一样(或物理距离极近),此处增加针对 modifySession 活跃状态下的强校验:
    // 1. isSameSite:坐标、ID或名称一致视为同一站点。
    // 2. isTooClose:物理直线距离小于后台配置的 minDistance(如有)。
    // 命中任一条件,即阻断并提示“建议步行前往”。
    if (modifySession?.active) {
      const isPickupTripStatus = [TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup].includes(modifySession.tripStatus);
      if (isPickupTripStatus) {
        const isSameSite = isSameModifyDestinationSite(startSiteCache, item);
        const isTooClose = !isWithinStartEndMinDistance({
          startSite: startSiteCache,
          endSite: item,
          minDistance: resolvedDistanceConfig?.minDistance
        });
        if (isSameSite || isTooClose) {
          setTipType(ORDER_DISTANCE_TIP_TYPE_MIN_DISTANCE);
          setShowTipPopup(true);
          return;
        }
      }
    }
    // 检查是否与上一个选择的终点相同
    if (modifySession?.active && isSameModifyDestinationSite(modifySession?.previousEndSite, item)) {
      Taro.setStorageSync(MODIFY_DESTINATION_PENDING_SAME_SITE_TOAST_KEY, 1);