|
|
@@ -1,1184 +1,28 @@
|
|
|
<template>
|
|
|
<div>
|
|
|
- <h1>蓝牙重量秤连接</h1>
|
|
|
-
|
|
|
- <!-- 平台信息显示 -->
|
|
|
- <div class="platform-info">
|
|
|
- <p><strong>📱 检测到平台:</strong> {{ platformInfo }}</p>
|
|
|
- <p><strong>🔧 蓝牙支持:</strong>
|
|
|
- <span :class="isBluetoothSupported ? 'success' : 'error'">
|
|
|
- {{ isBluetoothSupported ? '✅ 支持' : '❌ 不支持' }}
|
|
|
- </span>
|
|
|
- </p>
|
|
|
- <p><strong>🌐 HTTPS环境:</strong>
|
|
|
- <span :class="isHttpsEnvironment ? 'success' : 'error'">
|
|
|
- {{ isHttpsEnvironment ? '✅ 是' : '❌ 否' }}
|
|
|
- </span>
|
|
|
- </p>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 浏览器支持检查 -->
|
|
|
- <div v-if="!isBluetoothSupported" class="warning">
|
|
|
- <p>⚠️ 您的浏览器不支持Web Bluetooth API</p>
|
|
|
- <p>请使用Chrome、Edge或Opera浏览器,并确保在HTTPS环境下运行</p>
|
|
|
- <p v-if="isMobileDevice">📱 在移动端,请确保使用系统浏览器而非微信等内置浏览器</p>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 环境检查 -->
|
|
|
- <div v-if="!isHttpsEnvironment && isBluetoothSupported" class="warning">
|
|
|
- <p>⚠️ Web Bluetooth API 必须在HTTPS环境下使用</p>
|
|
|
- <p>请部署到HTTPS服务器或使用localhost进行开发</p>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 移动端特殊提示 -->
|
|
|
- <div v-if="isMobileDevice && isBluetoothSupported && isHttpsEnvironment" class="info">
|
|
|
- <p>📱 <strong>移动端提示:</strong></p>
|
|
|
- <ul>
|
|
|
- <li>确保设备蓝牙已开启</li>
|
|
|
- <li>蓝牙设备需要在附近(5米内)</li>
|
|
|
- <li>首次使用需要授予蓝牙权限</li>
|
|
|
- <li>如果扫描不到设备,请重启设备蓝牙</li>
|
|
|
- </ul>
|
|
|
- </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>
|
|
|
-
|
|
|
- <!-- 扫描模式按钮组 -->
|
|
|
- <div v-else class="scan-controls">
|
|
|
- <button @click="scanSingleDevice" :disabled="isScanning" class="btn-scan">
|
|
|
- {{ isScanning ? '扫描中...' : '扫描单个设备' }}
|
|
|
- </button>
|
|
|
-
|
|
|
- <div class="scan-info-compact">
|
|
|
- <p><strong>💡 提示:</strong> 每次点击只能发现一个设备,重复点击可发现更多设备</p>
|
|
|
- <p v-if="isMobileDevice" class="mobile-warning">
|
|
|
- 📱 <strong>移动端注意:</strong> 确保设备蓝牙已开启且在附近
|
|
|
- </p>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <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">
|
|
|
- <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 v-if="isMobileDevice" class="mobile-scan-tips">
|
|
|
- <p><strong>📱 移动端特殊提示:</strong></p>
|
|
|
- <ul>
|
|
|
- <li>确保App有蓝牙权限</li>
|
|
|
- <li>设备需要开启蓝牙广播模式</li>
|
|
|
- <li>尝试重启设备蓝牙功能</li>
|
|
|
- <li>检查是否在微信等内置浏览器中</li>
|
|
|
- </ul>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 设备列表 -->
|
|
|
- <div v-if="scanMode && availableDevices.length > 0" class="device-list">
|
|
|
- <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 filteredDevices" :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>
|
|
|
+ <button @click="scanBluetooth">扫描蓝牙设备</button>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
export default {
|
|
|
- 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: 连接指定设备
|
|
|
- deviceSearchQuery: '', // 设备搜索查询
|
|
|
- isMobileDevice: false, // 是否为移动设备
|
|
|
- platformInfo: '', // 平台信息
|
|
|
- // 重量秤设备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();
|
|
|
- this.detectPlatform();
|
|
|
- },
|
|
|
-
|
|
|
- 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() {
|
|
|
- // 检查浏览器支持
|
|
|
+ async scanBluetooth() {
|
|
|
if (!navigator.bluetooth) {
|
|
|
- alert('您的浏览器不支持Web Bluetooth API,请使用Chrome、Edge或Opera浏览器');
|
|
|
+ console.log('此浏览器不支持 Web Bluetooth API。:');
|
|
|
+ // alert("此浏览器不支持 Web Bluetooth API。");
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- // 检查是否在HTTPS环境下(生产环境必需)
|
|
|
- if (location.protocol !== 'https:' && location.hostname !== 'localhost' && location.hostname !== '127.0.0.1') {
|
|
|
- alert('Web Bluetooth API 必须在HTTPS环境下使用,请部署到HTTPS服务器或使用localhost');
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- 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 navigator.bluetooth.requestDevice({
|
|
|
+ filters: [{services: ['battery_service']}]
|
|
|
});
|
|
|
-
|
|
|
- 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}`);
|
|
|
- }
|
|
|
- }
|
|
|
- },
|
|
|
-
|
|
|
- async disconnectBluetooth() {
|
|
|
- if (this.device && this.device.gatt.connected) {
|
|
|
- await this.device.gatt.disconnect();
|
|
|
- console.log(`已断开设备: ${this.deviceName}`);
|
|
|
- }
|
|
|
-
|
|
|
- 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 = '已重新连接';
|
|
|
-
|
|
|
- // 重新设置服务和特征
|
|
|
- const server = this.device.gatt;
|
|
|
- const service = await server.getPrimaryService(this.SERVICE_UUID);
|
|
|
- const characteristic = await service.getCharacteristic(this.CHARACTERISTIC_UUID);
|
|
|
-
|
|
|
- await characteristic.startNotifications();
|
|
|
- characteristic.addEventListener('characteristicvaluechanged', this.handleCharacteristicValueChanged);
|
|
|
- }
|
|
|
- } catch (error) {
|
|
|
- console.error('自动重连失败:', error);
|
|
|
- this.connectionStatus = '重连失败';
|
|
|
- this.resetConnection();
|
|
|
- }
|
|
|
- },
|
|
|
-
|
|
|
- async readCharacteristic(characteristic) {
|
|
|
- try {
|
|
|
- const value = await characteristic.readValue();
|
|
|
- this.processReceivedData(value);
|
|
|
+ console.log('选择的设备:', device);
|
|
|
} catch (error) {
|
|
|
- console.error('读取特征值失败:', 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 scanSingleDevice() {
|
|
|
- if (this.isScanning) return;
|
|
|
-
|
|
|
- this.isScanning = true;
|
|
|
- this.connectionStatus = '正在扫描设备...';
|
|
|
-
|
|
|
- try {
|
|
|
- console.log('开始扫描蓝牙设备...');
|
|
|
-
|
|
|
- // 移动端特殊提示
|
|
|
- if (this.isMobileDevice) {
|
|
|
- console.log('📱 移动端扫描模式');
|
|
|
- // 在移动端给出更简洁的提示,避免弹窗干扰
|
|
|
- this.connectionStatus = '请在设备列表中选择蓝牙设备...';
|
|
|
- }
|
|
|
-
|
|
|
- // 创建超时Promise(根据平台调整超时时间)
|
|
|
- const timeoutMs = this.isMobileDevice ? 45000 : 30000; // 移动端给更长时间
|
|
|
- const timeoutPromise = new Promise((_, reject) => {
|
|
|
- setTimeout(() => reject(new Error('设备选择超时,请重试')), timeoutMs);
|
|
|
- });
|
|
|
-
|
|
|
- // 请求设备选择(带超时保护)
|
|
|
- const devicePromise = navigator.bluetooth.requestDevice({
|
|
|
- acceptAllDevices: true,
|
|
|
- optionalServices: [this.SERVICE_UUID]
|
|
|
- });
|
|
|
-
|
|
|
- console.log('等待用户选择设备...');
|
|
|
- const device = await Promise.race([devicePromise, timeoutPromise]);
|
|
|
-
|
|
|
- // 处理发现的设备
|
|
|
- this.addDeviceToList(device);
|
|
|
- const deviceName = device.name || '未知设备';
|
|
|
- this.connectionStatus = `✅ 已发现设备: ${deviceName}`;
|
|
|
-
|
|
|
- console.log('设备扫描成功:', deviceName, device.id);
|
|
|
-
|
|
|
- // 成功提示(仅在PC端显示,避免移动端弹窗干扰)
|
|
|
- if (!this.isMobileDevice) {
|
|
|
- setTimeout(() => {
|
|
|
- alert(`设备 "${deviceName}" 已添加到列表!\n\n如需发现更多设备,请再次点击"扫描单个设备"按钮。`);
|
|
|
- }, 500);
|
|
|
- }
|
|
|
-
|
|
|
- } catch (error) {
|
|
|
- console.error('扫描设备失败:', error);
|
|
|
-
|
|
|
- // 处理不同类型的错误
|
|
|
- if (error.name === 'NotFoundError') {
|
|
|
- // 用户主动取消选择
|
|
|
- this.connectionStatus = '扫描已取消';
|
|
|
- console.log('用户取消了设备选择');
|
|
|
-
|
|
|
- if (!this.isMobileDevice) {
|
|
|
- setTimeout(() => {
|
|
|
- alert('扫描已取消。如需扫描设备,请再次点击"扫描单个设备"按钮。');
|
|
|
- }, 300);
|
|
|
- }
|
|
|
-
|
|
|
- } else if (error.message.includes('超时')) {
|
|
|
- this.connectionStatus = '扫描超时,请重试';
|
|
|
- console.log('设备选择超时');
|
|
|
-
|
|
|
- if (!this.isMobileDevice) {
|
|
|
- alert('设备选择超时,请重试。\n\n可能的原因:\n• 设备选择对话框响应慢\n• 用户长时间未选择设备\n• 浏览器权限问题');
|
|
|
- }
|
|
|
-
|
|
|
- } else if (error.name === 'NotAllowedError') {
|
|
|
- this.connectionStatus = '权限被拒绝';
|
|
|
- console.log('蓝牙权限被拒绝');
|
|
|
-
|
|
|
- alert('蓝牙权限被拒绝!\n\n请检查:\n• 浏览器是否支持Web Bluetooth\n• 是否在HTTPS环境下\n• 是否授予了蓝牙访问权限');
|
|
|
-
|
|
|
- } else if (error.name === 'NotSupportedError') {
|
|
|
- this.connectionStatus = '不支持此功能';
|
|
|
- console.log('Web Bluetooth不支持');
|
|
|
-
|
|
|
- alert('您的浏览器不支持Web Bluetooth API\n\n请使用:\n• Chrome (56+)\n• Edge (79+)\n• Opera (43+)');
|
|
|
-
|
|
|
- } else {
|
|
|
- this.connectionStatus = '扫描失败';
|
|
|
- console.error('未知扫描错误:', error);
|
|
|
-
|
|
|
- if (!this.isMobileDevice) {
|
|
|
- alert(`扫描失败: ${error.message}`);
|
|
|
- }
|
|
|
- }
|
|
|
- } finally {
|
|
|
- // 确保扫描状态被重置
|
|
|
- this.isScanning = false;
|
|
|
- console.log('扫描过程结束');
|
|
|
- }
|
|
|
- },
|
|
|
-
|
|
|
- // 添加设备到列表
|
|
|
- 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可能无法正常工作');
|
|
|
- }
|
|
|
- },
|
|
|
-
|
|
|
- detectPlatform() {
|
|
|
- // 检测设备类型
|
|
|
- const userAgent = navigator.userAgent;
|
|
|
- const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent);
|
|
|
- const isAndroid = /Android/i.test(userAgent);
|
|
|
- const isIOS = /iPad|iPhone|iPod/.test(userAgent);
|
|
|
- const isWeChat = /MicroMessenger/i.test(userAgent);
|
|
|
- const isEmbedded = window.parent !== window || window.top !== window;
|
|
|
-
|
|
|
- this.isMobileDevice = isMobile;
|
|
|
-
|
|
|
- // 构建平台信息
|
|
|
- let platform = [];
|
|
|
- if (isAndroid) platform.push('Android');
|
|
|
- if (isIOS) platform.push('iOS');
|
|
|
- if (!isMobile) platform.push('PC桌面端');
|
|
|
- if (isWeChat) platform.push('微信内置浏览器');
|
|
|
- if (isEmbedded) platform.push('嵌入式H5');
|
|
|
-
|
|
|
- this.platformInfo = platform.join(' + ');
|
|
|
-
|
|
|
- console.log('📱 检测到平台:', this.platformInfo);
|
|
|
- console.log('📊 设备类型:', this.isMobileDevice ? '移动设备' : '桌面设备');
|
|
|
- console.log('🌐 是否嵌入式:', isEmbedded ? '是' : '否');
|
|
|
-
|
|
|
- // 安卓嵌入式H5的特殊处理
|
|
|
- if (isAndroid && isEmbedded) {
|
|
|
- console.warn('⚠️ 检测到安卓嵌入式H5环境,蓝牙功能可能受限');
|
|
|
- }
|
|
|
-
|
|
|
- return {
|
|
|
- isMobile,
|
|
|
- isAndroid,
|
|
|
- isIOS,
|
|
|
- isWeChat,
|
|
|
- isEmbedded
|
|
|
- };
|
|
|
- }
|
|
|
- },
|
|
|
-
|
|
|
- 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;
|
|
|
-}
|
|
|
-
|
|
|
-/* 平台信息 */
|
|
|
-.platform-info {
|
|
|
- margin: 15px 0;
|
|
|
- padding: 12px;
|
|
|
- background-color: #f8f9fa;
|
|
|
- border: 1px solid #dee2e6;
|
|
|
- border-radius: 8px;
|
|
|
- font-size: 14px;
|
|
|
-}
|
|
|
-
|
|
|
-.platform-info p {
|
|
|
- margin: 3px 0;
|
|
|
-}
|
|
|
-
|
|
|
-.success { color: #28a745; font-weight: bold; }
|
|
|
-.error { color: #dc3545; font-weight: bold; }
|
|
|
-
|
|
|
-/* 信息提示 */
|
|
|
-.info {
|
|
|
- margin: 15px 0;
|
|
|
- padding: 12px;
|
|
|
- background-color: #d1ecf1;
|
|
|
- border: 1px solid #bee5eb;
|
|
|
- border-radius: 8px;
|
|
|
- color: #0c5460;
|
|
|
-}
|
|
|
-
|
|
|
-.info ul {
|
|
|
- margin: 8px 0 0 0;
|
|
|
- padding-left: 20px;
|
|
|
}
|
|
|
-
|
|
|
-.info li {
|
|
|
- margin: 3px 0;
|
|
|
-}
|
|
|
-
|
|
|
-/* 扫描信息提示 */
|
|
|
-.scan-info {
|
|
|
- margin: 15px 0;
|
|
|
- padding: 15px;
|
|
|
- background-color: #d1ecf1;
|
|
|
- border: 1px solid #bee5eb;
|
|
|
- border-radius: 8px;
|
|
|
- color: #0c5460;
|
|
|
-}
|
|
|
-
|
|
|
-.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;
|
|
|
-}
|
|
|
-
|
|
|
-.scan-info-compact {
|
|
|
- font-size: 14px;
|
|
|
- color: #666;
|
|
|
-}
|
|
|
-
|
|
|
-.mobile-warning {
|
|
|
- color: #856404 !important;
|
|
|
- background-color: #fff3cd;
|
|
|
- padding: 8px;
|
|
|
- border-radius: 4px;
|
|
|
- margin-top: 8px !important;
|
|
|
- border: 1px solid #ffeaa7;
|
|
|
-}
|
|
|
-
|
|
|
-
|
|
|
-.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-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 {
|
|
|
- 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>
|
|
|
+</script>
|