|
@@ -0,0 +1,363 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <view class="connect-printer-page">
|
|
|
|
|
+ <!-- 提示信息图片 - 搜索中显示 gif,搜索完成显示 jpg -->
|
|
|
|
|
+ <view class="info-images">
|
|
|
|
|
+ <image v-if="isSearching" class="w-750 h-311" src="@/static/images/print/print_dt_ssly.gif" mode="widthFix" />
|
|
|
|
|
+ <image v-else class="w-750 h-311" src="@/static/images/print/print_jt_ssly.jpg" mode="widthFix" />
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 操作按钮区域 -->
|
|
|
|
|
+ <view class="pd-24 action-buttons">
|
|
|
|
|
+ <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="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="f-s-30 c-#333 f-w-5 mb-20">请选择设备</view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 搜索状态提示 -->
|
|
|
|
|
+ <view v-if="isSearching && devices.length === 0" class="searching-tip">
|
|
|
|
|
+ <text>正在搜索附近的蓝牙设备...</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>
|
|
|
|
|
+ <view class="device-status">
|
|
|
|
|
+ <text v-if="connectedDevice?.deviceId === device.deviceId" class="status-connected">已连接</text>
|
|
|
|
|
+ <text v-else class="status-new">未连接</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 无设备提示 -->
|
|
|
|
|
+ <view v-if="!isSearching && devices.length === 0" class="no-device-tip">
|
|
|
|
|
+ <ut-empty description="暂无可用设备" />
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup lang="ts">
|
|
|
|
|
+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;
|
|
|
|
|
+
|
|
|
|
|
+const toast = (title: string) => {
|
|
|
|
|
+ uni.showToast({ title, icon: 'none', duration: 2000 });
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+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;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ await stopBluetoothDevicesDiscovery();
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ // ignore
|
|
|
|
|
+ }
|
|
|
|
|
+ stopDiscoveryListener?.();
|
|
|
|
|
+ stopDiscoveryListener = null;
|
|
|
|
|
+ isSearching.value = false;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const initBluetooth = async () => {
|
|
|
|
|
+ if (isInitialized.value) return;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ await checkBluetoothPermission();
|
|
|
|
|
+ await initBluetoothAdapter();
|
|
|
|
|
+
|
|
|
|
|
+ // 获取当前蓝牙模块下已发现及已连接的设备(去重)
|
|
|
|
|
+ const list = await getBluetoothDevices();
|
|
|
|
|
+ devices.value = [];
|
|
|
|
|
+ if (Array.isArray(list)) {
|
|
|
|
|
+ list.forEach((item) => updateDeviceList(item));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ isInitialized.value = true;
|
|
|
|
|
+ toast('蓝牙初始化成功,请点击开始搜索');
|
|
|
|
|
+ } catch (error: any) {
|
|
|
|
|
+ console.error('蓝牙初始化失败', error);
|
|
|
|
|
+ toast(error?.message || '蓝牙初始化失败,请检查系统蓝牙权限');
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const handleStartSearch = async () => {
|
|
|
|
|
+ if (isSearching.value) return;
|
|
|
|
|
+
|
|
|
|
|
+ if (!isInitialized.value) {
|
|
|
|
|
+ toast('请先初始化蓝牙,再开始搜索');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ isSearching.value = true;
|
|
|
|
|
+
|
|
|
|
|
+ if (searchTimeout) {
|
|
|
|
|
+ clearTimeout(searchTimeout);
|
|
|
|
|
+ }
|
|
|
|
|
+ searchTimeout = setTimeout(() => {
|
|
|
|
|
+ stopSearch();
|
|
|
|
|
+ toast('搜索超时,已自动停止');
|
|
|
|
|
+ }, 15000);
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const list = await getBluetoothDevices();
|
|
|
|
|
+ devices.value = [];
|
|
|
|
|
+ if (Array.isArray(list)) {
|
|
|
|
|
+ list.forEach((item) => updateDeviceList(item));
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ // ignore
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const state = await getBluetoothAdapterState();
|
|
|
|
|
+ if (state.discovering) {
|
|
|
|
|
+ await stopBluetoothDevicesDiscovery();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ stopDiscoveryListener = await startBluetoothDevicesDiscovery((device) => {
|
|
|
|
|
+ updateDeviceList(device);
|
|
|
|
|
+ });
|
|
|
|
|
+ } catch (error: any) {
|
|
|
|
|
+ console.error('蓝牙搜索失败', error);
|
|
|
|
|
+ toast(error?.message || '蓝牙扫描失败,请重试');
|
|
|
|
|
+ isSearching.value = false;
|
|
|
|
|
+ if (searchTimeout) {
|
|
|
|
|
+ clearTimeout(searchTimeout);
|
|
|
|
|
+ searchTimeout = null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const handleConnectDevice = async (device: any) => {
|
|
|
|
|
+ if (!device?.deviceId) {
|
|
|
|
|
+ toast('无效设备,请重试');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (connectedDevice.value?.deviceId === device.deviceId) {
|
|
|
|
|
+ toast('已连接该设备');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ 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 {
|
|
|
|
|
+ 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;
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+ 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;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ padding: 30rpx;
|
|
|
|
|
+ margin-bottom: 10rpx;
|
|
|
|
|
+ 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;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .error-tip {
|
|
|
|
|
+ margin: 20rpx;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|