Ver código fonte

链接设备

huangxw 2 semanas atrás
pai
commit
bca5abdd87

+ 4 - 5
src/plant/packaging/list/index.vue

@@ -7,9 +7,9 @@
         <view class="pd3-24-24-0">
             <view class="d-flex a-c">
                 <view class="min-w-230 flex1">
-                    <ut-action-sheet v-model="form.processType" :tabs="[{ label: '全部', value: '', elTagClass: '15' }, ...pt_pack_code_type]" mode="custom" @change="onRefresh" title="选择入库类型">
+                    <ut-action-sheet v-model="form.refType" :tabs="[{ label: '全部', value: '', elTagClass: '15' }, ...pt_pack_ref_type]" mode="custom" @change="onRefresh" title="选择入库类型">
                         <view class="d-flex search-select-item a-c">
-                            <view class="flex1 ov-hd f-s-28 c-333 text-center f-w-5 up-line-1">{{ selectDictLabel(pt_pack_code_type, form.processType) || '全部' }} </view>
+                            <view class="flex1 ov-hd f-s-28 c-333 text-center f-w-5 up-line-1">{{ selectDictLabel(pt_pack_ref_type, form.refType) || '全部' }} </view>
                             <up-icon size="24rpx" color="#333" name="arrow-down-fill" class="mr-5"></up-icon>
                         </view>
                     </ut-action-sheet>
@@ -18,7 +18,6 @@
                     <ut-search ref="searchRef" v-model="form.keyword" @search="changeSeach" margin="0" :border="false" placeholder="搜品种名称、批号" bgColor="#fff" height="86rpx" borderRadius="10rpx"></ut-search>
                 </view>
             </view>
-            <view></view>
         </view>
         <view class="pd-24 bg-#f7f7f7">
             <up-swipe-action>
@@ -44,9 +43,9 @@ import { selectDictListClass } from '@/utils/ruoyi';
 import { getStorageRoomNames } from '@/utils/common';
 import FreshGoodsBottom from './model/fresh-goods-bottom.vue';
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
-const { pt_pack_code_type } = toRefs<any>(proxy?.useDict('pt_pack_code_type'));
+const { pt_pack_ref_type } = toRefs<any>(proxy?.useDict('pt_pack_ref_type'));
 const list = ref<any[]>();
