|
|
@@ -38,9 +38,29 @@
|
|
|
<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>
|
|
|
+
|
|
|
+ <!-- 扫描模式按钮组 -->
|
|
|
+ <div v-else class="scan-controls">
|
|
|
+ <button @click="scanAllDevices" :disabled="isScanning" class="btn-scan">
|
|
|
+ {{ isScanning ? '扫描中...' : '扫描设备' }}
|
|
|
+ </button>
|
|
|
+
|
|
|
+ <div class="batch-scan-section">
|
|
|
+ <label class="batch-scan-toggle">
|
|
|
+ <input type="checkbox" v-model="batchScanMode">
|
|
|
+ 批量扫描模式
|
|
|
+ </label>
|
|
|
+
|
|
|
+ <button v-if="batchScanMode" @click="startBatchScan" :disabled="isScanning" class="btn-batch-scan">
|
|
|
+ 🚀 开始批量扫描
|
|
|
+ </button>
|
|
|
+
|
|
|
+ <button v-if="batchScanMode && batchScanCount > 0" @click="stopBatchScan" class="btn-stop-batch">
|
|
|
+ ⏹️ 停止批量扫描
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
<button @click="disconnectBluetooth" :disabled="!device" class="btn-disconnect">
|
|
|
断开连接
|
|
|
</button>
|
|
|
@@ -50,15 +70,47 @@
|
|
|
|
|
|
<!-- 扫描提示 -->
|
|
|
<div v-if="scanMode" class="scan-info">
|
|
|
- <p>📱 <strong>扫描说明:</strong> 每次点击扫描按钮只能发现一个设备。如需发现更多设备,请多次点击"扫描所有设备"按钮。</p>
|
|
|
- <p v-if="availableDevices.length === 0">💡 提示: 确保蓝牙设备已开启并在附近,然后点击扫描按钮。</p>
|
|
|
+ <div class="scan-header">
|
|
|
+ <h4>🔍 设备扫描说明</h4>
|
|
|
+ <p><strong>Web Bluetooth API 限制:</strong> 每次只能选择一个设备,无法自动发现所有设备。</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="scan-guide">
|
|
|
+ <div class="guide-step">
|
|
|
+ <span class="step-number">1</span>
|
|
|
+ <span>点击"扫描所有设备"按钮</span>
|
|
|
+ </div>
|
|
|
+ <div class="guide-step">
|
|
|
+ <span class="step-number">2</span>
|
|
|
+ <span>在弹出的设备列表中选择一个设备</span>
|
|
|
+ </div>
|
|
|
+ <div class="guide-step">
|
|
|
+ <span class="step-number">3</span>
|
|
|
+ <span>重复步骤1-2来发现更多设备</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="scan-tips">
|
|
|
+ <p v-if="availableDevices.length === 0">💡 <strong>提示:</strong> 如果没有看到设备,请确保蓝牙设备已开启并在附近。</p>
|
|
|
+ <p v-if="availableDevices.length > 0">✅ 已发现 <strong>{{ availableDevices.length }}</strong> 个设备,可以继续扫描更多设备。</p>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
|
|
|
<!-- 设备列表 -->
|
|
|
<div v-if="scanMode && availableDevices.length > 0" class="device-list">
|
|
|
- <h3>发现的设备 ({{ availableDevices.length }})</h3>
|
|
|
+ <div class="device-list-header">
|
|
|
+ <h3>发现的设备 ({{ availableDevices.length }})</h3>
|
|
|
+ <div class="device-search">
|
|
|
+ <input type="text" v-model="deviceSearchQuery" placeholder="搜索设备名称..." class="search-input">
|
|
|
+ <button @click="deviceSearchQuery = ''" class="btn-clear-search" v-if="deviceSearchQuery">清除</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div v-if="filteredDevices.length === 0 && deviceSearchQuery" class="no-results">
|
|
|
+ 没有找到匹配的设备
|
|
|
+ </div>
|
|
|
+
|
|
|
<div class="device-grid">
|
|
|
- <div v-for="(device, index) in availableDevices" :key="device.id || index"
|
|
|
+ <div v-for="(device, index) in filteredDevices" :key="device.id || index"
|
|
|
class="device-card"
|
|
|
:class="{ connected: device.id === (this.device && this.device.id) }"
|
|
|
@click="connectToDevice(device)">
|
|
|
@@ -110,6 +162,9 @@ export default {
|
|
|
availableDevices: [],
|
|
|
isScanning: false,
|
|
|
scanMode: false, // true: 扫描所有设备, false: 连接指定设备
|
|
|
+ batchScanMode: false, // 是否启用批量扫描模式
|
|
|
+ batchScanCount: 0, // 批量扫描计数
|
|
|
+ deviceSearchQuery: '', // 设备搜索查询
|
|
|
// 重量秤设备UUID (HM-10模块)
|
|
|
SERVICE_UUID: '0000ffe0-0000-1000-8000-00805f9b34fb',
|
|
|
CHARACTERISTIC_UUID: '0000ffe1-0000-1000-8000-00805f9b34fb',
|
|
|
@@ -120,6 +175,21 @@ export default {
|
|
|
mounted() {
|
|
|
this.checkBrowserSupport();
|
|
|
},
|
|
|
+
|
|
|
+ computed: {
|
|
|
+ // 过滤设备列表
|
|
|
+ filteredDevices() {
|
|
|
+ if (!this.deviceSearchQuery) {
|
|
|
+ return this.availableDevices;
|
|
|
+ }
|
|
|
+
|
|
|
+ const query = this.deviceSearchQuery.toLowerCase();
|
|
|
+ return this.availableDevices.filter(device =>
|
|
|
+ device.name.toLowerCase().includes(query) ||
|
|
|
+ device.id.toLowerCase().includes(query)
|
|
|
+ );
|
|
|
+ }
|
|
|
+ },
|
|
|
methods: {
|
|
|
async connectBluetooth() {
|
|
|
// 检查浏览器支持
|
|
|
@@ -353,16 +423,70 @@ export default {
|
|
|
console.log('设备列表已清除');
|
|
|
},
|
|
|
|
|
|
+ // 开始批量扫描
|
|
|
+ async startBatchScan() {
|
|
|
+ if (this.isScanning) return;
|
|
|
+
|
|
|
+ this.batchScanCount = 0;
|
|
|
+ this.logBatchScan('开始批量扫描设备...');
|
|
|
+
|
|
|
+ // 开始第一次扫描
|
|
|
+ await this.performBatchScan();
|
|
|
+ },
|
|
|
+
|
|
|
+ // 执行批量扫描
|
|
|
+ async performBatchScan() {
|
|
|
+ if (!this.batchScanMode) return;
|
|
|
+
|
|
|
+ this.batchScanCount++;
|
|
|
+ this.logBatchScan(`执行第 ${this.batchScanCount} 次扫描...`);
|
|
|
+
|
|
|
+ try {
|
|
|
+ await this.scanAllDevices();
|
|
|
+
|
|
|
+ // 如果仍在批量扫描模式且没有达到最大次数,延迟后继续扫描
|
|
|
+ if (this.batchScanMode && this.batchScanCount < 10) { // 最多扫描10次
|
|
|
+ setTimeout(() => {
|
|
|
+ this.performBatchScan();
|
|
|
+ }, 2000); // 2秒延迟,避免过于频繁
|
|
|
+ } else if (this.batchScanCount >= 10) {
|
|
|
+ this.logBatchScan('已达到最大扫描次数 (10次),自动停止批量扫描');
|
|
|
+ this.batchScanMode = false;
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ this.logBatchScan(`批量扫描出错: ${error.message}`);
|
|
|
+ this.batchScanMode = false;
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 停止批量扫描
|
|
|
+ stopBatchScan() {
|
|
|
+ this.batchScanMode = false;
|
|
|
+ this.logBatchScan('批量扫描已手动停止');
|
|
|
+ },
|
|
|
+
|
|
|
+ // 批量扫描日志
|
|
|
+ logBatchScan(message) {
|
|
|
+ console.log(`[批量扫描] ${message}`);
|
|
|
+ // 可以在这里添加UI日志显示
|
|
|
+ },
|
|
|
+
|
|
|
// 扫描单个蓝牙设备(避免浏览器卡死)
|
|
|
async scanAllDevices() {
|
|
|
if (this.isScanning) return;
|
|
|
|
|
|
this.isScanning = true;
|
|
|
- this.connectionStatus = '正在扫描设备...';
|
|
|
+ const scanMessage = this.batchScanMode ? `正在扫描设备 (${this.batchScanCount})...` : '正在扫描设备...';
|
|
|
+ this.connectionStatus = scanMessage;
|
|
|
|
|
|
try {
|
|
|
console.log('开始扫描蓝牙设备...');
|
|
|
|
|
|
+ // 在批量扫描模式下给出不同的提示
|
|
|
+ if (this.batchScanMode) {
|
|
|
+ alert(`请在设备列表中选择第 ${this.batchScanCount} 个设备。\n\n如果没有更多设备可以选择,请点击"取消"或关闭对话框来停止批量扫描。`);
|
|
|
+ }
|
|
|
+
|
|
|
// 创建超时Promise,避免无限等待
|
|
|
const timeoutPromise = new Promise((_, reject) => {
|
|
|
setTimeout(() => reject(new Error('设备选择超时,请重试')), 30000);
|
|
|
@@ -378,23 +502,38 @@ export default {
|
|
|
|
|
|
// 处理发现的设备
|
|
|
this.addDeviceToList(device);
|
|
|
- this.connectionStatus = `已发现设备: ${device.name || '未知设备'}`;
|
|
|
+ const deviceName = device.name || '未知设备';
|
|
|
+ this.connectionStatus = `已发现设备: ${deviceName}`;
|
|
|
+
|
|
|
+ if (this.batchScanMode) {
|
|
|
+ this.logBatchScan(`成功发现设备: ${deviceName}`);
|
|
|
+ }
|
|
|
|
|
|
- console.log('设备扫描成功:', device.name);
|
|
|
+ console.log('设备扫描成功:', deviceName);
|
|
|
|
|
|
} catch (error) {
|
|
|
console.error('扫描设备失败:', error);
|
|
|
|
|
|
if (error.name === 'NotFoundError') {
|
|
|
- // 用户取消了设备选择,这是正常行为
|
|
|
- this.connectionStatus = '扫描已取消';
|
|
|
+ // 用户取消了设备选择
|
|
|
+ if (this.batchScanMode) {
|
|
|
+ this.connectionStatus = '批量扫描已停止';
|
|
|
+ this.batchScanMode = false;
|
|
|
+ this.logBatchScan('用户取消扫描,批量扫描已停止');
|
|
|
+ } else {
|
|
|
+ this.connectionStatus = '扫描已取消';
|
|
|
+ }
|
|
|
console.log('用户取消了设备选择');
|
|
|
} else if (error.message.includes('超时')) {
|
|
|
this.connectionStatus = '扫描超时';
|
|
|
- alert('设备选择超时,请重试');
|
|
|
+ if (!this.batchScanMode) {
|
|
|
+ alert('设备选择超时,请重试');
|
|
|
+ }
|
|
|
} else {
|
|
|
this.connectionStatus = '扫描失败';
|
|
|
- alert(`扫描失败: ${error.message}`);
|
|
|
+ if (!this.batchScanMode) {
|
|
|
+ alert(`扫描失败: ${error.message}`);
|
|
|
+ }
|
|
|
}
|
|
|
} finally {
|
|
|
this.isScanning = false;
|
|
|
@@ -727,18 +866,117 @@ button {
|
|
|
/* 扫描信息提示 */
|
|
|
.scan-info {
|
|
|
margin: 15px 0;
|
|
|
- padding: 12px;
|
|
|
+ padding: 15px;
|
|
|
background-color: #d1ecf1;
|
|
|
border: 1px solid #bee5eb;
|
|
|
border-radius: 8px;
|
|
|
color: #0c5460;
|
|
|
}
|
|
|
|
|
|
-.scan-info p {
|
|
|
+.scan-header h4 {
|
|
|
+ margin: 0 0 10px 0;
|
|
|
+ color: #0c5460;
|
|
|
+ font-size: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.scan-guide {
|
|
|
+ margin: 15px 0;
|
|
|
+}
|
|
|
+
|
|
|
+.guide-step {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ margin: 8px 0;
|
|
|
+ padding: 8px;
|
|
|
+ background-color: rgba(255, 255, 255, 0.7);
|
|
|
+ border-radius: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+.step-number {
|
|
|
+ display: inline-block;
|
|
|
+ width: 24px;
|
|
|
+ height: 24px;
|
|
|
+ line-height: 24px;
|
|
|
+ text-align: center;
|
|
|
+ background-color: #17a2b8;
|
|
|
+ color: white;
|
|
|
+ border-radius: 50%;
|
|
|
+ margin-right: 12px;
|
|
|
+ font-weight: bold;
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.scan-tips {
|
|
|
+ margin-top: 15px;
|
|
|
+ padding-top: 15px;
|
|
|
+ border-top: 1px solid #bee5eb;
|
|
|
+}
|
|
|
+
|
|
|
+.scan-tips p {
|
|
|
margin: 5px 0;
|
|
|
font-size: 14px;
|
|
|
}
|
|
|
|
|
|
+/* 扫描控制 */
|
|
|
+.scan-controls {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 15px;
|
|
|
+ align-items: flex-start;
|
|
|
+}
|
|
|
+
|
|
|
+.batch-scan-section {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 15px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+}
|
|
|
+
|
|
|
+.batch-scan-toggle {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ font-weight: bold;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.batch-scan-toggle input[type="checkbox"] {
|
|
|
+ transform: scale(1.2);
|
|
|
+}
|
|
|
+
|
|
|
+.btn-batch-scan {
|
|
|
+ background-color: #28a745;
|
|
|
+ color: white;
|
|
|
+ padding: 8px 16px;
|
|
|
+ border: none;
|
|
|
+ border-radius: 5px;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-batch-scan:hover:not(:disabled) {
|
|
|
+ background-color: #218838;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-batch-scan:disabled {
|
|
|
+ background-color: #6c757d;
|
|
|
+ cursor: not-allowed;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-stop-batch {
|
|
|
+ background-color: #dc3545;
|
|
|
+ color: white;
|
|
|
+ padding: 8px 16px;
|
|
|
+ border: none;
|
|
|
+ border-radius: 5px;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-stop-batch:hover {
|
|
|
+ background-color: #c82333;
|
|
|
+}
|
|
|
+
|
|
|
.mode-selection label {
|
|
|
margin: 0 15px;
|
|
|
font-size: 16px;
|
|
|
@@ -759,10 +997,53 @@ button {
|
|
|
border-radius: 8px;
|
|
|
}
|
|
|
|
|
|
-.device-list h3 {
|
|
|
- margin-top: 0;
|
|
|
+.device-list-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 15px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+.device-list-header h3 {
|
|
|
+ margin: 0;
|
|
|
color: #333;
|
|
|
+}
|
|
|
+
|
|
|
+.device-search {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.search-input {
|
|
|
+ padding: 8px 12px;
|
|
|
+ border: 1px solid #ccc;
|
|
|
+ border-radius: 4px;
|
|
|
+ font-size: 14px;
|
|
|
+ min-width: 200px;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-clear-search {
|
|
|
+ padding: 8px 12px;
|
|
|
+ background-color: #6c757d;
|
|
|
+ color: white;
|
|
|
+ border: none;
|
|
|
+ border-radius: 4px;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-clear-search:hover {
|
|
|
+ background-color: #545b62;
|
|
|
+}
|
|
|
+
|
|
|
+.no-results {
|
|
|
text-align: center;
|
|
|
+ padding: 20px;
|
|
|
+ color: #666;
|
|
|
+ font-style: italic;
|
|
|
}
|
|
|
|
|
|
.device-grid {
|