import Taro from '@tarojs/taro';
|
import { showToast } from '../../../components/toast';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { TripStatus } from '../../../constants/trip-status';
|
import { waitForOrderSuccessSubscribePopup } from '../../../../utils/subscribe';
|
|
type Deps = {
|
tripStatus: TripStatus;
|
orderId?: string;
|
bluetoothAuthSuccess: boolean;
|
startBluetoothAuth: () => void;
|
/** 当前订单已开门时,跳过后续自动蓝牙鉴权,避免反复连接 */
|
skipBluetoothAuth?: boolean;
|
/** 车端钥匙下发失败(getUnlocktToken 接口非 200),蓝牙不可解锁 */
|
unlockTokenFailed?: boolean;
|
/** 派单成功转场动效是否进行中:为 true 时接驾中(4)暂缓初始化蓝牙,待动效完成后再初始化 */
|
dispatchTransitionActive?: boolean;
|
};
|
|
type BluetoothAccessStatus = 'system_off' | 'unauthorized' | 'authorized';
|
type BluetoothErrorType = 'system_off' | 'permission_deny' | 'env_unsupported' | 'unknown';
|
type MiniappBluetoothAuthState = 'authorized' | 'denied' | 'unknown';
|
type BluetoothOpenResult = {
|
opened: boolean;
|
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;
|
const SYSTEM_OFF_ERROR_RE = /(not available|10001|未开启|开关未打开|bluetooth adapter is not available)/i;
|
const ENV_UNSUPPORTED_ERROR_RE = /(暂不支持|not support|devtools|Mac 以外)/i;
|
const PERMISSION_DENY_ERROR_RE = /deny|auth deny|authorize|permission|scope/i;
|
const BLUETOOTH_ACCESS_LOG_PREFIX = '[蓝牙权限]';
|
const BLUETOOTH_AUTH_SYNC_DELAY_MS = 300; // 授权完成后首轮刷新延迟,给小程序权限态一点同步时间
|
const BLUETOOTH_AUTH_PROTECTION_WINDOW_MS = 5000; // 授权成功后的保护窗口,避免瞬时 permission_deny 覆盖 UI
|
const SUBSCRIBE_POPUP_WAIT_TIMEOUT_MS = 8000; // 进入接驾态时等待订阅弹窗结束的最长兜底时间,超时后仍放行蓝牙初始化
|
// 蓝牙图标资源:点亮(已授权)与熄灭(未开/未授权)两态
|
const BLUETOOTH_ICON_ON = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/bluetooth-icon-20260706.png';
|
export const BLUETOOTH_ICON_OFF = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/close-bluetooth-20260716.png';
|
// 车端钥匙下发失败时的描述文案与文字颜色
|
const UNLOCK_TOKEN_FAILED_TEXT = '蓝牙解锁失败, 请点击右下方按键解锁车门';
|
const UNLOCK_TOKEN_FAILED_COLOR = '#FF4D4F';
|
|
/**
|
* 兼容读取小程序蓝牙权限状态。
|
* 优先使用新版 App 级权限接口,旧环境再回退到 getSetting。
|
*/
|
async function readMiniappBluetoothAuthState() : Promise<MiniappBluetoothAuthState> {
|
try {
|
const appAuthorizeSetting = await Promise.resolve((Taro as any).getAppAuthorizeSetting?.());
|
const bluetoothAuthorized = appAuthorizeSetting?.bluetoothAuthorized;
|
if (typeof bluetoothAuthorized === 'string') {
|
if (bluetoothAuthorized === 'authorized') {
|
return 'authorized';
|
}
|
if (bluetoothAuthorized === 'denied') {
|
return 'denied';
|
}
|
}
|
} catch (error) {
|
}
|
|
try {
|
const setting = await Taro.getSetting();
|
const authSetting = setting?.authSetting || {};
|
if (Object.prototype.hasOwnProperty.call(authSetting, BLUETOOTH_SCOPE)) {
|
return authSetting[BLUETOOTH_SCOPE] === true ? 'authorized' : 'denied';
|
}
|
} catch (error) {
|
}
|
|
return 'unknown';
|
}
|
|
/**
|
* 输出蓝牙权限链路日志,便于排查授权、系统蓝牙和环境问题。
|
*/
|
function logBluetoothAccess(message: string, payload?: Record<string, unknown>) {
|
if (payload) {
|
console.log(BLUETOOTH_ACCESS_LOG_PREFIX, message, payload);
|
return;
|
}
|
console.log(BLUETOOTH_ACCESS_LOG_PREFIX, message);
|
}
|
|
function getBluetoothPermissionDeniedMessage(snapshot?: Partial<BluetoothPermissionSnapshot>) {
|
if (snapshot?.appBluetoothAuthorized === 'denied') {
|
return '请先在微信设置中授权蓝牙';
|
}
|
return '请先授权蓝牙权限';
|
}
|
|
/**
|
* 统一分类蓝牙打开失败原因。
|
*/
|
function classifyBluetoothError(error: any): BluetoothErrorType {
|
const errCode = Number(error?.errCode || 0);
|
const errMsg = String(error?.errMsg || '');
|
if (errCode === 10001 || SYSTEM_OFF_ERROR_RE.test(errMsg)) {
|
return 'system_off';
|
}
|
if (ENV_UNSUPPORTED_ERROR_RE.test(errMsg)) {
|
return 'env_unsupported';
|
}
|
if (PERMISSION_DENY_ERROR_RE.test(errMsg)) {
|
return 'permission_deny';
|
}
|
return 'unknown';
|
}
|
|
/**
|
* 安全打开蓝牙适配器,返回统一结果结构。
|
*/
|
async function openBluetoothAdapterSafe(): Promise<BluetoothOpenResult> {
|
try {
|
await Taro.openBluetoothAdapter();
|
logBluetoothAccess('openBluetoothAdapter 成功');
|
return { opened: true, errorType: 'unknown', error: null };
|
} catch (error: any) {
|
const errorType = classifyBluetoothError(error);
|
logBluetoothAccess('openBluetoothAdapter 失败', {
|
errorType,
|
errCode: error?.errCode,
|
errMsg: error?.errMsg
|
});
|
return {
|
opened: false,
|
errorType,
|
error
|
};
|
}
|
}
|
|
/**
|
* 蓝牙能力编排:
|
* 1. 系统蓝牙未开启
|
* 2. 系统蓝牙已开启,但小程序未授权
|
* 3. 系统蓝牙已开启,且小程序已授权
|
*/
|
export default function useBluetoothAccess({
|
tripStatus,
|
orderId,
|
bluetoothAuthSuccess,
|
startBluetoothAuth,
|
skipBluetoothAuth = false,
|
unlockTokenFailed = false,
|
dispatchTransitionActive = false
|
}: Deps) {
|
const [status, setStatus] = useState<BluetoothAccessStatus>('system_off'); // 当前蓝牙访问态:系统未开 / 小程序未授权 / 已授权
|
const [statusReady, setStatusReady] = useState(false); // 蓝牙状态是否已完成首轮判定
|
const [showAuthBluetoothPopup, setShowAuthBluetoothPopup] = useState(false); // 蓝牙授权引导弹窗展示态
|
const [showSuccessAuthPopup, setShowSuccessAuthPopup] = useState(false); // 蓝牙授权成功弹窗展示态
|
const adapterStateHandlerRef = useRef<((res: any) => void) | null>(null); // 蓝牙适配器状态监听函数引用
|
const openSettingPollingTimerRef = useRef<ReturnType<typeof setInterval> | null>(null); // 去开启后的短轮询定时器
|
const openSettingPollingCountRef = useRef(0); // 去开启后的短轮询计数
|
const startBluetoothAuthRef = useRef(startBluetoothAuth); // 蓝牙鉴权动作引用,避免函数引用变化导致状态 effect 反复触发
|
const recentAuthorizedAtRef = useRef(0); // 最近一次确认蓝牙授权成功的时间戳,用于保护首次授权后的 UI 状态
|
const stopOpenSettingPollingRef = useRef<() => void>(() => {});
|
const refreshBluetoothStateRef = useRef<() => Promise<void>>(async () => {});
|
const startOpenSettingPollingRef = useRef<() => void>(() => {});
|
const startBluetoothStateWatchRef = useRef<() => void>(() => {});
|
const stopBluetoothStateWatchRef = useRef<() => void>(() => {});
|
const triggerBluetoothAuthRef = useRef<(scene: string) => void>(() => {});
|
|
triggerBluetoothAuthRef.current = (scene: string) => {
|
if (skipBluetoothAuth) {
|
logBluetoothAccess('当前订单已开门,跳过自动蓝牙鉴权触发', {
|
scene,
|
tripStatus,
|
orderId
|
});
|
return;
|
}
|
startBluetoothAuthRef.current?.();
|
};
|
|
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;
|
if (typeof bluetoothAuthorized === 'string') {
|
appBluetoothAuthorized = bluetoothAuthorized as BluetoothPermissionLayerState;
|
}
|
logBluetoothAccess('读取微信 App 蓝牙授权状态', {
|
bluetoothAuthorized,
|
appAuthorizeSetting,
|
systemInfo
|
});
|
} catch (error) {
|
logBluetoothAccess('读取微信 App 蓝牙授权状态失败', {
|
error,
|
systemInfo
|
});
|
}
|
|
try {
|
const setting = await Taro.getSetting();
|
const authSetting = setting?.authSetting || {};
|
if (Object.prototype.hasOwnProperty.call(authSetting, BLUETOOTH_SCOPE)) {
|
miniappBluetoothScope = authSetting[BLUETOOTH_SCOPE] === true ? 'authorized' : 'denied';
|
}
|
logBluetoothAccess('读取小程序蓝牙 scope 状态', {
|
bluetoothScope: authSetting?.[BLUETOOTH_SCOPE],
|
authSetting,
|
systemInfo
|
});
|
} catch (error) {
|
logBluetoothAccess('读取小程序蓝牙 scope 状态失败', {
|
error,
|
systemInfo
|
});
|
}
|
|
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]);
|
|
/**
|
* 已确认拿到小程序蓝牙权限后,统一收口后续初始化与兜底提示。
|
*/
|
const finishBluetoothAuthorization = useCallback(async () => {
|
logBluetoothAccess('进入授权完成收口流程', {
|
tripStatus
|
});
|
recentAuthorizedAtRef.current = Date.now();
|
Taro.setStorageSync(BLUETOOTH_AUTH_KEY, true);
|
setShowAuthBluetoothPopup(false);
|
setStatus('authorized');
|
setStatusReady(true);
|
triggerBluetoothAuthRef.current('finish_authorization');
|
startOpenSettingPollingRef.current();
|
|
const openResult = await openBluetoothAdapterSafe();
|
logBluetoothAccess('授权完成后再次初始化蓝牙适配器', {
|
opened: openResult.opened,
|
errorType: openResult.errorType,
|
errCode: openResult.error?.errCode,
|
errMsg: openResult.error?.errMsg
|
});
|
if (openResult.opened) {
|
setTimeout(() => {
|
refreshBluetoothStateRef.current();
|
}, BLUETOOTH_AUTH_SYNC_DELAY_MS);
|
return;
|
}
|
|
if (openResult.errorType === 'permission_deny') {
|
logBluetoothAccess('授权成功后权限态仍在同步,保护窗口内维持已授权态', {
|
protectionWindowMs: BLUETOOTH_AUTH_PROTECTION_WINDOW_MS
|
});
|
return;
|
}
|
|
if (openResult.errorType === 'system_off') {
|
recentAuthorizedAtRef.current = 0;
|
stopOpenSettingPollingRef.current();
|
setStatus('system_off');
|
showToast({ title: '请先开启系统蓝牙', icon: 'none' });
|
return;
|
}
|
|
showToast({ title: '蓝牙初始化失败,请稍后重试', icon: 'none' });
|
setTimeout(() => {
|
refreshBluetoothStateRef.current();
|
}, BLUETOOTH_AUTH_SYNC_DELAY_MS);
|
}, [orderId, skipBluetoothAuth, tripStatus]);
|
|
useEffect(() => {
|
startBluetoothAuthRef.current = startBluetoothAuth;
|
}, [startBluetoothAuth]);
|
|
/**
|
* 停止“去开启”后的短轮询。
|
*/
|
const stopOpenSettingPolling = () => {
|
if (openSettingPollingTimerRef.current) {
|
clearInterval(openSettingPollingTimerRef.current);
|
openSettingPollingTimerRef.current = null;
|
}
|
openSettingPollingCountRef.current = 0;
|
};
|
|
stopOpenSettingPollingRef.current = stopOpenSettingPolling;
|
|
/**
|
* 刷新蓝牙状态:同时判断系统蓝牙开关与小程序授权态。
|
*/
|
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,
|
bluetoothAuthSuccess,
|
systemInfo: systemInfoSnapshot
|
});
|
try {
|
const appAuthorizeSetting = await Promise.resolve((Taro as any).getAppAuthorizeSetting?.());
|
const bluetoothAuthorized = appAuthorizeSetting?.bluetoothAuthorized;
|
logBluetoothAccess('刷新蓝牙状态时读取设置', {
|
bluetoothAuthorized,
|
appAuthorizeSetting,
|
systemInfo: systemInfoSnapshot
|
});
|
if (bluetoothAuthorized === 'authorized') {
|
hasMiniappAuth = true;
|
recentAuthorizedAtRef.current = 0;
|
}
|
} catch (error) {
|
logBluetoothAccess('刷新蓝牙状态时读取 App 级权限失败', {
|
error,
|
systemInfo: systemInfoSnapshot
|
});
|
}
|
|
if (!hasMiniappAuth) {
|
const initialMiniappAuthState = await readMiniappBluetoothAuthState();
|
logBluetoothAccess('刷新蓝牙状态时统一确认权限结果', {
|
initialMiniappAuthState
|
});
|
if (initialMiniappAuthState === 'authorized') {
|
hasMiniappAuth = true;
|
Taro.setStorageSync(BLUETOOTH_AUTH_KEY, true);
|
recentAuthorizedAtRef.current = 0;
|
}
|
}
|
|
const syncMiniappAuthAfterSystemPrompt = async (scene: string) => {
|
const latestMiniappAuthState = await getMiniappBluetoothAuthState();
|
logBluetoothAccess(`${scene}后再次确认小程序蓝牙权限`, {
|
latestMiniappAuthState,
|
systemInfo: systemInfoSnapshot
|
});
|
if (latestMiniappAuthState === 'authorized') {
|
hasMiniappAuth = true;
|
Taro.setStorageSync(BLUETOOTH_AUTH_KEY, true);
|
recentAuthorizedAtRef.current = 0;
|
return true;
|
}
|
return false;
|
};
|
|
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) {
|
await syncMiniappAuthAfterSystemPrompt('蓝牙适配器打开');
|
}
|
} else if (openResult.errorType === 'system_off') {
|
systemBluetoothEnabled = false;
|
recentAuthorizedAtRef.current = 0;
|
} else if (openResult.errorType === 'permission_deny') {
|
systemBluetoothEnabled = true;
|
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,
|
systemInfo: systemInfoSnapshot
|
});
|
}
|
} else {
|
// 开发工具限制和未知异常按“系统已开启”兜底,避免误判成“去开启”。
|
systemBluetoothEnabled = true;
|
}
|
logBluetoothAccess('蓝牙状态刷新结果', {
|
systemBluetoothEnabled,
|
hasMiniappAuth,
|
opened: openResult.opened,
|
errorType: openResult.errorType,
|
errCode: openResult.error?.errCode,
|
errMsg: openResult.error?.errMsg,
|
systemInfo: systemInfoSnapshot
|
});
|
|
if (!systemBluetoothEnabled) {
|
setStatus('system_off');
|
setStatusReady(true);
|
logBluetoothAccess('蓝牙状态判定完成', {
|
nextStatus: 'system_off'
|
});
|
return;
|
}
|
const nextStatus = hasMiniappAuth ? 'authorized' : 'unauthorized';
|
setStatus(nextStatus);
|
setStatusReady(true);
|
stopOpenSettingPollingRef.current();
|
if (nextStatus !== 'authorized') {
|
recentAuthorizedAtRef.current = 0;
|
}
|
logBluetoothAccess('蓝牙状态判定完成', {
|
nextStatus
|
});
|
if (nextStatus === 'authorized') {
|
triggerBluetoothAuthRef.current('refresh_state_authorized');
|
}
|
};
|
|
refreshBluetoothStateRef.current = refreshBluetoothStateImpl;
|
|
/**
|
* 对外暴露稳定的蓝牙状态刷新函数。
|
*/
|
const refreshBluetoothState = useCallback(() => {
|
return refreshBluetoothStateRef.current();
|
}, []);
|
|
/**
|
* 开启“去开启”后的短轮询兜底,处理未触发页面 show 的场景。
|
*/
|
const startOpenSettingPollingImpl = () => {
|
stopOpenSettingPollingRef.current();
|
openSettingPollingCountRef.current = 0;
|
logBluetoothAccess('启动设置页返回后的短轮询');
|
openSettingPollingTimerRef.current = setInterval(() => {
|
openSettingPollingCountRef.current += 1;
|
logBluetoothAccess('轮询刷新蓝牙状态', {
|
pollingCount: openSettingPollingCountRef.current
|
});
|
refreshBluetoothStateRef.current();
|
if (openSettingPollingCountRef.current >= 10) {
|
stopOpenSettingPollingRef.current();
|
}
|
}, 500);
|
};
|
|
startOpenSettingPollingRef.current = startOpenSettingPollingImpl;
|
|
/**
|
* 开启蓝牙适配器状态监听,系统层蓝牙开关变化时立即刷新状态。
|
*/
|
const startBluetoothStateWatchImpl = () => {
|
if (adapterStateHandlerRef.current) return;
|
const onBluetoothStateChange = (res: any) => {
|
logBluetoothAccess('收到蓝牙适配器状态变更', {
|
available: res?.available,
|
discovering: res?.discovering
|
});
|
if (res?.available === false) {
|
setStatus('system_off');
|
return;
|
}
|
refreshBluetoothStateRef.current();
|
};
|
const on = (Taro as any).onBluetoothAdapterStateChange;
|
if (typeof on === 'function') {
|
on(onBluetoothStateChange);
|
adapterStateHandlerRef.current = onBluetoothStateChange;
|
}
|
};
|
|
startBluetoothStateWatchRef.current = startBluetoothStateWatchImpl;
|
|
/**
|
* 关闭蓝牙适配器状态监听。
|
*/
|
const stopBluetoothStateWatchImpl = () => {
|
const off = (Taro as any).offBluetoothAdapterStateChange;
|
if (typeof off === 'function' && adapterStateHandlerRef.current) {
|
off(adapterStateHandlerRef.current);
|
}
|
adapterStateHandlerRef.current = null;
|
};
|
|
stopBluetoothStateWatchRef.current = stopBluetoothStateWatchImpl;
|
|
// 仅在 4/5 状态维护蓝牙 UI;其他状态强制收起弹窗
|
useEffect(() => {
|
if ([TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup].includes(tripStatus)) {
|
if (!orderId) {
|
logBluetoothAccess('等待 orderId 就绪');
|
return;
|
}
|
// 接驾中(4)派单成功转场期间:与车控卡片一致,待转场动效完成后再初始化蓝牙,
|
// 避免转场动画播放时触发系统蓝牙授权弹窗。到达上车点(5)不受此限制(需尽快可开门)。
|
if (tripStatus === TripStatus.VehicleApproaching && dispatchTransitionActive) {
|
logBluetoothAccess('派单成功转场动效未完成,暂缓初始化蓝牙');
|
return;
|
}
|
logBluetoothAccess('进入蓝牙 UI 维护态', {
|
tripStatus,
|
orderId
|
});
|
setStatusReady(false);
|
// 方案 B:先等待“下单成功”订阅弹窗结束,再初始化蓝牙
|
// (refreshBluetoothState 内部 openBluetoothAdapter 会触发系统蓝牙授权弹窗),
|
// 保证订阅弹窗与蓝牙授权弹窗按先后顺序出现,避免叠加;
|
// Promise.race 设兜底超时,订阅异常未 complete 时不阻塞蓝牙初始化。
|
let cancelled = false;
|
const subscribeTimeout = new Promise<void>((resolve) => {
|
setTimeout(resolve, SUBSCRIBE_POPUP_WAIT_TIMEOUT_MS);
|
});
|
Promise.race([waitForOrderSuccessSubscribePopup(), subscribeTimeout]).then(() => {
|
if (cancelled) return;
|
logBluetoothAccess('订阅弹窗已结束,开始初始化蓝牙');
|
refreshBluetoothState();
|
});
|
startBluetoothStateWatchRef.current();
|
return () => {
|
cancelled = true;
|
stopBluetoothStateWatchRef.current();
|
};
|
}
|
logBluetoothAccess('离开蓝牙 UI 维护态,重置弹窗与监听', {
|
tripStatus
|
});
|
setStatusReady(false);
|
stopBluetoothStateWatchRef.current();
|
stopOpenSettingPollingRef.current();
|
setShowAuthBluetoothPopup(false);
|
setShowSuccessAuthPopup(false);
|
}, [tripStatus, orderId, refreshBluetoothState, dispatchTransitionActive]);
|
|
// 跳转系统蓝牙设置,并在返回后触发一次状态刷新
|
const handleOpenSystemBluetoothSetting = useCallback(async () => {
|
const systemInfoSnapshot = getBluetoothSystemInfoSnapshot();
|
logBluetoothAccess('用户点击去开启系统蓝牙', {
|
systemInfo: systemInfoSnapshot
|
});
|
try {
|
const openSystemBluetoothSetting = (Taro as any).openSystemBluetoothSetting;
|
if (typeof openSystemBluetoothSetting === 'function') {
|
await openSystemBluetoothSetting();
|
} else {
|
await Taro.openSetting();
|
}
|
} catch (error) {
|
logBluetoothAccess('打开系统蓝牙设置失败', {
|
error,
|
systemInfo: systemInfoSnapshot
|
});
|
showToast({ title: '请手动开启系统蓝牙', icon: 'none' });
|
} finally {
|
logBluetoothAccess('系统蓝牙设置流程结束,准备刷新状态', {
|
systemInfo: systemInfoSnapshot
|
});
|
startOpenSettingPollingRef.current();
|
setTimeout(() => {
|
refreshBluetoothState();
|
}, 300);
|
}
|
}, [getBluetoothSystemInfoSnapshot, refreshBluetoothState]);
|
|
// 打开蓝牙授权弹窗
|
const handleOpenAuthBluetoothPopup = () => setShowAuthBluetoothPopup(true);
|
|
// 关闭蓝牙授权弹窗
|
const handleCloseAuthBluetoothPopup = () => setShowAuthBluetoothPopup(false);
|
|
// 关闭蓝牙授权成功弹窗
|
const handleCloseSuccessAuthPopup = () => setShowSuccessAuthPopup(false);
|
|
// 执行小程序蓝牙授权,成功后展示成功弹窗
|
const handleAuthorizeBluetooth = useCallback(async () => {
|
const miniappBluetoothAuthState = await getMiniappBluetoothAuthState();
|
const permissionSnapshot = await readBluetoothPermissionSnapshot();
|
logBluetoothAccess('用户触发蓝牙授权', {
|
miniappBluetoothAuthState,
|
...permissionSnapshot
|
});
|
if (miniappBluetoothAuthState === 'denied') {
|
try {
|
const openedSettingType = await openBluetoothPermissionSetting('bluetooth_denied', permissionSnapshot);
|
logBluetoothAccess('已从拒绝态打开权限设置页', {
|
openedSettingType
|
});
|
} catch (openSettingError) {
|
console.error('打开设置失败:', openSettingError);
|
logBluetoothAccess('蓝牙权限已拒绝,打开设置失败', {
|
openSettingError,
|
...permissionSnapshot
|
});
|
showToast({ title: getBluetoothPermissionDeniedMessage(permissionSnapshot), icon: 'none' });
|
return;
|
}
|
const latestAuthState = await getMiniappBluetoothAuthState();
|
const latestSnapshot = await readBluetoothPermissionSnapshot();
|
logBluetoothAccess('从设置页返回后重新读取蓝牙权限', {
|
latestAuthState,
|
...latestSnapshot
|
});
|
if (latestAuthState !== 'authorized') {
|
showToast({ title: getBluetoothPermissionDeniedMessage(latestSnapshot), icon: 'none' });
|
return;
|
}
|
await finishBluetoothAuthorization();
|
return;
|
}
|
|
try {
|
await Taro.authorize({ scope: BLUETOOTH_SCOPE });
|
logBluetoothAccess('调用 authorize(scope.bluetooth) 成功');
|
} catch (authorizeError) {
|
console.log('授权蓝牙失败:', authorizeError);
|
logBluetoothAccess('调用 authorize(scope.bluetooth) 失败', {
|
authorizeError,
|
...permissionSnapshot
|
});
|
try {
|
const openedSettingType = await openBluetoothPermissionSetting('authorize_failed', permissionSnapshot);
|
logBluetoothAccess('authorize 失败后已打开权限设置页', {
|
openedSettingType
|
});
|
} catch (openSettingError) {
|
console.log('打开设置失败:', openSettingError);
|
logBluetoothAccess('authorize 失败后打开设置失败', {
|
openSettingError,
|
...permissionSnapshot
|
});
|
showToast({ title: getBluetoothPermissionDeniedMessage(permissionSnapshot), icon: 'none' });
|
return;
|
}
|
const latestAuthState = await getMiniappBluetoothAuthState();
|
const latestSnapshot = await readBluetoothPermissionSnapshot();
|
logBluetoothAccess('authorize 失败后从设置页返回,重新读取蓝牙权限', {
|
latestAuthState,
|
...latestSnapshot
|
});
|
if (latestAuthState !== 'authorized') {
|
showToast({ title: getBluetoothPermissionDeniedMessage(latestSnapshot), icon: 'none' });
|
return;
|
}
|
await finishBluetoothAuthorization();
|
return;
|
}
|
await finishBluetoothAuthorization();
|
}, [finishBluetoothAuthorization, getMiniappBluetoothAuthState, openBluetoothPermissionSetting, readBluetoothPermissionSnapshot]);
|
|
// 蓝牙卡片点击分发:未开蓝牙->去开启,未授权->弹授权,已授权->弹成功态说明
|
const handleBluetoothCardClick = useCallback(() => {
|
logBluetoothAccess('点击蓝牙卡片', {
|
status,
|
tripStatus
|
});
|
if (status === 'system_off') {
|
handleOpenSystemBluetoothSetting();
|
return;
|
}
|
if (status === 'unauthorized') {
|
handleOpenAuthBluetoothPopup();
|
return;
|
}
|
setShowSuccessAuthPopup(true);
|
}, [status, handleOpenSystemBluetoothSetting, handleOpenAuthBluetoothPopup]);
|
|
// 蓝牙卡片主文案
|
const cardDescription = useMemo(() => {
|
if (unlockTokenFailed) {
|
return UNLOCK_TOKEN_FAILED_TEXT;
|
}
|
if (status === 'system_off') {
|
return '系统蓝牙未开启, 无法自动解锁车辆';
|
}
|
if (status === 'unauthorized') {
|
return '授权蓝牙连接, 解锁无感出行';
|
}
|
return '手机靠近车辆,可自动解锁车门';
|
}, [status, unlockTokenFailed]);
|
|
|
|
useEffect(() => {
|
return () => {
|
stopBluetoothStateWatchRef.current();
|
stopOpenSettingPollingRef.current();
|
};
|
}, []);
|
|
// 蓝牙卡片右侧动作文案
|
const cardActionText = useMemo(() => {
|
// 失败态:无右侧动作(不展示按钮文字与箭头)
|
if (unlockTokenFailed) {
|
return '';
|
}
|
if (status === 'system_off') {
|
return '去开启';
|
}
|
if (status === 'authorized') {
|
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 {
|
bluetoothStatus: status,
|
bluetoothStatusReady: statusReady,
|
bluetoothCardDescription: cardDescription,
|
bluetoothCardActionText: cardActionText,
|
bluetoothCardIconSrc: cardIconSrc,
|
bluetoothCardDescriptionColor: cardDescriptionColor,
|
bluetoothCardClickable: cardClickable,
|
showAuthBluetoothPopup,
|
showSuccessAuthPopup,
|
handleBluetoothCardClick,
|
handleOpenAuthBluetoothPopup,
|
handleCloseAuthBluetoothPopup,
|
handleCloseSuccessAuthPopup,
|
handleAuthorizeBluetooth,
|
refreshBluetoothState
|
};
|
}
|