-const form = ref({ keyword: '' });
+const form = ref({ keyword: '', refType: '' });
 const paging = ref();
 const showAddDialog = ref(false);
 const tabs = ref([

+ 9 - 1
src/plant/print/print-steps/index.vue

@@ -10,7 +10,7 @@
         <view>
             <template v-show="currentStep === 'connect'">
                 <!-- 业务组件链接打印机 -->
-                <connect-printer></connect-printer>
+                <connect-printer :info="stepsObject[currentStep]" @next="nextSteps" prevStepValue="confirm" nextStepValue="print"></connect-printer>
             </template>
         </view>
     </z-paging>
@@ -46,5 +46,13 @@ const stepsObject = reactive<any>({
 const currentStep = ref('connect');
 const paging = ref(null);
 onLoad((options: any) => {});
+const nextSteps = (data: any) => {
+    if (!data.info || !data?.info?.value) return
+    stepsObject[data?.info?.value] = {
+        ...data.info,
+    };
+    stepsObject[data?.nextStepValue].status = 'in-progress';
+    currentStep.value = data?.nextStepValue;
+};
 </script>
 <style lang="scss" scoped></style>

+ 190 - 350
src/plant/print/print-steps/models/connect-printer.vue

@@ -8,420 +8,260 @@
 
         <!-- 操作按钮区域 -->
         <view class="pd-24 action-buttons">
-            <up-button 
-                @click="handleStartSearch" 
-                :loading="isSearching"
-                class="mb-30" 
-                type="primary"
-                :disabled="isConnected"
-            >
+            <up-button @click="handleStartSearch" class="mb-30" type="primary" :disabled="isConnected">
                 {{ isSearching ? '搜索中...' : '开始搜索' }}
             </up-button>
-            <up-button 
-                v-if="isConnected"
-                @click="handleDisconnect" 
-                class="mb-30" 
-                type="warn"
-            >
-                断开连接
-            </up-button>
-            <up-button plain type="primary">上一步</up-button>
+            <up-button v-if="isConnected" @click="handleDisconnect" class="mb-30" type="warning"> 断开连接 </up-button>
+            <view class="d-flex a-c j-c">
+                <up-button class="mr-30" plain type="primary">上一步</up-button>
+                <up-button @click="nextSteps" type="primary" plain>下一步</up-button>
+            </view>
         </view>
 
         <!-- 设备列表区域 -->
         <view class="device-list-section">
-            <view class="section-title">请选择设备</view>
-            
+            <view class="f-s-30 c-#333 f-w-5 mb-20">请选择设备</view>
+
             <!-- 搜索状态提示 -->
-            <view v-if="isSearching && discoveredDevices.length === 0" class="searching-tip">
+            <view v-if="isSearching && devices.length === 0" class="searching-tip">
                 <text>正在搜索附近的蓝牙设备...</text>
             </view>
 
-            <!-- 已配对设备列表 -->
-            <view v-if="pairedDevices.length > 0 && !isSearching" class="paired-devices">
-                <view class="section-subtitle">已配对的设备</view>
-                <scroll-view scroll-y class="device-scroll">
-                    <view 
-                        v-for="device in pairedDevices" 
-                        :key="device.deviceId"
-                        class="device-item"
-                        :class="{ active: connectedDevice?.deviceId === device.deviceId }"
-                        @click="handleConnectDevice(device)"
-                    >
-                        <view class="device-info">
-                            <view class="device-name">{{ device.deviceName }}</view>
-                            <view class="device-address">{{ device.address }}</view>
-                        </view>
-                        <view class="device-status">
-                            <text v-if="connectedDevice?.deviceId === device.deviceId" class="status-connected">已连接</text>
-                            <text v-else class="status-paired">已配对</text>
-                        </view>
+            <!-- 设备列表 -->
+            <view v-if="devices.length" class="device-list">
+                <view v-for="device in devices" :key="device.deviceId" class="device-item" :class="{ active: connectedDevice?.deviceId === device.deviceId }" @click="handleConnectDevice(device)">
+                    <view class="device-info">
+                        <view class="device-name">{{ device.name }}</view>
+                        <view class="device-address">{{ device.deviceId }}</view>
+                        <view v-if="device.RSSI !== undefined" class="signal-strength"> 信号:{{ device.RSSI }} dBm </view>
                     </view>
-                </scroll-view>
-            </view>
-
-            <!-- 新发现设备列表 -->
-            <view v-if="discoveredDevices.length > 0 && !isSearching" class="new-devices">
-                <view class="section-subtitle">新发现的设备</view>
-                <scroll-view scroll-y class="device-scroll">
-                    <view 
-                        v-for="device in discoveredDevices" 
-                        :key="device.deviceId"
-                        class="device-item"
-                        :class="{ active: connectedDevice?.deviceId === device.deviceId }"
-                        @click="handleConnectDevice(device)"
-                    >
-                        <view class="device-info">
-                            <view class="device-name">{{ device.deviceName }}</view>
-                            <view class="device-address">{{ device.address }}</view>
-                            <view v-if="device.RSSI !== undefined" class="signal-strength">
-                                信号:{{ device.RSSI }} dBm
-                            </view>
-                        </view>
-                        <view class="device-status">
-                            <text v-if="connectedDevice?.deviceId === device.deviceId" class="status-connected">已连接</text>
-                            <text v-else class="status-new">新设备</text>
-                        </view>
+                    <view class="device-status">
+                        <text v-if="connectedDevice?.deviceId === device.deviceId" class="status-connected">已连接</text>
+                        <text v-else class="status-new">未连接</text>
                     </view>
-                </scroll-view>
+                </view>
             </view>
 
             <!-- 无设备提示 -->
-            <view v-if="!isSearching && pairedDevices.length === 0 && discoveredDevices.length === 0" class="no-device-tip">
+            <view v-if="!isSearching && devices.length === 0" class="no-device-tip">
                 <ut-empty description="暂无可用设备" />
             </view>
         </view>
-
     </view>
 </template>
 
 <script setup lang="ts">
-import { blueDeviceService, BluetoothDeviceInfo } from '@/utils/blue-device-services';
-
-// Props
-const props = defineProps<{
-    // 定义组件需要的 props
-}>();
-
-// Emits
-const emit = defineEmits<{
-    (e: 'connected', device: BluetoothDeviceInfo): void;
-    (e: 'disconnected'): void;
-}>();
-
-// 状态数据
-const isSearching = ref(false);
-const isConnected = ref(false);
-const connectedDevice = ref<BluetoothDeviceInfo | null>(null);
-const pairedDevices = ref<BluetoothDeviceInfo[]>([]);
-const discoveredDevices = ref<BluetoothDeviceInfo[]>([]);
-const errorMessage = ref('');
-
-// 初始化时获取已配对的设备
-onLoad(() => {
-    loadPairedDevices();
+import { checkBluetoothPermission, initBluetoothAdapter, getBluetoothAdapterState, startBluetoothDevicesDiscovery, stopBluetoothDevicesDiscovery, connectBLEDevice, closeBLEConnection, getBLEDeviceServicesAndCharacteristics, getBluetoothDevices } from '@/utils/blue-device-services';
+const props = defineProps({
+    info: {
+        type: Object,
+        default: () => ({})
+    },
+    // 下一个步骤的标识,供父组件使用
+    nextStepValue: {
+        type: String,
+        default: ''
+    },
+    // 上一个步骤标识
+    prevStepValue: {
+        type: String,
+        default: ''
+    }
 });
+const emit = defineEmits(['connected', 'disconnected', 'next', 'prev']);
+const isSearching = ref(false);
+const isInitialized = ref(false);
+const devices = ref<Array<any>>([]);
+const connectedDevice = ref<any | null>(null);
+const isConnected = computed(() => !!connectedDevice.value);
+let stopDiscoveryListener: (() => void) | null = null;
+let searchTimeout: number | null = null;
 
-/**
- * 加载已配对的设备列表
- */
-async function loadPairedDevices(): Promise<void> {
-    try {
-        // 确保蓝牙模块已经初始化(首次进入页面可能还未初始化)
-        try {
-            await blueDeviceService.initializeBluetooth();
-        } catch (initErr: any) {
-            // 如果正在初始化中,则忽略(可能是其它地方已触发初始化)
-            if (typeof initErr === 'string' && initErr.includes('初始化中')) {
-                console.warn('蓝牙初始化进行中,跳过重复初始化');
-            } else {
-                throw initErr;
-            }
-        }
+const toast = (title: string) => {
+    uni.showToast({ title, icon: 'none', duration: 2000 });
+};
 
-        const devices = await blueDeviceService.getBluetoothDevices();
-        pairedDevices.value = devices.filter(d => d.isPaired);
-    } catch (err) {
-        console.error('加载已配对设备失败:', err);
-        showError('加载设备列表失败');
+const updateDeviceList = (device: any) => {
+    if (!device?.deviceId) return;
+    if (device.name.startsWith('未知或不支持的设备')) return;
+    if (!device.name.startsWith('GP-')) return;
+    const idx = devices.value.findIndex((d) => d.deviceId === device.deviceId);
+    if (idx >= 0) {
+        devices.value[idx] = { ...devices.value[idx], ...device };
+    } else {
+        devices.value.unshift(device);
+    }
+};
+
+const stopSearch = async () => {
+    if (searchTimeout) {
+        clearTimeout(searchTimeout);
+        searchTimeout = null;
     }
-}
 
-/**
- * 开始搜索蓝牙设备
- */
-async function handleStartSearch(): Promise<void> {
-    if (isSearching.value) return;
-    
-    resetSearchState();
-    isSearching.value = true;
-    discoveredDevices.value = [];
-    
     try {
-        // 初始化蓝牙
-        await blueDeviceService.initializeBluetooth();
-        
-        // 设置回调
-        blueDeviceService.setOnDeviceFound(handleDeviceFound);
-        blueDeviceService.setOnConnectionChanged(handleConnectionChanged);
-        blueDeviceService.setOnError(handleError);
-        
-        // 开始搜索(可以指定要搜索的服务 UUID)
-        await blueDeviceService.startDiscoveryBluetoothDevices([
-            '000018f0-0000-1000-8000-00805f9b34fb', // 打印机服务
-            '6e400001-b5a3-f393-e0a9-e50e24dcca9e'  // BLE 打印机常用服务
-        ]);
-        
-        // 30 秒后自动停止搜索
-        setTimeout(() => {
-            stopSearch();
-        }, 30000);
-        
+        await stopBluetoothDevicesDiscovery();
     } catch (err) {
-        console.error('搜索设备失败:', err);
-        showError(`搜索失败:${err}`);
-        stopSearch();
+        // ignore
     }
-}
-
-/**
- * 停止搜索
- */
-function stopSearch(): void {
+    stopDiscoveryListener?.();
+    stopDiscoveryListener = null;
     isSearching.value = false;
-    blueDeviceService.stopDiscoveryBluetoothDevices().catch(console.error);
-}
+};
 
-/**
- * 重置搜索状态
- */
-function resetSearchState(): void {
-    errorMessage.value = '';
-    discoveredDevices.value = [];
-}
+const initBluetooth = async () => {
+    if (isInitialized.value) return;
 
-/**
- * 处理设备发现
- */
-function handleDeviceFound(device: BluetoothDeviceInfo): void {
-    // 检查是否已经存在于列表中
-    const exists = pairedDevices.value.some(d => d.deviceId === device.deviceId) ||
-                   discoveredDevices.value.some(d => d.deviceId === device.deviceId);
-    
-    if (!exists) {
-        discoveredDevices.value.push(device);
-    }
-}
+    try {
+        await checkBluetoothPermission();
+        await initBluetoothAdapter();
 
-/**
- * 处理连接状态变化
- */
-function handleConnectionChanged(connected: boolean, device?: BluetoothDeviceInfo): void {
-    if (connected && device) {
-        isConnected.value = true;
-        connectedDevice.value = device;
-        
-        // 从发现列表中移除,添加到已配对列表
-        discoveredDevices.value = discoveredDevices.value.filter(d => d.deviceId !== device.deviceId);
-        if (!pairedDevices.value.some(d => d.deviceId === device.deviceId)) {
-            pairedDevices.value.push(device);
+        // 获取当前蓝牙模块下已发现及已连接的设备(去重)
+        const list = await getBluetoothDevices();
+        devices.value = [];
+        if (Array.isArray(list)) {
+            list.forEach((item) => updateDeviceList(item));
         }
-        
-        emit('connected', device);
-        uni.showToast({
-            title: '连接成功',
-            icon: 'success'
-        });
-    } else {
-        isConnected.value = false;
-        connectedDevice.value = null;
-        emit('disconnected');
-    }
-}
 
-/**
- * 处理错误
- */
-function handleError(error: string): void {
-    showError(error);
-}
+        isInitialized.value = true;
+        toast('蓝牙初始化成功,请点击开始搜索');
+    } catch (error: any) {
+        console.error('蓝牙初始化失败', error);
+        toast(error?.message || '蓝牙初始化失败,请检查系统蓝牙权限');
+    }
+};
 
-/**
- * 显示错误信息
- */
-function showError(message: string): void {
-    errorMessage.value = message;
-    uni.showToast({
-        title: message,
-        icon: 'none'
-    });
-}
+const handleStartSearch = async () => {
+    if (isSearching.value) return;
 
-/**
- * 连接设备
- */
-async function handleConnectDevice(device: BluetoothDeviceInfo): Promise<void> {
-    if (isSearching.value) {
-        uni.showToast({
-            title: '搜索中,请稍后',
-            icon: 'none'
-        });
+    if (!isInitialized.value) {
+        toast('请先初始化蓝牙,再开始搜索');
         return;
     }
-    
-    if (connectedDevice.value?.deviceId === device.deviceId) {
-        return; // 已经连接
-    }
-    
-    try {
-        // 确保蓝牙模块已初始化
-        try {
-            await blueDeviceService.initializeBluetooth();
-        } catch (initErr: any) {
-            if (typeof initErr === 'string' && initErr.includes('初始化中')) {
-                console.warn('蓝牙初始化进行中,跳过重复初始化');
-            } else {
-                throw initErr;
-            }
-        }
 
-        // 连接设备
-        const connectedInfo = await blueDeviceService.connectBluetoothDevice(device.deviceId, 10000);
-        connectedDevice.value = connectedInfo;
-
-        // 获取设备服务
-        await getDeviceServices(device.deviceId);
+    isSearching.value = true;
 
-    } catch (err) {
-        console.error('连接设备失败:', err);
-        showError(`连接失败:${err}`);
+    if (searchTimeout) {
+        clearTimeout(searchTimeout);
     }
-}
-
+    searchTimeout = setTimeout(() => {
+        stopSearch();
+        toast('搜索超时,已自动停止');
+    }, 15000);
 
-/**
- * 获取设备服务
- */
-async function getDeviceServices(deviceId: string): Promise<void> {
     try {
-        const services = await blueDeviceService.getBluetoothDeviceServices(deviceId);
-        console.log('设备服务:', services);
-        
-        // 遍历服务并获取特征值
-        for (const service of services) {
-            if (service.isPrimary) {
-                const characteristics = await blueDeviceService.getBluetoothDeviceCharacteristics(
-                    deviceId,
-                    service.uuid
-                );
-                console.log(`服务 ${service.uuid} 的特征值:`, characteristics);
-                
-                // 可以根据需要启用通知或写入
-                for (const characteristic of characteristics) {
-                    if (characteristic.properties.notify) {
-                        // 启用通知
-                        await blueDeviceService.notifyCharacteristicValue(
-                            deviceId,
-                            service.uuid,
-                            characteristic.uuid,
-                            true
-                        );
-                    }
-                }
-            }
+        const list = await getBluetoothDevices();
+        devices.value = [];
+        if (Array.isArray(list)) {
+            list.forEach((item) => updateDeviceList(item));
         }
-    } catch (err) {
-        console.error('获取设备服务失败:', err);
+    } catch {
+        // ignore
     }
-}
 
-/**
- * 断开连接
- */
-async function handleDisconnect(): Promise<void> {
     try {
-        await blueDeviceService.disconnectBluetoothDevice();
-        isConnected.value = false;
-        connectedDevice.value = null;
-        emit('disconnected');
-        
-        uni.showToast({
-            title: '已断开连接',
-            icon: 'success'
+        const state = await getBluetoothAdapterState();
+        if (state.discovering) {
+            await stopBluetoothDevicesDiscovery();
+        }
+
+        stopDiscoveryListener = await startBluetoothDevicesDiscovery((device) => {
+            updateDeviceList(device);
         });
-    } catch (err) {
-        console.error('断开连接失败:', err);
-        showError('断开连接失败');
+    } catch (error: any) {
+        console.error('蓝牙搜索失败', error);
+        toast(error?.message || '蓝牙扫描失败,请重试');
+        isSearching.value = false;
+        if (searchTimeout) {
+            clearTimeout(searchTimeout);
+            searchTimeout = null;
+        }
     }
-}
+};
 
-/**
- * 写入数据到设备
- */
-async function writeToDevice(
-    serviceId: string,
-    characteristicId: string,
-    data: string | Uint8Array | ArrayBuffer
-): Promise<void> {
-    if (!connectedDevice.value) {
-        throw new Error('未连接设备');
+const handleConnectDevice = async (device: any) => {
+    if (!device?.deviceId) {
+        toast('无效设备,请重试');
+        return;
     }
-    
-    await blueDeviceService.writeCharacteristicValue(
-        connectedDevice.value.deviceId,
-        serviceId,
-        characteristicId,
-        data
-    );
-}
 
-/**
- * 打印数据
- */
-async function printData(data: string | Uint8Array | ArrayBuffer): Promise<void> {
-    if (!connectedDevice.value) {
-        throw new Error('未连接设备');
+    if (connectedDevice.value?.deviceId === device.deviceId) {
+        toast('已连接该设备');
+        return;
     }
-    
-    await blueDeviceService.printData(data);
-}
 
-// 暴露方法给模板使用
-defineExpose({
-    writeToDevice,
-    printData,
-    isConnected,
-    connectedDevice
+    try {
+        await stopSearch();
+        await connectBLEDevice(device.deviceId);
+
+        connectedDevice.value = device;
+        updateDeviceList(device);
+
+        const { services, characteristicsMap } = await getBLEDeviceServicesAndCharacteristics(device.deviceId);
+        console.log('已获取服务', services);
+        console.log('已获取特征值', characteristicsMap);
+        toast('蓝牙已连接');
+    } catch (error: any) {
+        console.error('连接失败', error);
+        toast(error?.message || '设备连接失败,请重试');
+    }
+};
+
+const handleDisconnect = async () => {
+    if (!connectedDevice.value?.deviceId) return;
+
+    try {
+        await closeBLEConnection(connectedDevice.value.deviceId);
+        toast('已断开连接');
+    } catch (error) {
+        console.warn('断开连接失败', error);
+    }
+    connectedDevice.value = null;
+};
+const nextSteps = () => {
+    if (!connectedDevice.value) {
+        toast('请先连接设备');
+        return;
+    }
+   emit('next', {
+      info: props.info,
+      nextStepValue: props.nextStepValue || 'print'
+   });
+};
+onMounted(() => {
+    initBluetooth();
+});
+onBeforeUnmount(() => {
+    stopSearch();
 });
 </script>
 
 <style lang="scss" scoped>
 .connect-printer-page {
-    min-height: 100vh;
     background-color: #f5f5f5;
-    
+
     .info-images {
         display: flex;
         flex-direction: column;
         align-items: center;
         padding: 20rpx;
         background-color: #fff;
-        
+
         image {
             margin-bottom: 20rpx;
         }
     }
-    
+
     .action-buttons {
         background-color: #fff;
         padding: 20rpx;
     }
-    
+
     .device-list-section {
         background-color: #fff;
         padding: 20rpx;
         margin-top: 20rpx;
-        
+
         .section-title {
             font-size: 32rpx;
             font-weight: bold;
@@ -429,33 +269,33 @@ defineExpose({
             padding-bottom: 20rpx;
             border-bottom: 1rpx solid #eee;
         }
-        
+
         .section-subtitle {
             font-size: 28rpx;
             color: #666;
             margin: 20rpx 0;
         }
-        
+
         .searching-tip {
             display: flex;
             flex-direction: column;
             align-items: center;
             padding: 60rpx 0;
             color: #999;
-            
+
             text {
                 margin-top: 20rpx;
             }
         }
-        
+
         .no-device-tip {
             padding: 60rpx 0;
         }
-        
+
         .device-scroll {
             max-height: 600rpx;
         }
-        
+
         .device-item {
             display: flex;
             justify-content: space-between;
@@ -465,49 +305,49 @@ defineExpose({
             background-color: #fafafa;
             border-radius: 10rpx;
             transition: all 0.3s;
-            
+
             &.active {
                 background-color: #e8f4ff;
                 border: 2rpx solid #1989fa;
             }
-            
+
             &:active {
                 opacity: 0.8;
             }
-            
+
             .device-info {
                 flex: 1;
-                
+
                 .device-name {
                     font-size: 30rpx;
                     color: #333;
                     font-weight: 500;
                 }
-                
+
                 .device-address {
                     font-size: 24rpx;
                     color: #999;
                     margin-top: 8rpx;
                 }
-                
+
                 .signal-strength {
                     font-size: 24rpx;
                     color: #67c23a;
                     margin-top: 8rpx;
                 }
             }
-            
+
             .device-status {
                 .status-connected {
                     font-size: 24rpx;
                     color: #67c23a;
                 }
-                
+
                 .status-paired {
                     font-size: 24rpx;
                     color: #909399;
                 }
-                
+
                 .status-new {
                     font-size: 24rpx;
                     color: #1989fa;
@@ -515,7 +355,7 @@ defineExpose({
             }
         }
     }
-    
+
     .error-tip {
         margin: 20rpx;
     }

+ 155 - 578
src/utils/blue-device-services.ts

@@ -1,604 +1,181 @@
 /**
- * 低功耗蓝牙设备服务 (BLE - Bluetooth Low Energy)
- * 提供蓝牙设备的初始化、搜索、连接、配对和数据写入等功能
- * 使用 uni-app 的 BLE API
+ * 蓝牙相关封装,面向 uni-app 的 API。
+ *
+ * 说明:
+ *  - 仅支持蓝牙低功耗(BLE),适用于打印机等外设。
+ *  - 组件内必须先调用 initBluetoothAdapter 之后才可开始搜索/连接。
  */
 
-// 蓝牙设备信息接口
-export interface BluetoothDeviceInfo {
-  deviceId: string;      // 设备 ID
-  deviceName: string;    // 设备名称
-  address: string;       // 设备地址
-  RSSI?: number;         // 信号强度
-  isPaired?: boolean;    // 是否已配对
-}
-
-// 蓝牙服务特征值接口
-export interface BluetoothServiceCharacteristic {
-  serviceId: string;     // 服务 UUID
-  characteristicId: string; // 特征值 UUID
-}
-
-// 蓝牙服务类
-export class BlueDeviceServices {
-  private bluetoothAdapter: any = null;
-  private connectedDeviceId: string = '';
-  private isConnected: boolean = false;
-  private isInitializing: boolean = false;
-  
-  // 回调函数
-  private onDeviceFound: ((device: BluetoothDeviceInfo) => void) | null = null;
-  private onConnectionChanged: ((connected: boolean, device?: BluetoothDeviceInfo) => void) | null = null;
-  private onReceiveData: ((data: ArrayBuffer) => void) | null = null;
-  private onError: ((error: string) => void) | null = null;
-
-  /**
-   * 初始化蓝牙管理器
-   */
-  async initializeBluetooth(): Promise<boolean> {
-    return new Promise((resolve, reject) => {
-      if (this.isInitializing) {
-        reject('蓝牙正在初始化中');
-        return;
-      }
-
-      this.isInitializing = true;
-
-      const ensureBluetoothScope = () => {
-        return new Promise<void>((resolveScope, rejectScope) => {
-          uni.getSetting({
-            success: (res: any) => {
-              const hasScope = res.authSetting?.['scope.bluetooth'];
-              if (hasScope) {
-                resolveScope();
-                return;
-              }
-
-              uni.authorize({
-                scope: 'scope.bluetooth',
-                success: () => {
-                  resolveScope();
-                },
-                fail: (authErr: any) => {
-                  console.warn('蓝牙权限授权失败:', authErr);
-                  rejectScope(authErr);
-                }
-              });
-            },
-            fail: (err: any) => {
-              console.warn('获取权限设置失败:', err);
-              rejectScope(err);
-            }
-          });
-        });
-      };
-
-      const handleInitFail = (err: any) => {
-        console.error('蓝牙适配器初始化失败:', err);
-        this.isInitializing = false;
-        this.onError?.(`蓝牙适配器初始化失败:${err.message || JSON.stringify(err)}`);
-        reject(err);
-      };
-
-      // 先检查权限
-      ensureBluetoothScope().then(() => {
-        uni.openBluetoothAdapter({
-          success: () => {
-            console.log('蓝牙适配器初始化成功');
-            this.isInitializing = false;
-            resolve(true);
-          },
-          fail: handleInitFail
-        });
-      }).catch((err) => {
-        // 权限被拒绝或获取失败,提示用户打开设置
-        this.isInitializing = false;
-        this.onError?.('蓝牙权限未授权,请到系统设置中开启');
-        reject(err);
-      });
-    });
-  }
-
-  /**
-   * 关闭蓝牙适配器
-   */
-  async closeBluetoothAdapter(): Promise<void> {
-    return new Promise((resolve, reject) => {
-      try {
-        uni.closeBluetoothAdapter({
-          success: () => {
-            console.log('蓝牙适配器已关闭');
-            this.resetState();
-            resolve();
-          },
-          fail: (err: any) => {
-            console.error('关闭蓝牙适配器失败:', err);
-            reject(err);
-          }
-        });
-      } catch (error) {
-        reject(error);
-      }
-    });
-  }
-
-  /**
-   * 重置蓝牙状态
-   */
-  private resetState(): void {
-    this.connectedDeviceId = '';
-    this.isConnected = false;
-    this.onConnectionChanged?.(false);
-  }
-
-  /**
-   * 设置设备发现回调
-   */
-  setOnDeviceFound(callback: (device: BluetoothDeviceInfo) => void): void {
-    this.onDeviceFound = callback;
-  }
-
-  /**
-   * 设置连接状态变化回调
-   */
-  setOnConnectionChanged(callback: (connected: boolean, device?: BluetoothDeviceInfo) => void): void {
-    this.onConnectionChanged = callback;
-  }
-
-  /**
-   * 设置数据接收回调
-   */
-  setOnReceiveData(callback: (data: ArrayBuffer) => void): void {
-    this.onReceiveData = callback;
-  }
-
-  /**
-   * 设置错误回调
-   */
-  setOnError(callback: (error: string) => void): void {
-    this.onError = callback;
-  }
-
-  /**
-   * 获取所有已配对的蓝牙设备列表
-   */
-  async getBluetoothDevices(): Promise<BluetoothDeviceInfo[]> {
-    return new Promise((resolve, reject) => {
-      uni.getBluetoothDevices({
-        success: (res: any) => {
-          const devices: BluetoothDeviceInfo[] = Object.values(res.devices).map((device: any) => ({
-            deviceId: device.deviceId,
-            deviceName: device.name || '未知设备',
-            address: device.address,
-            RSSI: device.RSSI,
-            isPaired: device.isPaired || false
-          }));
-          resolve(devices);
-        },
-        fail: (err: any) => {
-          console.error('获取蓝牙设备列表失败:', err);
-          reject(err);
-        }
-      });
-    });
-  }
-
-  /**
-   * 开始搜索附近的蓝牙设备
-   * @param services 要搜索的服务 UUID 列表(可选)
-   */
-  async startDiscoveryBluetoothDevices(services?: string[]): Promise<boolean> {
-    return new Promise((resolve, reject) => {
-      if (!this.bluetoothAdapter) {
-        reject('请先初始化蓝牙模块');
-        return;
-      }
-
-      const options: Record<string, any> = {
-        success: () => {
-          console.log('开始搜索蓝牙设备');
-          resolve(true);
-        },
-        fail: (err: any) => {
-          console.error('搜索蓝牙设备失败:', err);
-          reject(err);
-        }
-      };
-
-      // 如果有指定服务 UUID,添加到选项中
-      if (services && services.length > 0) {
-        options.services = services;
-      }
-
-      // 监听设备发现
-      this.bluetoothAdapter.onDeviceFound((res: any) => {
-        const device: BluetoothDeviceInfo = {
-          deviceId: res.device.deviceId,
-          deviceName: res.device.name || '未知设备',
-          address: res.device.address,
-          RSSI: res.device.RSSI,
-          isPaired: res.device.isPaired || false
-        };
-        
-        console.log('发现新设备:', device);
-        this.onDeviceFound?.(device);
-      });
-
-      this.bluetoothAdapter.startDiscoveryBluetoothDevices(options);
-    });
-  }
-
-  /**
-   * 停止搜索蓝牙设备
-   */
-  async stopDiscoveryBluetoothDevices(): Promise<void> {
-    return new Promise((resolve, reject) => {
-      if (!this.bluetoothAdapter) {
-        resolve();
-        return;
-      }
-
-      this.bluetoothAdapter.stopDiscoveryBluetoothDevices({
-        success: () => {
-          console.log('停止搜索蓝牙设备');
-          resolve();
-        },
-        fail: (err: any) => {
-          console.error('停止搜索蓝牙设备失败:', err);
-          reject(err);
+export async function checkBluetoothPermission(): Promise<void> {
+  return new Promise((resolve, reject) => {
+    uni.getSetting({
+      success: (res) => {
+        const hasLocation = res.authSetting?.['scope.userLocation']
+        if (hasLocation) {
+          return resolve()
         }
-      });
-    });
-  }
-
-  /**
-   * 连接蓝牙设备(建立 BLE 连接)
-   * @param deviceId 设备 ID
-   * @param timeout 超时时间(毫秒),默认 10000
-   */
-  async connectBluetoothDevice(deviceId: string, timeout: number = 10000): Promise<BluetoothDeviceInfo> {
-    return new Promise((resolve, reject) => {
-      if (!this.bluetoothAdapter) {
-        reject('请先初始化蓝牙模块');
-        return;
-      }
-
-      const connectTimeout = setTimeout(() => {
-        reject(new Error('连接超时'));
-      }, timeout);
-
-      // 监听连接成功
-      this.bluetoothAdapter.onConnectSuccess(() => {
-        clearTimeout(connectTimeout);
-        console.log('连接蓝牙设备成功:', deviceId);
-        this.connectedDeviceId = deviceId;
-        this.isConnected = true;
-        
-        // 获取设备信息
-        this.getDeviceInfo(deviceId).then((info) => {
-          this.onConnectionChanged?.(true, info);
-          resolve(info);
-        }).catch(reject);
-      });
 
-      // 监听连接失败
-      this.bluetoothAdapter.onConnectFail(() => {
-        clearTimeout(connectTimeout);
-        console.error('连接蓝牙设备失败');
-        this.isConnected = false;
-        reject(new Error('连接失败'));
-      });
+        uni.authorize({
+          scope: 'scope.userLocation',
+          success: () => resolve(),
+          fail: (err) => {
+            uni.showModal({
+              title: '权限提示',
+              content: '需要定位权限才能扫描蓝牙设备,请前往设置开启定位权限。',
+              showCancel: false,
+              confirmText: '去设置',
+              success: () => {
+                uni.openSetting({})
+              },
+            })
+            reject(err)
+          },
+        })
+      },
+      fail: reject,
+    })
+  })
+}
 
-      // 发起 BLE 连接
-      uni.createBLEConnection({
-        deviceId,
-        success: () => {
-          console.log('发起蓝牙连接请求');
+export async function initBluetoothAdapter(): Promise<void> {
+  const closeAdapter = () =>
+    new Promise<void>((resolve) => {
+      uni.closeBluetoothAdapter({
+        success: () => resolve(),
+        fail: () => resolve(),
+      })
+    })
+
+  const openAdapter = () =>
+    new Promise<void>((resolve, reject) => {
+      uni.openBluetoothAdapter({
+        success: () => resolve(),
+        fail: (err) => {
+          console.log(err)
+          reject(new Error(err.errMsg || `openBluetoothAdapter(${mode}) 失败`))
         },
-        fail: (err: any) => {
-          clearTimeout(connectTimeout);
-          console.error('发起蓝牙连接失败:', err);
-          reject(err);
-        }
-      });
-    });
-  }
+      })
+    })
 
-  /**
-   * 断开蓝牙连接
-   */
-  async disconnectBluetoothDevice(): Promise<void> {
-    return new Promise((resolve, reject) => {
-      if (!this.connectedDeviceId) {
-        resolve();
-        return;
-      }
+  // 先尝试关闭现有 adapter,避免重复初始化导致的异常
+  await closeAdapter()
 
-      uni.closeBLEConnection({
-        deviceId: this.connectedDeviceId,
-        success: () => {
-          console.log('断开蓝牙连接成功');
-          this.resetState();
-          resolve();
-        },
-        fail: (err: any) => {
-          console.error('断开蓝牙连接失败:', err);
-          reject(err);
-        }
-      });
-    });
-  }
+  // iOS 需要分别以主机/从机模式初始化一次,才能兼容全部场景。
+  const platform = uni.getSystemInfoSync?.()?.platform || ''
+  console.log(platform, 'initBluetoothAdapter platform')
 
-  /**
-   * 获取设备信息
-   */
-  private async getDeviceInfo(deviceId: string): Promise<BluetoothDeviceInfo> {
-    return {
-      deviceId,
-      deviceName: '未知设备',
-      address: deviceId,
-      isPaired: false
-    };
-  }
+  // Android 等其他平台只需 central 模式
+  await openAdapter()
+}
 
-  /**
-   * 获取设备支持的所有服务
-   */
-  async getBluetoothDeviceServices(deviceId: string): Promise<any[]> {
-    return new Promise((resolve, reject) => {
-      uni.getBLEDeviceServices({
-        deviceId,
-        success: (res: any) => {
-          console.log('获取设备服务成功:', res.services);
-          resolve(res.services);
-        },
-        fail: (err: any) => {
-          console.error('获取设备服务失败:', err);
-          reject(err);
-        }
-      });
-    });
-  }
+export async function getBluetoothAdapterState(): Promise<any> {
+  return new Promise((resolve, reject) => {
+    uni.getBluetoothAdapterState({
+      success: resolve,
+      fail: reject,
+    })
+  })
+}
 
-  /**
-   * 获取指定服务的特征值
-   */
-  async getBluetoothDeviceCharacteristics(
-    deviceId: string, 
-    serviceId: string
-  ): Promise<any[]> {
-    return new Promise((resolve, reject) => {
-      uni.getBLEDeviceCharacteristics({
-        deviceId,
-        serviceId,
-        success: (res: any) => {
-          console.log('获取特征值成功:', res.characteristics);
-          resolve(res.characteristics);
-        },
-        fail: (err: any) => {
-          console.error('获取特征值失败:', err);
-          reject(err);
+export async function startBluetoothDevicesDiscovery(
+  onDeviceFound: (device: any) => void,
+): Promise<() => void> {
+  return new Promise((resolve, reject) => {
+    let listener: any | null = null
+
+    uni.startBluetoothDevicesDiscovery({
+      allowDuplicatesKey: false,
+      success: () => {
+        listener = (res: any) => {
+          if (!res || !res.devices) return
+          res.devices.forEach((device: any) => {
+            if (device) onDeviceFound(device)
+          })
         }
-      });
-    });
-  }
 
-  /**
-   * 读取特征值
-   */
-  async readCharacteristicValue(
-    deviceId: string,
-    serviceId: string,
-    characteristicId: string
-  ): Promise<ArrayBuffer> {
-    return new Promise((resolve, reject) => {
-      uni.readBLECharacteristicValue({
-        deviceId,
-        serviceId,
-        characteristicId,
-        success: (res: any) => {
-          console.log('读取特征值成功');
-          resolve(res.value);
-        },
-        fail: (err: any) => {
-          console.error('读取特征值失败:', err);
-          reject(err);
-        }
-      });
-    });
-  }
+        uni.onBluetoothDeviceFound(listener)
 
-  /**
-   * 订阅特征值变化
-   */
-  async notifyCharacteristicValue(
-    deviceId: string,
-    serviceId: string,
-    characteristicId: string,
-    enable: boolean = true
-  ): Promise<void> {
-    return new Promise((resolve, reject) => {
-      uni.notifyBLECharacteristicValueChange({
-        deviceId,
-        serviceId,
-        characteristicId,
-        state: enable,
-        success: () => {
-          console.log(`${enable ? '启用' : '禁用'}特征值通知成功`);
-          
-          if (enable) {
-            // 添加监听
-            this.bluetoothAdapter?.onBLECharacteristicValueChange((res: any) => {
-              if (res.deviceId === deviceId && 
-                  res.serviceId === serviceId && 
-                  res.characteristicId === characteristicId) {
-                this.onReceiveData?.(res.value);
-              }
-            });
+        resolve(() => {
+          if (listener) {
+            uni.offBluetoothDeviceFound()
+            listener = null
           }
-          
-          resolve();
-        },
-        fail: (err: any) => {
-          console.error(`${enable ? '启用' : '禁用'}特征值通知失败:`, err);
-          reject(err);
-        }
-      });
-    });
-  }
+        })
+      },
+      fail: reject,
+    })
+  })
+}
 
-  /**
-   * 写入数据到特征值
-   * @param deviceId 设备 ID
-   * @param serviceId 服务 UUID
-   * @param characteristicId 特征值 UUID
-   * @param value 要写入的数据(ArrayBuffer 或 Uint8Array)
-   * @param needResponse 是否需要响应,默认 true
-   */
-  async writeCharacteristicValue(
-    deviceId: string,
-    serviceId: string,
-    characteristicId: string,
-    value: ArrayBuffer | Uint8Array,
-    needResponse: boolean = true
-  ): Promise<void> {
-    return new Promise((resolve, reject) => {
-      // 转换为 ArrayBuffer
-      let buffer: ArrayBuffer;
-      if (value instanceof Uint8Array) {
-        buffer = value.buffer as ArrayBuffer;
-      } else if (value instanceof ArrayBuffer) {
-        buffer = value;
-      } else {
-        // 如果是字符串,转换为 UTF-8 编码
-        const encoder = new TextEncoder();
-        buffer = encoder.encode(value as string).buffer as ArrayBuffer;
-      }
+export async function stopBluetoothDevicesDiscovery(): Promise<void> {
+  return new Promise((resolve, reject) => {
+    uni.stopBluetoothDevicesDiscovery({
+      success: () => resolve(),
+      fail: reject,
+    })
+  })
+}
 
-      uni.writeBLECharacteristicValue({
-        deviceId,
-        serviceId,
-        characteristicId,
-        value: buffer,
-        success: () => {
-          console.log('写入特征值成功');
-          resolve();
-        },
-        fail: (err: any) => {
-          console.error('写入特征值失败:', err);
-          reject(err);
-        }
-      });
-    });
-  }
+export async function connectBLEDevice(deviceId: string): Promise<void> {
+  return new Promise((resolve, reject) => {
+    uni.createBLEConnection({
+      deviceId,
+      success: resolve,
+      fail: reject,
+    })
+  })
+}
 
-  /**
-   * 打印数据到蓝牙打印机
-   * @param data 要打印的数据
-   * @param printerServiceId 打印机服务 UUID
-   * @param printerCharacteristicId 打印机特征值 UUID
-   */
-  async printData(
-    data: string | Uint8Array,
-    printerServiceId: string = '000018f0-0000-1000-8000-00805f9b34fb',
-    printerCharacteristicId: string = '00002af1-0000-1000-8000-00805f9b34fb'
-  ): Promise<void> {
-    if (!this.connectedDeviceId) {
-      throw new Error('未连接蓝牙设备');
-    }
+export async function closeBLEConnection(deviceId: string): Promise<void> {
+  return new Promise((resolve, reject) => {
+    uni.closeBLEConnection({
+      deviceId,
+      success: resolve,
+      fail: reject,
+    })
+  })
+}
 
-    await this.writeCharacteristicValue(
-      this.connectedDeviceId,
-      printerServiceId,
-      printerCharacteristicId,
-      data
-    );
-  }
+export async function getBluetoothDevices(): Promise<any[]> {
+  return new Promise((resolve, reject) => {
+    uni.getBluetoothDevices({
+      success: (res: any) => resolve(res.devices || []),
+      fail: reject,
+    })
+  })
+}
 
-  /**
-   * 检查设备是否已连接
-   */
-  isConnectedToDevice(): boolean {
-    return this.isConnected;
-  }
+export async function getBLEDeviceServicesAndCharacteristics(
+  deviceId: string,
+): Promise<{ services: any[]; characteristicsMap: Record<string, any[]> }> {
+  const services = await new Promise<any[]>((resolve, reject) => {
+    uni.getBLEDeviceServices({
+      deviceId,
+      success: (res: any) => resolve(res.services || []),
+      fail: reject,
+    })
+  })
 
-  /**
-   * 获取当前连接的设备信息
-   */
-  getCurrentDevice(): BluetoothDeviceInfo | null {
-    if (!this.connectedDeviceId) {
-      return null;
-    }
-    
-    return {
-      deviceId: this.connectedDeviceId,
-      deviceName: '未知设备',
-      address: this.connectedDeviceId
-    };
-  }
+  const characteristicsMap: Record<string, any[]> = {}
 
-  /**
-   * 查找并连接指定的蓝牙打印机
-   * @param nameFilter 设备名称过滤器(部分匹配)
-   * @param timeout 超时时间
-   */
-  async findAndConnectPrinter(
-    nameFilter: string = '',
-    timeout: number = 15000
-  ): Promise<BluetoothDeviceInfo> {
-    return new Promise(async (resolve, reject) => {
+  await Promise.all(
+    services.map(async (service) => {
       try {
-        // 初始化蓝牙
-        await this.initializeBluetooth();
-        
-        // 开始搜索
-        await this.startDiscoveryBluetoothDevices();
-        
-        let foundDevice: BluetoothDeviceInfo | null = null;
-        
-        // 设置设备发现回调
-        this.setOnDeviceFound((device) => {
-          // 检查是否符合过滤条件
-          const matchesFilter = !nameFilter || 
-            device.deviceName.includes(nameFilter) ||
-            device.deviceName.toLowerCase().includes(nameFilter.toLowerCase());
-          
-          if (matchesFilter) {
-            foundDevice = device;
-            console.log('找到目标设备:', device);
-            
-            // 停止搜索
-            this.stopDiscoveryBluetoothDevices();
-            
-            // 连接设备
-            this.connectBluetoothDevice(device.deviceId, timeout)
-              .then(resolve)
-              .catch(reject);
-          }
-        });
-        
-        // 超时处理
-        const searchTimeout = setTimeout(() => {
-          this.stopDiscoveryBluetoothDevices();
-          if (!foundDevice) {
-            reject(new Error('未找到匹配的蓝牙设备'));
-          }
-        }, timeout);
-        
-        // 监听连接成功
-        this.setOnConnectionChanged((connected, device) => {
-          if (connected && device) {
-            clearTimeout(searchTimeout);
-            resolve(device);
-          }
-        });
-        
-      } catch (error) {
-        reject(error);
+        const characteristics = await new Promise<any[]>((resolve, reject) => {
+          uni.getBLEDeviceCharacteristics({
+            deviceId,
+            serviceId: service.uuid,
+            success: (res: any) => resolve(res.characteristics || []),
+            fail: reject,
+          })
+        })
+        characteristicsMap[service.uuid] = characteristics
+      } catch (err) {
+        // 忽略某些服务获取特征值失败
+        console.warn('getBLEDeviceCharacteristics fail', service.uuid, err)
       }
-    });
-  }
-}
+    }),
+  )
 
-// 导出单例实例
-export const blueDeviceService = new BlueDeviceServices();
+  return { services, characteristicsMap }
+}