|
|
@@ -1,84 +1,855 @@
|
|
|
<template>
|
|
|
<div>
|
|
|
- <input ref="inputValueRef" v-model="inputValue" placeholder="请输入内容" @keydown.enter="handleInputKeydown" />
|
|
|
- <p>扫描的条形码值: {{ barcode }}</p>
|
|
|
+ <h1>蓝牙重量秤连接</h1>
|
|
|
+
|
|
|
+ <!-- 浏览器支持检查 -->
|
|
|
+ <div v-if="!isBluetoothSupported" class="warning">
|
|
|
+ <p>⚠️ 您的浏览器不支持Web Bluetooth API</p>
|
|
|
+ <p>请使用Chrome、Edge或Opera浏览器,并确保在HTTPS环境下运行</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 环境检查 -->
|
|
|
+ <div v-if="!isHttpsEnvironment && isBluetoothSupported" class="warning">
|
|
|
+ <p>⚠️ Web Bluetooth API 必须在HTTPS环境下使用</p>
|
|
|
+ <p>请部署到HTTPS服务器或使用localhost进行开发</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="status">
|
|
|
+ <p>连接状态: <span :class="['connection-status', getStatusClass()]">{{ connectionStatus }}</span></p>
|
|
|
+ <p v-if="deviceName">已连接设备: {{ deviceName }}</p>
|
|
|
+ <p v-if="receivedData">最新数据: {{ receivedData }}</p>
|
|
|
+ <p v-if="weightValue !== null">重量值: {{ weightValue }} kg</p>
|
|
|
+ <p v-if="isConnecting">请在弹出的设备选择对话框中选择您的重量秤设备...</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 模式选择 -->
|
|
|
+ <div class="mode-selection">
|
|
|
+ <label>
|
|
|
+ <input type="radio" v-model="scanMode" :value="false">
|
|
|
+ 连接重量秤设备
|
|
|
+ </label>
|
|
|
+ <label>
|
|
|
+ <input type="radio" v-model="scanMode" :value="true">
|
|
|
+ 扫描所有设备
|
|
|
+ </label>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="controls">
|
|
|
+ <button v-if="!scanMode" @click="connectBluetooth" :disabled="isConnecting || device" class="btn-connect">
|
|
|
+ {{ isConnecting ? '连接中...' : '连接重量秤' }}
|
|
|
+ </button>
|
|
|
+ <button v-else @click="scanAllDevices" :disabled="isScanning" class="btn-scan">
|
|
|
+ {{ isScanning ? '扫描中...' : '扫描所有设备' }}
|
|
|
+ </button>
|
|
|
+ <button @click="disconnectBluetooth" :disabled="!device" class="btn-disconnect">
|
|
|
+ 断开连接
|
|
|
+ </button>
|
|
|
+ <button @click="clearData" class="btn-clear">清除数据</button>
|
|
|
+ <button v-if="scanMode" @click="clearDeviceList" class="btn-clear">清除设备列表</button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 扫描提示 -->
|
|
|
+ <div v-if="scanMode" class="scan-info">
|
|
|
+ <p>📱 <strong>扫描说明:</strong> 每次点击扫描按钮只能发现一个设备。如需发现更多设备,请多次点击"扫描所有设备"按钮。</p>
|
|
|
+ <p v-if="availableDevices.length === 0">💡 提示: 确保蓝牙设备已开启并在附近,然后点击扫描按钮。</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 设备列表 -->
|
|
|
+ <div v-if="scanMode && availableDevices.length > 0" class="device-list">
|
|
|
+ <h3>发现的设备 ({{ availableDevices.length }})</h3>
|
|
|
+ <div class="device-grid">
|
|
|
+ <div v-for="(device, index) in availableDevices" :key="device.id || index"
|
|
|
+ class="device-card"
|
|
|
+ :class="{ connected: device.id === (this.device && this.device.id) }"
|
|
|
+ @click="connectToDevice(device)">
|
|
|
+ <div class="device-info">
|
|
|
+ <h4>{{ device.name || '未知设备' }}</h4>
|
|
|
+ <p><strong>ID:</strong> {{ device.id }}</p>
|
|
|
+ <p v-if="device.rssi !== undefined"><strong>信号强度:</strong> {{ device.rssi }} dBm</p>
|
|
|
+ <p v-if="device.txPower !== undefined"><strong>发射功率:</strong> {{ device.txPower }} dBm</p>
|
|
|
+ <div v-if="device.adData && device.adData.serviceUUIDs && device.adData.serviceUUIDs.length > 0" class="services">
|
|
|
+ <strong>服务UUID:</strong>
|
|
|
+ <ul>
|
|
|
+ <li v-for="uuid in device.adData.serviceUUIDs" :key="uuid">{{ uuid }}</li>
|
|
|
+ </ul>
|
|
|
+ </div>
|
|
|
+ <p v-if="device.timestamp" class="timestamp">发现时间: {{ device.timestamp }}</p>
|
|
|
+ </div>
|
|
|
+ <button class="btn-connect-device" :disabled="isConnecting">
|
|
|
+ {{ device.id === (this.device && this.device.id) ? '已连接' : '连接' }}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="data-history" v-if="dataHistory.length > 0">
|
|
|
+ <h3>数据历史记录</h3>
|
|
|
+ <ul>
|
|
|
+ <li v-for="(data, index) in dataHistory.slice(-10)" :key="index">
|
|
|
+ {{ data.timestamp }}: {{ data.value }}
|
|
|
+ </li>
|
|
|
+ </ul>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
-import { ref, onMounted, onBeforeUnmount } from 'vue';
|
|
|
-import { showToast } from 'vant'
|
|
|
-
|
|
|
export default {
|
|
|
- setup() {
|
|
|
- const barcode = ref('');
|
|
|
- const inputValue = ref('');
|
|
|
- const inputValueRef=ref(null);
|
|
|
- let typingTimer = null; // 用来控制防抖
|
|
|
- const debounceDelay = 300; // 防抖延迟,300ms足够让条形码扫描器输入完成
|
|
|
-
|
|
|
- // 处理全局的键盘事件
|
|
|
- const handleGlobalKeydown = (event) => {
|
|
|
- // 判断焦点是否在输入框中
|
|
|
- const focusedElement = document.activeElement;
|
|
|
- if (focusedElement.tagName.toLowerCase() === 'input' || focusedElement.tagName.toLowerCase() === 'textarea') {
|
|
|
- return; // 如果焦点在输入框内,不处理条形码输入
|
|
|
+ name: 'BluetoothScale',
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ device: null,
|
|
|
+ deviceName: '',
|
|
|
+ receivedData: '',
|
|
|
+ weightValue: null,
|
|
|
+ connectionStatus: '未连接',
|
|
|
+ isConnecting: false,
|
|
|
+ dataHistory: [],
|
|
|
+ isBluetoothSupported: false,
|
|
|
+ isHttpsEnvironment: false,
|
|
|
+ availableDevices: [],
|
|
|
+ isScanning: false,
|
|
|
+ scanMode: false, // true: 扫描所有设备, false: 连接指定设备
|
|
|
+ // 重量秤设备UUID (HM-10模块)
|
|
|
+ SERVICE_UUID: '0000ffe0-0000-1000-8000-00805f9b34fb',
|
|
|
+ CHARACTERISTIC_UUID: '0000ffe1-0000-1000-8000-00805f9b34fb',
|
|
|
+ CLIENT_CHARACTERISTIC_CONFIG: '00002902-0000-1000-8000-00805f9b34fb'
|
|
|
+ };
|
|
|
+ },
|
|
|
+
|
|
|
+ mounted() {
|
|
|
+ this.checkBrowserSupport();
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ async connectBluetooth() {
|
|
|
+ // 检查浏览器支持
|
|
|
+ if (!navigator.bluetooth) {
|
|
|
+ alert('您的浏览器不支持Web Bluetooth API,请使用Chrome、Edge或Opera浏览器');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否在HTTPS环境下(生产环境必需)
|
|
|
+ if (location.protocol !== 'https:' && location.hostname !== 'localhost' && location.hostname !== '127.0.0.1') {
|
|
|
+ alert('Web Bluetooth API 必须在HTTPS环境下使用,请部署到HTTPS服务器或使用localhost');
|
|
|
+ return;
|
|
|
}
|
|
|
- // 处理条形码扫描输入
|
|
|
- if (event.key.length === 1) { // 只处理字符键
|
|
|
- barcode.value += event.key;
|
|
|
|
|
|
- // 如果有一个定时器,清除它,重新计时
|
|
|
- if (typingTimer) {
|
|
|
- clearTimeout(typingTimer);
|
|
|
+ this.isConnecting = true;
|
|
|
+ this.connectionStatus = '正在连接...';
|
|
|
+
|
|
|
+ try {
|
|
|
+ console.log('开始请求蓝牙设备...');
|
|
|
+
|
|
|
+ // 创建超时Promise
|
|
|
+ const timeoutPromise = new Promise((_, reject) => {
|
|
|
+ setTimeout(() => reject(new Error('设备选择超时,请重试')), 30000); // 30秒超时
|
|
|
+ });
|
|
|
+
|
|
|
+ // 请求设备选择(带超时)
|
|
|
+ const devicePromise = navigator.bluetooth.requestDevice({
|
|
|
+ filters: [{
|
|
|
+ services: [this.SERVICE_UUID]
|
|
|
+ }],
|
|
|
+ optionalServices: [this.SERVICE_UUID]
|
|
|
+ });
|
|
|
+
|
|
|
+ const device = await Promise.race([devicePromise, timeoutPromise]);
|
|
|
+ console.log('设备已选择:', device.name);
|
|
|
+
|
|
|
+ // 检查设备是否已断开
|
|
|
+ if (!device) {
|
|
|
+ throw new Error('未选择设备');
|
|
|
+ }
|
|
|
+
|
|
|
+ this.connectionStatus = '正在连接到设备...';
|
|
|
+
|
|
|
+ // 创建连接超时
|
|
|
+ const connectTimeoutPromise = new Promise((_, reject) => {
|
|
|
+ setTimeout(() => reject(new Error('连接超时')), 10000); // 10秒连接超时
|
|
|
+ });
|
|
|
+
|
|
|
+ // 连接到GATT服务器(带超时)
|
|
|
+ const server = await Promise.race([
|
|
|
+ device.gatt.connect(),
|
|
|
+ connectTimeoutPromise
|
|
|
+ ]);
|
|
|
+
|
|
|
+ console.log('GATT服务器连接成功');
|
|
|
+ this.device = device;
|
|
|
+ this.deviceName = device.name || '未知设备';
|
|
|
+ this.connectionStatus = '正在配置服务...';
|
|
|
+
|
|
|
+ // 监听断开连接事件
|
|
|
+ device.addEventListener('gattserverdisconnected', this.handleDisconnection);
|
|
|
+
|
|
|
+ // 获取重量秤服务
|
|
|
+ console.log('获取服务...');
|
|
|
+ const service = await server.getPrimaryService(this.SERVICE_UUID);
|
|
|
+
|
|
|
+ // 获取特征
|
|
|
+ console.log('获取特征...');
|
|
|
+ const characteristic = await service.getCharacteristic(this.CHARACTERISTIC_UUID);
|
|
|
+
|
|
|
+ this.connectionStatus = '正在启动通知...';
|
|
|
+
|
|
|
+ // 设置通知
|
|
|
+ await characteristic.startNotifications();
|
|
|
+ characteristic.addEventListener('characteristicvaluechanged', this.handleCharacteristicValueChanged);
|
|
|
+
|
|
|
+ // 读取初始数据
|
|
|
+ console.log('读取初始数据...');
|
|
|
+ await this.readCharacteristic(characteristic);
|
|
|
+
|
|
|
+ this.connectionStatus = '已连接';
|
|
|
+ console.log(`成功连接到设备: ${this.deviceName}`);
|
|
|
+ this.isConnecting = false;
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('连接蓝牙设备失败:', error);
|
|
|
+ this.connectionStatus = '连接失败';
|
|
|
+ this.isConnecting = false;
|
|
|
+ this.resetConnection();
|
|
|
+
|
|
|
+ // 处理不同类型的错误
|
|
|
+ if (error.name === 'NotFoundError' || error.message.includes('未选择设备')) {
|
|
|
+ alert('未找到蓝牙设备,请确保设备已开启并在附近,然后重新点击连接');
|
|
|
+ } else if (error.name === 'NotAllowedError') {
|
|
|
+ alert('蓝牙权限被拒绝,请允许网站访问蓝牙设备');
|
|
|
+ } else if (error.name === 'NetworkError') {
|
|
|
+ alert('蓝牙连接失败,请检查设备是否支持BLE模式');
|
|
|
+ } else if (error.message.includes('超时')) {
|
|
|
+ alert(error.message + ',请重试');
|
|
|
+ } else {
|
|
|
+ alert(`连接失败: ${error.message}`);
|
|
|
}
|
|
|
+ }
|
|
|
+ },
|
|
|
|
|
|
- // 设置一个新的定时器,在一定时间后处理条形码
|
|
|
- typingTimer = setTimeout(() => {
|
|
|
- // 处理条形码完成逻辑,比如发送请求或其他操作
|
|
|
- console.log('扫描完成:', barcode.value);
|
|
|
- // showToast(inputValue.value)
|
|
|
- // barcode.value = ''; // 清空条形码值(根据实际需要决定是否清空)
|
|
|
- }, debounceDelay);
|
|
|
+ async disconnectBluetooth() {
|
|
|
+ if (this.device && this.device.gatt.connected) {
|
|
|
+ await this.device.gatt.disconnect();
|
|
|
+ console.log(`已断开设备: ${this.deviceName}`);
|
|
|
}
|
|
|
- };
|
|
|
|
|
|
- // 处理输入框的键盘事件
|
|
|
- const handleInputKeydown = (event) => {
|
|
|
- if (event.key === 'Enter') {
|
|
|
- event.preventDefault();
|
|
|
- setTimeout(() => {
|
|
|
- inputValueRef.value?.blur()
|
|
|
- }, 300)
|
|
|
+ this.resetConnection();
|
|
|
+ },
|
|
|
+
|
|
|
+ handleDisconnection() {
|
|
|
+ console.log('设备断开连接');
|
|
|
+ this.resetConnection();
|
|
|
+ this.connectionStatus = '连接已断开';
|
|
|
+ // 可以在这里添加自动重连逻辑
|
|
|
+ // this.autoReconnect();
|
|
|
+ },
|
|
|
+
|
|
|
+ resetConnection() {
|
|
|
+ // 清理事件监听器
|
|
|
+ if (this.device) {
|
|
|
+ try {
|
|
|
+ this.device.removeEventListener('gattserverdisconnected', this.handleDisconnection);
|
|
|
+ } catch (e) {
|
|
|
+ console.log('清理事件监听器失败:', e);
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
+ this.device = null;
|
|
|
+ this.deviceName = '';
|
|
|
+ this.receivedData = '';
|
|
|
+ this.weightValue = null;
|
|
|
+ this.connectionStatus = '未连接';
|
|
|
+ this.isConnecting = false;
|
|
|
+ },
|
|
|
|
|
|
+ // 检查连接状态
|
|
|
+ isConnected() {
|
|
|
+ return this.device && this.device.gatt && this.device.gatt.connected;
|
|
|
+ },
|
|
|
|
|
|
- // 在输入框内,避免条形码输入逻辑
|
|
|
- };
|
|
|
+ // 自动重连(可选功能)
|
|
|
+ async autoReconnect() {
|
|
|
+ if (this.isConnected()) return;
|
|
|
+
|
|
|
+ console.log('尝试自动重连...');
|
|
|
+ this.connectionStatus = '正在重连...';
|
|
|
+
|
|
|
+ try {
|
|
|
+ if (this.device && this.device.gatt) {
|
|
|
+ await this.device.gatt.connect();
|
|
|
+ this.connectionStatus = '已重新连接';
|
|
|
|
|
|
- // 初始化时绑定全局键盘事件
|
|
|
- onMounted(() => {
|
|
|
- window.addEventListener('keydown', handleGlobalKeydown);
|
|
|
- });
|
|
|
+ // 重新设置服务和特征
|
|
|
+ const server = this.device.gatt;
|
|
|
+ const service = await server.getPrimaryService(this.SERVICE_UUID);
|
|
|
+ const characteristic = await service.getCharacteristic(this.CHARACTERISTIC_UUID);
|
|
|
|
|
|
- // 组件销毁时移除全局键盘事件监听
|
|
|
- onBeforeUnmount(() => {
|
|
|
- window.removeEventListener('keydown', handleGlobalKeydown);
|
|
|
- if (typingTimer) {
|
|
|
- clearTimeout(typingTimer); // 清除防抖定时器
|
|
|
+ await characteristic.startNotifications();
|
|
|
+ characteristic.addEventListener('characteristicvaluechanged', this.handleCharacteristicValueChanged);
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('自动重连失败:', error);
|
|
|
+ this.connectionStatus = '重连失败';
|
|
|
+ this.resetConnection();
|
|
|
}
|
|
|
- });
|
|
|
+ },
|
|
|
|
|
|
- return {
|
|
|
- barcode,
|
|
|
- inputValue, // 输入框的绑定值
|
|
|
- handleInputKeydown, // 输入框的键盘事件处理
|
|
|
- };
|
|
|
+ async readCharacteristic(characteristic) {
|
|
|
+ try {
|
|
|
+ const value = await characteristic.readValue();
|
|
|
+ this.processReceivedData(value);
|
|
|
+ } catch (error) {
|
|
|
+ console.error('读取特征值失败:', error);
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ handleCharacteristicValueChanged(event) {
|
|
|
+ const value = event.target.value;
|
|
|
+ this.processReceivedData(value);
|
|
|
+ },
|
|
|
+
|
|
|
+ processReceivedData(value) {
|
|
|
+ try {
|
|
|
+ // 将DataView转换为字节数组
|
|
|
+ const bytes = new Uint8Array(value.buffer);
|
|
|
+
|
|
|
+ // 转换为字符串 (重量秤通常发送ASCII数据)
|
|
|
+ let dataString = '';
|
|
|
+ for (let i = 0; i < bytes.length; i++) {
|
|
|
+ if (bytes[i] !== 0) { // 过滤掉空字节
|
|
|
+ dataString += String.fromCharCode(bytes[i]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ this.receivedData = dataString.trim();
|
|
|
+
|
|
|
+ // 尝试解析重量值 (假设数据格式为数字)
|
|
|
+ const weightMatch = this.receivedData.match(/(\d+(\.\d+)?)/);
|
|
|
+ if (weightMatch) {
|
|
|
+ this.weightValue = parseFloat(weightMatch[1]);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加到历史记录
|
|
|
+ this.dataHistory.push({
|
|
|
+ timestamp: new Date().toLocaleTimeString(),
|
|
|
+ value: this.receivedData,
|
|
|
+ weight: this.weightValue
|
|
|
+ });
|
|
|
+
|
|
|
+ console.log(`接收到数据: ${this.receivedData}`, bytes);
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('处理接收数据失败:', error);
|
|
|
+ this.receivedData = `错误: ${error.message}`;
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ clearData() {
|
|
|
+ this.receivedData = '';
|
|
|
+ this.weightValue = null;
|
|
|
+ this.dataHistory = [];
|
|
|
+ },
|
|
|
+
|
|
|
+ clearDeviceList() {
|
|
|
+ this.availableDevices = [];
|
|
|
+ console.log('设备列表已清除');
|
|
|
+ },
|
|
|
+
|
|
|
+ // 扫描单个蓝牙设备(避免浏览器卡死)
|
|
|
+ async scanAllDevices() {
|
|
|
+ if (this.isScanning) return;
|
|
|
+
|
|
|
+ this.isScanning = true;
|
|
|
+ this.connectionStatus = '正在扫描设备...';
|
|
|
+
|
|
|
+ try {
|
|
|
+ console.log('开始扫描蓝牙设备...');
|
|
|
+
|
|
|
+ // 创建超时Promise,避免无限等待
|
|
|
+ const timeoutPromise = new Promise((_, reject) => {
|
|
|
+ setTimeout(() => reject(new Error('设备选择超时,请重试')), 30000);
|
|
|
+ });
|
|
|
+
|
|
|
+ // 请求设备选择(带超时保护)
|
|
|
+ const devicePromise = navigator.bluetooth.requestDevice({
|
|
|
+ acceptAllDevices: true,
|
|
|
+ optionalServices: [this.SERVICE_UUID]
|
|
|
+ });
|
|
|
+
|
|
|
+ const device = await Promise.race([devicePromise, timeoutPromise]);
|
|
|
+
|
|
|
+ // 处理发现的设备
|
|
|
+ this.addDeviceToList(device);
|
|
|
+ this.connectionStatus = `已发现设备: ${device.name || '未知设备'}`;
|
|
|
+
|
|
|
+ console.log('设备扫描成功:', device.name);
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('扫描设备失败:', error);
|
|
|
+
|
|
|
+ if (error.name === 'NotFoundError') {
|
|
|
+ // 用户取消了设备选择,这是正常行为
|
|
|
+ this.connectionStatus = '扫描已取消';
|
|
|
+ console.log('用户取消了设备选择');
|
|
|
+ } else if (error.message.includes('超时')) {
|
|
|
+ this.connectionStatus = '扫描超时';
|
|
|
+ alert('设备选择超时,请重试');
|
|
|
+ } else {
|
|
|
+ this.connectionStatus = '扫描失败';
|
|
|
+ alert(`扫描失败: ${error.message}`);
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ this.isScanning = false;
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 添加设备到列表
|
|
|
+ addDeviceToList(device) {
|
|
|
+ const deviceInfo = {
|
|
|
+ id: device.id,
|
|
|
+ name: device.name || '未知设备',
|
|
|
+ rssi: device.rssi,
|
|
|
+ txPower: device.txPower,
|
|
|
+ adData: device.adData,
|
|
|
+ device: device, // 保存原始设备对象
|
|
|
+ timestamp: new Date().toLocaleString(),
|
|
|
+ discoveredAt: Date.now()
|
|
|
+ };
|
|
|
+
|
|
|
+ // 检查是否已存在
|
|
|
+ const existingIndex = this.availableDevices.findIndex(d => d.id === device.id);
|
|
|
+ if (existingIndex >= 0) {
|
|
|
+ // 更新现有设备信息
|
|
|
+ this.$set(this.availableDevices, existingIndex, { ...this.availableDevices[existingIndex], ...deviceInfo });
|
|
|
+ console.log('更新设备信息:', deviceInfo.name);
|
|
|
+ } else {
|
|
|
+ // 添加新设备
|
|
|
+ this.availableDevices.push(deviceInfo);
|
|
|
+ console.log('发现新设备:', deviceInfo.name);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 按发现时间排序(最新的在前面)
|
|
|
+ this.availableDevices.sort((a, b) => b.discoveredAt - a.discoveredAt);
|
|
|
+ },
|
|
|
+
|
|
|
+ // 连接到特定设备
|
|
|
+ async connectToDevice(deviceInfo) {
|
|
|
+ if (this.isConnecting) return;
|
|
|
+
|
|
|
+ this.isConnecting = true;
|
|
|
+ this.connectionStatus = '正在连接到设备...';
|
|
|
+
|
|
|
+ try {
|
|
|
+ const device = deviceInfo.device;
|
|
|
+
|
|
|
+ console.log('正在连接到设备:', device.name);
|
|
|
+
|
|
|
+ // 如果设备已经连接,先断开
|
|
|
+ if (device.gatt && device.gatt.connected) {
|
|
|
+ await device.gatt.disconnect();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 连接到GATT服务器
|
|
|
+ const server = await device.gatt.connect();
|
|
|
+ this.device = device;
|
|
|
+ this.deviceName = device.name || '未知设备';
|
|
|
+
|
|
|
+ // 监听断开连接事件
|
|
|
+ device.addEventListener('gattserverdisconnected', this.handleDisconnection);
|
|
|
+
|
|
|
+ this.connectionStatus = '正在查找服务...';
|
|
|
+
|
|
|
+ // 尝试获取重量秤服务,如果失败则查找所有可用服务
|
|
|
+ try {
|
|
|
+ const service = await server.getPrimaryService(this.SERVICE_UUID);
|
|
|
+ const characteristic = await service.getCharacteristic(this.CHARACTERISTIC_UUID);
|
|
|
+
|
|
|
+ // 设置通知
|
|
|
+ await characteristic.startNotifications();
|
|
|
+ characteristic.addEventListener('characteristicvaluechanged', this.handleCharacteristicValueChanged);
|
|
|
+
|
|
|
+ // 读取初始数据
|
|
|
+ await this.readCharacteristic(characteristic);
|
|
|
+
|
|
|
+ this.connectionStatus = '已连接 (重量秤模式)';
|
|
|
+ console.log('成功连接到重量秤设备');
|
|
|
+
|
|
|
+ } catch (serviceError) {
|
|
|
+ console.warn('未找到重量秤服务,尝试查找其他服务:', serviceError);
|
|
|
+
|
|
|
+ // 获取所有可用服务
|
|
|
+ const services = await server.getPrimaryServices();
|
|
|
+ console.log('发现的服务:', services.map(s => s.uuid));
|
|
|
+
|
|
|
+ // 查找包含我们特征的服务
|
|
|
+ let foundCharacteristic = null;
|
|
|
+ for (const service of services) {
|
|
|
+ try {
|
|
|
+ const characteristic = await service.getCharacteristic(this.CHARACTERISTIC_UUID);
|
|
|
+ foundCharacteristic = characteristic;
|
|
|
+ break;
|
|
|
+ } catch (e) {
|
|
|
+ // 继续查找下一个服务
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (foundCharacteristic) {
|
|
|
+ await foundCharacteristic.startNotifications();
|
|
|
+ foundCharacteristic.addEventListener('characteristicvaluechanged', this.handleCharacteristicValueChanged);
|
|
|
+ await this.readCharacteristic(foundCharacteristic);
|
|
|
+ this.connectionStatus = '已连接 (通用模式)';
|
|
|
+ } else {
|
|
|
+ this.connectionStatus = '已连接 (无可用特征)';
|
|
|
+ console.log('设备已连接但未找到可读特征');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ this.isConnecting = false;
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('连接设备失败:', error);
|
|
|
+ this.connectionStatus = '连接失败';
|
|
|
+ this.isConnecting = false;
|
|
|
+ alert(`连接失败: ${error.message}`);
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ getStatusClass() {
|
|
|
+ switch (this.connectionStatus) {
|
|
|
+ case '未连接': return 'status-disconnected';
|
|
|
+ case '正在连接...':
|
|
|
+ case '正在连接到设备...':
|
|
|
+ case '正在配置服务...':
|
|
|
+ case '正在启动通知...':
|
|
|
+ case '正在重连...': return 'status-connecting';
|
|
|
+ case '已连接':
|
|
|
+ case '已重新连接': return 'status-connected';
|
|
|
+ case '连接失败':
|
|
|
+ case '连接已断开':
|
|
|
+ case '重连失败': return 'status-error';
|
|
|
+ default: return 'status-disconnected';
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ checkBrowserSupport() {
|
|
|
+ // 检查Web Bluetooth API支持
|
|
|
+ this.isBluetoothSupported = !!navigator.bluetooth;
|
|
|
+
|
|
|
+ // 检查是否在HTTPS环境下
|
|
|
+ this.isHttpsEnvironment = location.protocol === 'https:' ||
|
|
|
+ location.hostname === 'localhost' ||
|
|
|
+ location.hostname === '127.0.0.1';
|
|
|
+
|
|
|
+ if (this.isBluetoothSupported) {
|
|
|
+ console.log('✅ 浏览器支持Web Bluetooth API');
|
|
|
+ } else {
|
|
|
+ console.warn('❌ 浏览器不支持Web Bluetooth API');
|
|
|
+ }
|
|
|
+
|
|
|
+ if (this.isHttpsEnvironment) {
|
|
|
+ console.log('✅ 在HTTPS环境下运行');
|
|
|
+ } else {
|
|
|
+ console.warn('❌ 不在HTTPS环境下运行,Web Bluetooth API可能无法正常工作');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ beforeUnmount() {
|
|
|
+ // 组件销毁前断开连接并清理
|
|
|
+ this.cleanup();
|
|
|
+ },
|
|
|
+
|
|
|
+ unmounted() {
|
|
|
+ // 确保清理完成
|
|
|
+ this.cleanup();
|
|
|
},
|
|
|
+
|
|
|
+ cleanup() {
|
|
|
+ try {
|
|
|
+ if (this.device && this.device.gatt && this.device.gatt.connected) {
|
|
|
+ this.device.gatt.disconnect();
|
|
|
+ console.log('组件销毁时断开蓝牙连接');
|
|
|
+ }
|
|
|
+ this.resetConnection();
|
|
|
+ } catch (error) {
|
|
|
+ console.error('清理蓝牙连接失败:', error);
|
|
|
+ }
|
|
|
+ }
|
|
|
};
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
|
-/* 样式部分 */
|
|
|
+.warning {
|
|
|
+ margin: 20px 0;
|
|
|
+ padding: 15px;
|
|
|
+ background-color: #fff3cd;
|
|
|
+ border: 1px solid #ffeaa7;
|
|
|
+ border-radius: 8px;
|
|
|
+ color: #856404;
|
|
|
+}
|
|
|
+
|
|
|
+.warning p {
|
|
|
+ margin: 5px 0;
|
|
|
+ font-weight: bold;
|
|
|
+}
|
|
|
+
|
|
|
+.status {
|
|
|
+ margin: 20px 0;
|
|
|
+ padding: 15px;
|
|
|
+ background-color: #f5f5f5;
|
|
|
+ border-radius: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.status p {
|
|
|
+ margin: 5px 0;
|
|
|
+ font-size: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.connection-status {
|
|
|
+ font-weight: bold;
|
|
|
+}
|
|
|
+
|
|
|
+.connection-status.status-disconnected {
|
|
|
+ color: #666;
|
|
|
+}
|
|
|
+
|
|
|
+.connection-status.status-connecting {
|
|
|
+ color: #ffa500;
|
|
|
+}
|
|
|
+
|
|
|
+.connection-status.status-connected {
|
|
|
+ color: #28a745;
|
|
|
+}
|
|
|
+
|
|
|
+.connection-status.status-error {
|
|
|
+ color: #dc3545;
|
|
|
+}
|
|
|
+
|
|
|
+.controls {
|
|
|
+ margin: 20px 0;
|
|
|
+}
|
|
|
+
|
|
|
+button {
|
|
|
+ padding: 10px 20px;
|
|
|
+ margin: 0 10px 10px 0;
|
|
|
+ border: none;
|
|
|
+ border-radius: 5px;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 14px;
|
|
|
+ transition: background-color 0.3s;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-connect {
|
|
|
+ background-color: #007bff;
|
|
|
+ color: white;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-connect:hover:not(:disabled) {
|
|
|
+ background-color: #0056b3;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-connect:disabled {
|
|
|
+ background-color: #6c757d;
|
|
|
+ cursor: not-allowed;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-disconnect {
|
|
|
+ background-color: #dc3545;
|
|
|
+ color: white;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-disconnect:hover:not(:disabled) {
|
|
|
+ background-color: #c82333;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-disconnect:disabled {
|
|
|
+ background-color: #6c757d;
|
|
|
+ cursor: not-allowed;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-clear {
|
|
|
+ background-color: #6c757d;
|
|
|
+ color: white;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-clear:hover {
|
|
|
+ background-color: #545b62;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-scan {
|
|
|
+ background-color: #17a2b8;
|
|
|
+ color: white;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-scan:hover:not(:disabled) {
|
|
|
+ background-color: #138496;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-scan:disabled {
|
|
|
+ background-color: #6c757d;
|
|
|
+ cursor: not-allowed;
|
|
|
+}
|
|
|
+
|
|
|
+.data-history {
|
|
|
+ margin-top: 30px;
|
|
|
+ padding: 15px;
|
|
|
+ background-color: #f8f9fa;
|
|
|
+ border-radius: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.data-history h3 {
|
|
|
+ margin-top: 0;
|
|
|
+ color: #333;
|
|
|
+}
|
|
|
+
|
|
|
+.data-history ul {
|
|
|
+ list-style-type: none;
|
|
|
+ padding: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.data-history li {
|
|
|
+ padding: 8px 0;
|
|
|
+ border-bottom: 1px solid #dee2e6;
|
|
|
+ font-family: monospace;
|
|
|
+}
|
|
|
+
|
|
|
+.data-history li:last-child {
|
|
|
+ border-bottom: none;
|
|
|
+}
|
|
|
+
|
|
|
+/* 模式选择 */
|
|
|
+.mode-selection {
|
|
|
+ margin: 20px 0;
|
|
|
+ padding: 15px;
|
|
|
+ background-color: #e9ecef;
|
|
|
+ border-radius: 8px;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+/* 扫描信息提示 */
|
|
|
+.scan-info {
|
|
|
+ margin: 15px 0;
|
|
|
+ padding: 12px;
|
|
|
+ background-color: #d1ecf1;
|
|
|
+ border: 1px solid #bee5eb;
|
|
|
+ border-radius: 8px;
|
|
|
+ color: #0c5460;
|
|
|
+}
|
|
|
+
|
|
|
+.scan-info p {
|
|
|
+ margin: 5px 0;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.mode-selection label {
|
|
|
+ margin: 0 15px;
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: bold;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.mode-selection input[type="radio"] {
|
|
|
+ margin-right: 8px;
|
|
|
+ transform: scale(1.2);
|
|
|
+}
|
|
|
+
|
|
|
+/* 设备列表 */
|
|
|
+.device-list {
|
|
|
+ margin-top: 20px;
|
|
|
+ padding: 15px;
|
|
|
+ background-color: #f8f9fa;
|
|
|
+ border-radius: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.device-list h3 {
|
|
|
+ margin-top: 0;
|
|
|
+ color: #333;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+.device-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
|
+ gap: 15px;
|
|
|
+ margin-top: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+.device-card {
|
|
|
+ background-color: white;
|
|
|
+ border: 2px solid #dee2e6;
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 15px;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ position: relative;
|
|
|
+}
|
|
|
+
|
|
|
+.device-card:hover {
|
|
|
+ border-color: #007bff;
|
|
|
+ box-shadow: 0 4px 8px rgba(0,123,255,0.1);
|
|
|
+}
|
|
|
+
|
|
|
+.device-card.connected {
|
|
|
+ border-color: #28a745;
|
|
|
+ background-color: #f8fff8;
|
|
|
+}
|
|
|
+
|
|
|
+.device-info h4 {
|
|
|
+ margin: 0 0 10px 0;
|
|
|
+ color: #333;
|
|
|
+ font-size: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.device-info p {
|
|
|
+ margin: 5px 0;
|
|
|
+ font-size: 14px;
|
|
|
+ color: #666;
|
|
|
+}
|
|
|
+
|
|
|
+.services {
|
|
|
+ margin-top: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.services ul {
|
|
|
+ margin: 5px 0 0 0;
|
|
|
+ padding-left: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.services li {
|
|
|
+ font-family: monospace;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #666;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-connect-device {
|
|
|
+ position: absolute;
|
|
|
+ bottom: 10px;
|
|
|
+ right: 10px;
|
|
|
+ padding: 6px 12px;
|
|
|
+ border: none;
|
|
|
+ border-radius: 4px;
|
|
|
+ background-color: #007bff;
|
|
|
+ color: white;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 12px;
|
|
|
+ transition: background-color 0.3s;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-connect-device:hover:not(:disabled) {
|
|
|
+ background-color: #0056b3;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-connect-device:disabled {
|
|
|
+ background-color: #6c757d;
|
|
|
+ cursor: not-allowed;
|
|
|
+}
|
|
|
+
|
|
|
+.device-card.connected .btn-connect-device {
|
|
|
+ background-color: #28a745;
|
|
|
+}
|
|
|
+
|
|
|
+.timestamp {
|
|
|
+ font-size: 12px !important;
|
|
|
+ color: #999 !important;
|
|
|
+ margin-top: 8px !important;
|
|
|
+ font-style: italic;
|
|
|
+}
|
|
|
</style>
|