1284 lines
44 KiB
Vue
1284 lines
44 KiB
Vue
<script lang="ts" setup>
|
||
/**
|
||
* 蓝牙设备测试页面
|
||
* 功能:使用Web Bluetooth API连接和控制蓝牙设备
|
||
* 支持:设备扫描、连接、数据通信、WiFi配置、自定义命令等
|
||
*/
|
||
|
||
// 页面布局配置
|
||
definePageMeta({
|
||
layout: 'empty' // 使用空布局,不包含头部和底部
|
||
})
|
||
|
||
/**
|
||
* 蓝牙设备状态管理
|
||
* 用于跟踪蓝牙连接的各种状态信息
|
||
*/
|
||
const bluetoothState = ref({
|
||
isSupported: false, // 浏览器是否支持Web Bluetooth API
|
||
isConnected: false, // 是否已连接到设备
|
||
isScanning: false, // 是否正在扫描设备
|
||
devices: [] as BluetoothDevice[], // 发现的设备列表
|
||
selectedDevice: null as BluetoothDevice | null, // 当前选中的设备
|
||
server: null as BluetoothRemoteGATTServer | null, // GATT服务器连接
|
||
service: null as BluetoothRemoteGATTService | null, // 目标服务
|
||
characteristic: null as BluetoothRemoteGATTCharacteristic | null, // 通用特征值
|
||
writeCharacteristic: null as BluetoothRemoteGATTCharacteristic | null, // 写入特征值
|
||
readCharacteristic: null as BluetoothRemoteGATTCharacteristic | null // 读取特征值
|
||
})
|
||
|
||
/**
|
||
* 设备信息管理
|
||
* 存储连接设备的基本信息和状态
|
||
*/
|
||
const deviceInfo = ref({
|
||
name: '', // 设备名称
|
||
id: '', // 设备唯一标识符
|
||
connected: false, // 连接状态
|
||
batteryLevel: 0, // 电池电量百分比
|
||
signalStrength: 0, // 信号强度
|
||
firmwareVersion: '', // 固件版本
|
||
deviceType: '', // 设备类型
|
||
services: [] as string[] // 设备支持的服务列表
|
||
})
|
||
|
||
/**
|
||
* 控制命令管理
|
||
* 存储各种设备控制命令的参数
|
||
*/
|
||
const controlCommands = ref({
|
||
ledOn: false, // LED开关状态
|
||
motorSpeed: 0, // 电机速度 (0-100)
|
||
temperature: 0, // 温度设置 (0-50°C)
|
||
humidity: 0, // 湿度设置 (0-100%)
|
||
wifiSsid: '', // WiFi网络名称
|
||
wifiPassword: '', // WiFi密码
|
||
customData: '' // 自定义数据命令
|
||
})
|
||
|
||
/**
|
||
* 设备配置管理
|
||
* 存储蓝牙设备的服务UUID和通信配置
|
||
*/
|
||
const deviceConfig = ref({
|
||
serviceUuid: '000000ff-0000-1000-8000-00805f9b34fb', // 主服务UUID (小写格式)
|
||
writeUuid: '0000ff01-0000-1000-8000-00805f9b34fb', // 写入特征值UUID
|
||
readUuid: '0000ff02-0000-1000-8000-00805f9b34fb', // 读取特征值UUID
|
||
sequenceControl: 0, // 序列号控制
|
||
isEncrypted: false, // 是否启用加密
|
||
isChecksum: true // 是否启用校验和
|
||
})
|
||
|
||
/**
|
||
* 日志信息管理
|
||
* 存储操作日志,用于调试和状态跟踪
|
||
*/
|
||
const logs = ref<string[]>([])
|
||
|
||
/**
|
||
* 页面初始化
|
||
* 检查浏览器对Web Bluetooth API的支持情况
|
||
*/
|
||
onMounted(() => {
|
||
// 检查浏览器是否支持Web Bluetooth API
|
||
bluetoothState.value.isSupported = 'bluetooth' in navigator
|
||
|
||
if (!bluetoothState.value.isSupported) {
|
||
addLog('浏览器不支持Web Bluetooth API')
|
||
} else {
|
||
addLog('Web Bluetooth API 支持正常')
|
||
}
|
||
})
|
||
|
||
/**
|
||
* 添加日志记录
|
||
* @param message 日志消息内容
|
||
* 功能:将操作日志添加到日志列表,包含时间戳,最多保留50条记录
|
||
*/
|
||
const addLog = (message: string) => {
|
||
const timestamp = new Date().toLocaleTimeString() // 获取当前时间
|
||
logs.value.unshift(`[${timestamp}] ${message}`) // 将新日志添加到列表开头
|
||
|
||
// 限制日志数量,避免内存占用过多
|
||
if (logs.value.length > 50) {
|
||
logs.value = logs.value.slice(0, 50)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 扫描所有蓝牙设备
|
||
* 功能:扫描附近所有可用的蓝牙设备,不限制服务类型
|
||
*/
|
||
const scanDevices = async () => {
|
||
// 检查浏览器支持
|
||
if (!bluetoothState.value.isSupported) {
|
||
addLog('浏览器不支持蓝牙')
|
||
return
|
||
}
|
||
|
||
try {
|
||
bluetoothState.value.isScanning = true
|
||
addLog('开始扫描蓝牙设备...')
|
||
|
||
// 请求设备选择,接受所有设备类型
|
||
const device = await navigator.bluetooth.requestDevice({
|
||
acceptAllDevices: true, // 接受所有设备
|
||
optionalServices: [
|
||
'battery_service', // 电池服务
|
||
'device_information', // 设备信息服务
|
||
'generic_access' // 通用访问服务
|
||
]
|
||
})
|
||
|
||
if (device) {
|
||
// 保存选中的设备信息
|
||
bluetoothState.value.selectedDevice = device
|
||
deviceInfo.value.name = device.name || '未知设备'
|
||
deviceInfo.value.id = device.id
|
||
addLog(`发现设备: ${deviceInfo.value.name}`)
|
||
|
||
// 监听设备断开连接事件
|
||
device.addEventListener('gattserverdisconnected', onDeviceDisconnected)
|
||
}
|
||
} catch (error) {
|
||
addLog(`扫描失败: ${error}`)
|
||
} finally {
|
||
bluetoothState.value.isScanning = false
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 扫描特定服务设备
|
||
* 功能:只扫描包含指定服务的蓝牙设备,用于精确匹配目标设备
|
||
*/
|
||
const scanSpecificService = async () => {
|
||
// 检查浏览器支持
|
||
if (!bluetoothState.value.isSupported) {
|
||
addLog('浏览器不支持蓝牙')
|
||
return
|
||
}
|
||
|
||
try {
|
||
bluetoothState.value.isScanning = true
|
||
addLog('开始扫描特定服务设备...')
|
||
|
||
// 请求设备选择,只显示包含指定服务的设备
|
||
const device = await navigator.bluetooth.requestDevice({
|
||
filters: [
|
||
{ services: [deviceConfig.value.serviceUuid] } // 过滤条件:包含指定服务
|
||
],
|
||
optionalServices: [
|
||
'battery_service', // 电池服务
|
||
'device_information', // 设备信息服务
|
||
'generic_access' // 通用访问服务
|
||
]
|
||
})
|
||
|
||
if (device) {
|
||
// 保存选中的设备信息
|
||
bluetoothState.value.selectedDevice = device
|
||
deviceInfo.value.name = device.name || '未知设备'
|
||
deviceInfo.value.id = device.id
|
||
addLog(`发现设备: ${deviceInfo.value.name}`)
|
||
|
||
// 监听设备断开连接事件
|
||
device.addEventListener('gattserverdisconnected', onDeviceDisconnected)
|
||
}
|
||
} catch (error) {
|
||
addLog(`扫描特定服务失败: ${error}`)
|
||
} finally {
|
||
bluetoothState.value.isScanning = false
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 连接蓝牙设备
|
||
* 功能:建立与选中设备的GATT连接,并初始化设备服务
|
||
*/
|
||
const connectDevice = async () => {
|
||
// 检查是否已选择设备
|
||
if (!bluetoothState.value.selectedDevice) {
|
||
addLog('请先选择设备')
|
||
return
|
||
}
|
||
|
||
try {
|
||
addLog('正在连接设备...')
|
||
|
||
// 建立GATT连接
|
||
bluetoothState.value.server = await bluetoothState.value.selectedDevice.gatt?.connect()
|
||
|
||
if (bluetoothState.value.server) {
|
||
// 更新连接状态
|
||
bluetoothState.value.isConnected = true
|
||
deviceInfo.value.connected = true
|
||
addLog('设备连接成功')
|
||
|
||
// 初始化设备服务和特征值
|
||
await initDevice()
|
||
}
|
||
} catch (error) {
|
||
addLog(`连接失败: ${error}`)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 断开设备连接
|
||
* 功能:主动断开与设备的连接
|
||
*/
|
||
const disconnectDevice = async () => {
|
||
if (bluetoothState.value.server) {
|
||
bluetoothState.value.server.disconnect()
|
||
onDeviceDisconnected()
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 设备断开连接处理
|
||
* 功能:处理设备断开连接事件,清理相关状态和资源
|
||
*/
|
||
const onDeviceDisconnected = () => {
|
||
// 重置连接状态
|
||
bluetoothState.value.isConnected = false
|
||
deviceInfo.value.connected = false
|
||
|
||
// 清理连接资源
|
||
bluetoothState.value.server = null
|
||
bluetoothState.value.service = null
|
||
bluetoothState.value.characteristic = null
|
||
bluetoothState.value.writeCharacteristic = null
|
||
bluetoothState.value.readCharacteristic = null
|
||
|
||
addLog('设备已断开连接')
|
||
}
|
||
|
||
/**
|
||
* 初始化设备
|
||
* 功能:发现设备服务,获取特征值,启用数据通知,发送初始化命令
|
||
*/
|
||
const initDevice = async () => {
|
||
if (!bluetoothState.value.server) return
|
||
|
||
try {
|
||
addLog('正在初始化设备...')
|
||
|
||
// 获取设备的所有主要服务
|
||
const services = await bluetoothState.value.server.getPrimaryServices()
|
||
deviceInfo.value.services = services.map(s => s.uuid)
|
||
addLog(`发现 ${services.length} 个服务`)
|
||
|
||
// 打印所有服务UUID用于调试
|
||
services.forEach((service, index) => {
|
||
addLog(`服务 ${index + 1}: ${service.uuid}`)
|
||
})
|
||
|
||
// 查找目标服务(不区分大小写匹配)
|
||
const targetService = services.find(s =>
|
||
s.uuid.toLowerCase() === deviceConfig.value.serviceUuid.toLowerCase()
|
||
)
|
||
|
||
if (targetService) {
|
||
bluetoothState.value.service = targetService
|
||
addLog('找到目标服务')
|
||
|
||
// 获取目标服务的所有特征值
|
||
const characteristics = await targetService.getCharacteristics()
|
||
addLog(`发现 ${characteristics.length} 个特征值`)
|
||
|
||
// 打印所有特征值UUID用于调试
|
||
characteristics.forEach((char, index) => {
|
||
addLog(`特征值 ${index + 1}: ${char.uuid}`)
|
||
})
|
||
|
||
// 查找读写特征值(不区分大小写匹配)
|
||
bluetoothState.value.writeCharacteristic = characteristics.find(c =>
|
||
c.uuid.toLowerCase() === deviceConfig.value.writeUuid.toLowerCase()
|
||
)
|
||
bluetoothState.value.readCharacteristic = characteristics.find(c =>
|
||
c.uuid.toLowerCase() === deviceConfig.value.readUuid.toLowerCase()
|
||
)
|
||
|
||
if (bluetoothState.value.writeCharacteristic) {
|
||
addLog('找到写入特征值')
|
||
}
|
||
if (bluetoothState.value.readCharacteristic) {
|
||
addLog('找到读取特征值')
|
||
|
||
// 启用特征值通知,用于接收设备数据
|
||
await bluetoothState.value.readCharacteristic.startNotifications()
|
||
bluetoothState.value.readCharacteristic.addEventListener('characteristicvaluechanged', onCharacteristicValueChanged)
|
||
addLog('已启用数据通知')
|
||
}
|
||
|
||
// 获取设备基本信息(如电池电量)
|
||
await getDeviceInfo()
|
||
|
||
// 发送初始化命令获取固件版本
|
||
await sendCustomData('get_fw')
|
||
} else {
|
||
addLog('未找到目标服务,尝试获取设备基本信息')
|
||
await getDeviceInfo()
|
||
|
||
// 如果没有目标服务,尝试使用通用访问服务进行通信
|
||
const genericAccessService = services.find(s => s.uuid === '00001800-0000-1000-8000-00805f9b34fb')
|
||
if (genericAccessService) {
|
||
addLog('找到通用访问服务,尝试获取特征值')
|
||
const characteristics = await genericAccessService.getCharacteristics()
|
||
|
||
// 打印通用访问服务的特征值
|
||
characteristics.forEach((char, index) => {
|
||
addLog(`通用访问特征值 ${index + 1}: ${char.uuid}`)
|
||
})
|
||
|
||
// 查找可写的特征值
|
||
const writableChars = characteristics.filter(c => c.properties.write || c.properties.writeWithoutResponse)
|
||
if (writableChars.length > 0) {
|
||
bluetoothState.value.writeCharacteristic = writableChars[0]
|
||
addLog(`使用通用访问服务的写入特征值: ${writableChars[0].uuid}`)
|
||
}
|
||
|
||
// 查找可读的特征值
|
||
const readableChars = characteristics.filter(c => c.properties.read || c.properties.notify)
|
||
if (readableChars.length > 0) {
|
||
bluetoothState.value.readCharacteristic = readableChars[0]
|
||
addLog(`使用通用访问服务的读取特征值: ${readableChars[0].uuid}`)
|
||
|
||
// 尝试读取设备名称
|
||
const deviceNameChar = characteristics.find(c => c.uuid === '00002a00-0000-1000-8000-00805f9b34fb')
|
||
if (deviceNameChar && deviceNameChar.properties.read) {
|
||
try {
|
||
const deviceNameValue = await deviceNameChar.readValue()
|
||
const deviceName = String.fromCharCode(...new Uint8Array(deviceNameValue.buffer))
|
||
deviceInfo.value.name = deviceName
|
||
addLog(`读取设备名称: ${deviceName}`)
|
||
} catch (readError) {
|
||
addLog(`读取设备名称失败: ${readError}`)
|
||
}
|
||
}
|
||
|
||
// 尝试读取外观信息
|
||
const appearanceChar = characteristics.find(c => c.uuid === '00002a01-0000-1000-8000-00805f9b34fb')
|
||
if (appearanceChar && appearanceChar.properties.read) {
|
||
try {
|
||
const appearanceValue = await appearanceChar.readValue()
|
||
const appearance = new DataView(appearanceValue.buffer).getUint16(0, true)
|
||
addLog(`设备外观: 0x${appearance.toString(16)}`)
|
||
} catch (readError) {
|
||
addLog(`读取设备外观失败: ${readError}`)
|
||
}
|
||
}
|
||
|
||
// 尝试启用通知(如果支持)
|
||
if (readableChars[0].properties.notify) {
|
||
try {
|
||
await bluetoothState.value.readCharacteristic.startNotifications()
|
||
bluetoothState.value.readCharacteristic.addEventListener('characteristicvaluechanged', onCharacteristicValueChanged)
|
||
addLog('已启用通用访问服务的数据通知')
|
||
} catch (notifyError) {
|
||
addLog('无法启用通用访问服务的数据通知')
|
||
}
|
||
} else {
|
||
addLog('该特征值不支持通知功能')
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} catch (error) {
|
||
addLog(`设备初始化失败: ${error}`)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 特征值变化处理
|
||
* 功能:处理从设备接收到的数据,解析并显示相关信息
|
||
* @param event 特征值变化事件
|
||
*/
|
||
const onCharacteristicValueChanged = (event: Event) => {
|
||
const characteristic = event.target as BluetoothRemoteGATTCharacteristic
|
||
const value = characteristic.value
|
||
|
||
if (value) {
|
||
// 将接收到的数据转换为文本
|
||
const data = new Uint8Array(value.buffer)
|
||
const text = String.fromCharCode(...data)
|
||
addLog(`收到数据: ${text}`)
|
||
|
||
// 处理特定的设备响应
|
||
if (text.includes('wifi_ok')) {
|
||
addLog('WiFi连接成功')
|
||
} else if (text.includes('wifi_err')) {
|
||
addLog('WiFi连接失败')
|
||
} else if (text.includes('fw_version')) {
|
||
// 解析固件版本信息
|
||
deviceInfo.value.firmwareVersion = text.replace('fw_version:', '')
|
||
addLog(`固件版本: ${deviceInfo.value.firmwareVersion}`)
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取设备信息
|
||
* 功能:读取设备的基本信息,如电池电量等
|
||
*/
|
||
const getDeviceInfo = async () => {
|
||
if (!bluetoothState.value.server) return
|
||
|
||
try {
|
||
// 尝试获取电池服务并读取电池电量
|
||
try {
|
||
const batteryService = await bluetoothState.value.server.getPrimaryService('battery_service')
|
||
const batteryCharacteristic = await batteryService.getCharacteristic('battery_level')
|
||
const batteryValue = await batteryCharacteristic.readValue()
|
||
deviceInfo.value.batteryLevel = batteryValue.getUint8(0)
|
||
addLog(`电池电量: ${deviceInfo.value.batteryLevel}%`)
|
||
} catch (batteryError) {
|
||
addLog('设备不支持电池服务')
|
||
deviceInfo.value.batteryLevel = 0
|
||
}
|
||
|
||
// 尝试获取设备信息服务
|
||
try {
|
||
const deviceInfoService = await bluetoothState.value.server.getPrimaryService('device_information')
|
||
const characteristics = await deviceInfoService.getCharacteristics()
|
||
|
||
// 尝试读取设备名称
|
||
const deviceNameChar = characteristics.find(c => c.uuid === '00002a00-0000-1000-8000-00805f9b34fb')
|
||
if (deviceNameChar) {
|
||
const deviceNameValue = await deviceNameChar.readValue()
|
||
const deviceName = String.fromCharCode(...new Uint8Array(deviceNameValue.buffer))
|
||
addLog(`设备名称: ${deviceName}`)
|
||
}
|
||
|
||
// 尝试读取固件版本
|
||
const firmwareChar = characteristics.find(c => c.uuid === '00002a26-0000-1000-8000-00805f9b34fb')
|
||
if (firmwareChar) {
|
||
const firmwareValue = await firmwareChar.readValue()
|
||
const firmwareVersion = String.fromCharCode(...new Uint8Array(firmwareValue.buffer))
|
||
deviceInfo.value.firmwareVersion = firmwareVersion
|
||
addLog(`固件版本: ${firmwareVersion}`)
|
||
}
|
||
} catch (deviceInfoError) {
|
||
addLog('设备不支持设备信息服务')
|
||
}
|
||
|
||
} catch (error) {
|
||
addLog(`获取设备信息失败: ${error}`)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 发送数据到设备
|
||
* 功能:通过写入特征值向设备发送数据
|
||
* @param data 要发送的数据(Uint8Array格式)
|
||
* @returns 发送是否成功
|
||
*/
|
||
const sendDataToDevice = async (data: Uint8Array) => {
|
||
if (!bluetoothState.value.writeCharacteristic) {
|
||
addLog('写入特征值不可用,尝试查找可写特征值')
|
||
|
||
// 尝试查找其他可写的特征值
|
||
if (bluetoothState.value.server) {
|
||
try {
|
||
const services = await bluetoothState.value.server.getPrimaryServices()
|
||
for (const service of services) {
|
||
const characteristics = await service.getCharacteristics()
|
||
const writableChars = characteristics.filter(c =>
|
||
c.properties.write || c.properties.writeWithoutResponse
|
||
)
|
||
|
||
if (writableChars.length > 0) {
|
||
bluetoothState.value.writeCharacteristic = writableChars[0]
|
||
addLog(`找到可写特征值: ${writableChars[0].uuid}`)
|
||
break
|
||
}
|
||
}
|
||
} catch (error) {
|
||
addLog(`查找可写特征值失败: ${error}`)
|
||
return false
|
||
}
|
||
}
|
||
|
||
if (!bluetoothState.value.writeCharacteristic) {
|
||
addLog('未找到任何可写的特征值,无法发送控制命令')
|
||
addLog('提示:该设备可能不支持通过蓝牙进行控制,或者需要特定的服务UUID')
|
||
return false
|
||
}
|
||
}
|
||
|
||
try {
|
||
// 通过写入特征值发送数据
|
||
await bluetoothState.value.writeCharacteristic.writeValue(data)
|
||
return true
|
||
} catch (error) {
|
||
addLog(`发送数据失败: ${error}`)
|
||
return false
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 字符串转ArrayBuffer
|
||
* 功能:将字符串转换为ArrayBuffer格式,用于蓝牙数据传输
|
||
* @param str 要转换的字符串
|
||
* @returns ArrayBuffer格式的数据
|
||
*/
|
||
const stringToBuffer = (str: string): ArrayBuffer => {
|
||
const bytes = new Uint8Array(str.length)
|
||
for (let i = 0; i < str.length; i++) {
|
||
bytes[i] = str.charCodeAt(i) // 将字符转换为ASCII码
|
||
}
|
||
return bytes.buffer
|
||
}
|
||
|
||
/**
|
||
* 发送自定义数据
|
||
* 功能:向设备发送自定义字符串数据
|
||
* @param data 要发送的字符串数据
|
||
*/
|
||
const sendCustomData = async (data: string) => {
|
||
if (!bluetoothState.value.isConnected) {
|
||
addLog('设备未连接')
|
||
return
|
||
}
|
||
|
||
try {
|
||
// 将字符串转换为ArrayBuffer
|
||
const buffer = stringToBuffer(data)
|
||
const success = await sendDataToDevice(new Uint8Array(buffer))
|
||
if (success) {
|
||
addLog(`发送自定义数据: ${data}`)
|
||
}
|
||
} catch (error) {
|
||
addLog(`发送自定义数据失败: ${error}`)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 发送WiFi配置
|
||
* 功能:向设备发送WiFi网络配置信息(SSID和密码)
|
||
*/
|
||
const sendWiFiConfig = async () => {
|
||
if (!bluetoothState.value.isConnected) {
|
||
addLog('设备未连接')
|
||
return
|
||
}
|
||
|
||
if (!controlCommands.value.wifiSsid || !controlCommands.value.wifiPassword) {
|
||
addLog('请输入WiFi名称和密码')
|
||
return
|
||
}
|
||
|
||
try {
|
||
addLog('正在发送WiFi配置...')
|
||
|
||
// 发送WiFi网络名称(SSID)
|
||
const ssidBuffer = stringToBuffer(controlCommands.value.wifiSsid)
|
||
await sendDataToDevice(new Uint8Array(ssidBuffer))
|
||
|
||
// 发送WiFi密码
|
||
const passwordBuffer = stringToBuffer(controlCommands.value.wifiPassword)
|
||
await sendDataToDevice(new Uint8Array(passwordBuffer))
|
||
|
||
addLog('WiFi配置发送完成')
|
||
} catch (error) {
|
||
addLog(`发送WiFi配置失败: ${error}`)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 发送控制命令
|
||
* 功能:向设备发送各种控制命令,如LED、电机、温度、湿度等
|
||
* @param command 命令类型
|
||
* @param value 命令参数值
|
||
*/
|
||
const sendCommand = async (command: string, value?: number) => {
|
||
if (!bluetoothState.value.isConnected) {
|
||
addLog('设备未连接')
|
||
return
|
||
}
|
||
|
||
try {
|
||
let data: Uint8Array
|
||
|
||
// 根据命令类型构造不同的数据包
|
||
switch (command) {
|
||
case 'led':
|
||
// LED控制:0x01 + 状态(0x00关闭/0x01开启)
|
||
data = new Uint8Array([0x01, value ? 0x01 : 0x00])
|
||
addLog(`发送LED命令: ${value ? '开启' : '关闭'}`)
|
||
break
|
||
case 'motor':
|
||
// 电机控制:0x02 + 速度值(0-100)
|
||
data = new Uint8Array([0x02, value || 0])
|
||
addLog(`发送电机速度: ${value}%`)
|
||
break
|
||
case 'temperature':
|
||
// 温度控制:0x03 + 温度值(0-50)
|
||
data = new Uint8Array([0x03, value || 0])
|
||
addLog(`设置温度: ${value}°C`)
|
||
break
|
||
case 'humidity':
|
||
// 湿度控制:0x04 + 湿度值(0-100)
|
||
data = new Uint8Array([0x04, value || 0])
|
||
addLog(`设置湿度: ${value}%`)
|
||
break
|
||
default:
|
||
addLog(`未知命令: ${command}`)
|
||
return
|
||
}
|
||
|
||
// 发送构造好的数据包
|
||
await sendDataToDevice(data)
|
||
} catch (error) {
|
||
addLog(`发送命令失败: ${error}`)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 控制LED开关
|
||
* 功能:切换LED灯的开关状态
|
||
*/
|
||
const toggleLED = () => {
|
||
controlCommands.value.ledOn = !controlCommands.value.ledOn
|
||
sendCommand('led', controlCommands.value.ledOn ? 1 : 0)
|
||
}
|
||
|
||
/**
|
||
* 控制电机速度
|
||
* 功能:设置电机运行速度
|
||
* @param speed 电机速度 (0-100)
|
||
*/
|
||
const setMotorSpeed = (speed: number) => {
|
||
controlCommands.value.motorSpeed = speed
|
||
sendCommand('motor', speed)
|
||
}
|
||
|
||
/**
|
||
* 设置温度
|
||
* 功能:设置设备的目标温度
|
||
* @param temp 目标温度 (0-50°C)
|
||
*/
|
||
const setTemperature = (temp: number) => {
|
||
controlCommands.value.temperature = temp
|
||
sendCommand('temperature', temp)
|
||
}
|
||
|
||
/**
|
||
* 设置湿度
|
||
* 功能:设置设备的目标湿度
|
||
* @param humidity 目标湿度 (0-100%)
|
||
*/
|
||
const setHumidity = (humidity: number) => {
|
||
controlCommands.value.humidity = humidity
|
||
sendCommand('humidity', humidity)
|
||
}
|
||
|
||
/**
|
||
* 发送自定义命令
|
||
* 功能:发送用户输入的自定义数据命令
|
||
*/
|
||
const sendCustomCommand = () => {
|
||
if (controlCommands.value.customData) {
|
||
sendCustomData(controlCommands.value.customData)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 读取通用访问服务信息
|
||
* 功能:读取通用访问服务的所有可用信息
|
||
*/
|
||
const readGenericAccessInfo = async () => {
|
||
if (!bluetoothState.value.isConnected) {
|
||
addLog('设备未连接')
|
||
return
|
||
}
|
||
|
||
try {
|
||
addLog('读取通用访问服务信息...')
|
||
|
||
// 获取通用访问服务
|
||
const genericAccessService = await bluetoothState.value.server.getPrimaryService('00001800-0000-1000-8000-00805f9b34fb')
|
||
const characteristics = await genericAccessService.getCharacteristics()
|
||
|
||
for (const char of characteristics) {
|
||
addLog(`特征值: ${char.uuid}`)
|
||
addLog(`属性: ${JSON.stringify(char.properties)}`)
|
||
|
||
// 根据特征值UUID读取对应信息
|
||
if (char.properties.read) {
|
||
try {
|
||
const value = await char.readValue()
|
||
|
||
switch (char.uuid) {
|
||
case '00002a00-0000-1000-8000-00805f9b34fb': // 设备名称
|
||
const deviceName = String.fromCharCode(...new Uint8Array(value.buffer))
|
||
deviceInfo.value.name = deviceName
|
||
addLog(`设备名称: ${deviceName}`)
|
||
break
|
||
|
||
case '00002a01-0000-1000-8000-00805f9b34fb': // 外观
|
||
const appearance = new DataView(value.buffer).getUint16(0, true)
|
||
addLog(`设备外观: 0x${appearance.toString(16)}`)
|
||
break
|
||
|
||
case '00002a02-0000-1000-8000-00805f9b34fb': // 隐私标志
|
||
const privacyFlag = new DataView(value.buffer).getUint8(0)
|
||
addLog(`隐私标志: ${privacyFlag}`)
|
||
break
|
||
|
||
case '00002a03-0000-1000-8000-00805f9b34fb': // 重新连接地址
|
||
const reconnectAddress = Array.from(new Uint8Array(value.buffer))
|
||
.map(b => b.toString(16).padStart(2, '0'))
|
||
.join(':')
|
||
addLog(`重新连接地址: ${reconnectAddress}`)
|
||
break
|
||
|
||
case '00002a04-0000-1000-8000-00805f9b34fb': // 外设首选连接参数
|
||
const params = new DataView(value.buffer)
|
||
const minInterval = params.getUint16(0, true)
|
||
const maxInterval = params.getUint16(2, true)
|
||
const latency = params.getUint16(4, true)
|
||
const timeout = params.getUint16(6, true)
|
||
addLog(`连接参数: 最小间隔=${minInterval}ms, 最大间隔=${maxInterval}ms, 延迟=${latency}, 超时=${timeout}ms`)
|
||
break
|
||
|
||
case '00002aa6-0000-1000-8000-00805f9b34fb': // 中央地址解析
|
||
const centralAddressResolution = new DataView(value.buffer).getUint8(0)
|
||
addLog(`中央地址解析: ${centralAddressResolution ? '支持' : '不支持'}`)
|
||
break
|
||
|
||
default:
|
||
// 其他特征值,尝试作为文本读取
|
||
const text = String.fromCharCode(...new Uint8Array(value.buffer))
|
||
addLog(`未知特征值数据: ${text}`)
|
||
break
|
||
}
|
||
} catch (readError) {
|
||
addLog(`读取特征值 ${char.uuid} 失败: ${readError}`)
|
||
}
|
||
}
|
||
}
|
||
} catch (error) {
|
||
addLog(`读取通用访问服务信息失败: ${error}`)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 查找可写特征值
|
||
* 功能:在所有服务中查找支持写入操作的特征值
|
||
*/
|
||
const findWritableCharacteristics = async () => {
|
||
if (!bluetoothState.value.isConnected) {
|
||
addLog('设备未连接')
|
||
return
|
||
}
|
||
|
||
try {
|
||
addLog('查找可写特征值...')
|
||
|
||
const services = await bluetoothState.value.server.getPrimaryServices()
|
||
const writableChars = []
|
||
|
||
for (const service of services) {
|
||
addLog(`检查服务: ${service.uuid}`)
|
||
|
||
try {
|
||
const characteristics = await service.getCharacteristics()
|
||
|
||
for (const char of characteristics) {
|
||
const properties = char.properties
|
||
addLog(`特征值: ${char.uuid}`)
|
||
addLog(`属性: ${JSON.stringify(properties)}`)
|
||
|
||
// 检查是否支持写入
|
||
if (properties.write || properties.writeWithoutResponse) {
|
||
writableChars.push({
|
||
serviceUuid: service.uuid,
|
||
characteristicUuid: char.uuid,
|
||
properties: properties
|
||
})
|
||
addLog(`✓ 找到可写特征值: ${char.uuid}`)
|
||
}
|
||
|
||
// 检查是否支持读取
|
||
if (properties.read) {
|
||
addLog(`✓ 找到可读特征值: ${char.uuid}`)
|
||
}
|
||
|
||
// 检查是否支持通知
|
||
if (properties.notify) {
|
||
addLog(`✓ 找到可通知特征值: ${char.uuid}`)
|
||
}
|
||
}
|
||
} catch (charError) {
|
||
addLog(`获取特征值失败: ${charError}`)
|
||
}
|
||
}
|
||
|
||
if (writableChars.length > 0) {
|
||
addLog(`共找到 ${writableChars.length} 个可写特征值:`)
|
||
writableChars.forEach((char, index) => {
|
||
addLog(`${index + 1}. 服务: ${char.serviceUuid}`)
|
||
addLog(` 特征值: ${char.characteristicUuid}`)
|
||
addLog(` 属性: ${JSON.stringify(char.properties)}`)
|
||
})
|
||
|
||
// 使用第一个可写特征值
|
||
const firstWritable = writableChars[0]
|
||
const service = await bluetoothState.value.server.getPrimaryService(firstWritable.serviceUuid)
|
||
const characteristic = await service.getCharacteristic(firstWritable.characteristicUuid)
|
||
bluetoothState.value.writeCharacteristic = characteristic
|
||
addLog(`已设置写入特征值: ${firstWritable.characteristicUuid}`)
|
||
} else {
|
||
addLog('未找到任何可写的特征值')
|
||
addLog('该设备可能不支持通过蓝牙进行控制')
|
||
}
|
||
|
||
} catch (error) {
|
||
addLog(`查找可写特征值失败: ${error}`)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 尝试通用设备通信
|
||
* 功能:当设备没有特定服务时,尝试使用通用方式进行通信
|
||
*/
|
||
const tryGenericCommunication = async () => {
|
||
if (!bluetoothState.value.isConnected) {
|
||
addLog('设备未连接')
|
||
return
|
||
}
|
||
|
||
try {
|
||
addLog('尝试通用设备通信...')
|
||
|
||
// 首先读取通用访问服务信息
|
||
await readGenericAccessInfo()
|
||
|
||
// 查找可写特征值
|
||
await findWritableCharacteristics()
|
||
|
||
// 获取所有服务
|
||
const services = await bluetoothState.value.server.getPrimaryServices()
|
||
|
||
for (const service of services) {
|
||
addLog(`检查服务: ${service.uuid}`)
|
||
|
||
try {
|
||
const characteristics = await service.getCharacteristics()
|
||
|
||
for (const char of characteristics) {
|
||
addLog(`特征值: ${char.uuid}, 属性: ${JSON.stringify(char.properties)}`)
|
||
|
||
// 尝试读取可读的特征值
|
||
if (char.properties.read) {
|
||
try {
|
||
const value = await char.readValue()
|
||
const data = new Uint8Array(value.buffer)
|
||
const text = String.fromCharCode(...data)
|
||
addLog(`读取数据: ${text}`)
|
||
} catch (readError) {
|
||
addLog(`读取失败: ${readError}`)
|
||
}
|
||
}
|
||
|
||
// 尝试启用通知
|
||
if (char.properties.notify) {
|
||
try {
|
||
await char.startNotifications()
|
||
char.addEventListener('characteristicvaluechanged', onCharacteristicValueChanged)
|
||
addLog(`已启用通知: ${char.uuid}`)
|
||
} catch (notifyError) {
|
||
addLog(`启用通知失败: ${notifyError}`)
|
||
}
|
||
}
|
||
}
|
||
} catch (charError) {
|
||
addLog(`获取特征值失败: ${charError}`)
|
||
}
|
||
}
|
||
} catch (error) {
|
||
addLog(`通用通信失败: ${error}`)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 清除日志
|
||
* 功能:清空操作日志列表
|
||
*/
|
||
const clearLogs = () => {
|
||
logs.value = []
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 p-4">
|
||
<div class="max-w-6xl mx-auto">
|
||
<!-- 页面标题 -->
|
||
<div class="text-center mb-8">
|
||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-2">
|
||
蓝牙设备测试
|
||
</h1>
|
||
<p class="text-gray-600 dark:text-gray-300">
|
||
Web Bluetooth API 设备连接与控制
|
||
</p>
|
||
</div>
|
||
|
||
<!-- 支持状态 -->
|
||
<div class="mb-6">
|
||
<div class="bg-white dark:bg-gray-800 rounded-lg p-4 shadow-sm">
|
||
<div class="flex items-center space-x-2">
|
||
<div
|
||
class="w-3 h-3 rounded-full"
|
||
:class="bluetoothState.isSupported ? 'bg-green-500' : 'bg-red-500'"
|
||
></div>
|
||
<span class="text-sm font-medium text-gray-900 dark:text-white">
|
||
Web Bluetooth API {{ bluetoothState.isSupported ? '支持' : '不支持' }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||
<!-- 设备控制面板 -->
|
||
<div class="space-y-6">
|
||
<!-- 设备扫描 -->
|
||
<div class="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm">
|
||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">
|
||
设备扫描
|
||
</h2>
|
||
<div class="space-y-4">
|
||
<button
|
||
@click="scanDevices"
|
||
:disabled="bluetoothState.isScanning || !bluetoothState.isSupported"
|
||
class="w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white font-medium rounded-lg transition-colors"
|
||
>
|
||
{{ bluetoothState.isScanning ? '扫描中...' : '扫描所有设备' }}
|
||
</button>
|
||
|
||
<button
|
||
@click="scanSpecificService"
|
||
:disabled="bluetoothState.isScanning || !bluetoothState.isSupported"
|
||
class="w-full px-4 py-2 bg-indigo-600 hover:bg-indigo-700 disabled:bg-gray-400 text-white font-medium rounded-lg transition-colors"
|
||
>
|
||
{{ bluetoothState.isScanning ? '扫描中...' : '扫描特定服务设备' }}
|
||
</button>
|
||
|
||
<button
|
||
@click="connectDevice"
|
||
:disabled="!bluetoothState.selectedDevice || bluetoothState.isConnected"
|
||
class="w-full px-4 py-2 bg-green-600 hover:bg-green-700 disabled:bg-gray-400 text-white font-medium rounded-lg transition-colors"
|
||
>
|
||
连接设备
|
||
</button>
|
||
|
||
<button
|
||
@click="disconnectDevice"
|
||
:disabled="!bluetoothState.isConnected"
|
||
class="w-full px-4 py-2 bg-red-600 hover:bg-red-700 disabled:bg-gray-400 text-white font-medium rounded-lg transition-colors"
|
||
>
|
||
断开连接
|
||
</button>
|
||
|
||
<button
|
||
@click="readGenericAccessInfo"
|
||
:disabled="!bluetoothState.isConnected"
|
||
class="w-full px-4 py-2 bg-orange-600 hover:bg-orange-700 disabled:bg-gray-400 text-white font-medium rounded-lg transition-colors"
|
||
>
|
||
读取通用访问信息
|
||
</button>
|
||
|
||
<button
|
||
@click="findWritableCharacteristics"
|
||
:disabled="!bluetoothState.isConnected"
|
||
class="w-full px-4 py-2 bg-yellow-600 hover:bg-yellow-700 disabled:bg-gray-400 text-white font-medium rounded-lg transition-colors"
|
||
>
|
||
查找可写特征值
|
||
</button>
|
||
|
||
<button
|
||
@click="tryGenericCommunication"
|
||
:disabled="!bluetoothState.isConnected"
|
||
class="w-full px-4 py-2 bg-purple-600 hover:bg-purple-700 disabled:bg-gray-400 text-white font-medium rounded-lg transition-colors"
|
||
>
|
||
通用设备通信
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 设备信息 -->
|
||
<div class="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm">
|
||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">
|
||
设备信息
|
||
</h2>
|
||
<div class="space-y-3">
|
||
<div class="flex justify-between">
|
||
<span class="text-gray-600 dark:text-gray-300">设备名称:</span>
|
||
<span class="text-gray-900 dark:text-white">{{ deviceInfo.name || '未连接' }}</span>
|
||
</div>
|
||
<div class="flex justify-between">
|
||
<span class="text-gray-600 dark:text-gray-300">设备ID:</span>
|
||
<span class="text-gray-900 dark:text-white text-sm">{{ deviceInfo.id || '未连接' }}</span>
|
||
</div>
|
||
<div class="flex justify-between">
|
||
<span class="text-gray-600 dark:text-gray-300">连接状态:</span>
|
||
<span
|
||
class="font-medium"
|
||
:class="deviceInfo.connected ? 'text-green-600' : 'text-red-600'"
|
||
>
|
||
{{ deviceInfo.connected ? '已连接' : '未连接' }}
|
||
</span>
|
||
</div>
|
||
<div class="flex justify-between">
|
||
<span class="text-gray-600 dark:text-gray-300">电池电量:</span>
|
||
<span class="text-gray-900 dark:text-white">{{ deviceInfo.batteryLevel }}%</span>
|
||
</div>
|
||
<div class="flex justify-between">
|
||
<span class="text-gray-600 dark:text-gray-300">固件版本:</span>
|
||
<span class="text-gray-900 dark:text-white">{{ deviceInfo.firmwareVersion || '未知' }}</span>
|
||
</div>
|
||
<div class="flex justify-between">
|
||
<span class="text-gray-600 dark:text-gray-300">服务数量:</span>
|
||
<span class="text-gray-900 dark:text-white">{{ deviceInfo.services.length }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 设备控制 -->
|
||
<div class="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm">
|
||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">
|
||
设备控制
|
||
</h2>
|
||
<div class="space-y-4">
|
||
<!-- LED控制 -->
|
||
<div class="flex items-center justify-between">
|
||
<span class="text-gray-600 dark:text-gray-300">LED控制</span>
|
||
<button
|
||
@click="toggleLED"
|
||
:disabled="!bluetoothState.isConnected"
|
||
class="px-4 py-2 rounded-lg font-medium transition-colors"
|
||
:class="controlCommands.ledOn
|
||
? 'bg-yellow-500 hover:bg-yellow-600 text-white'
|
||
: 'bg-gray-300 hover:bg-gray-400 text-gray-700'"
|
||
>
|
||
{{ controlCommands.ledOn ? '关闭' : '开启' }}
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 电机速度控制 -->
|
||
<div>
|
||
<label class="block text-gray-600 dark:text-gray-300 mb-2">
|
||
电机速度: {{ controlCommands.motorSpeed }}%
|
||
</label>
|
||
<input
|
||
type="range"
|
||
min="0"
|
||
max="100"
|
||
v-model="controlCommands.motorSpeed"
|
||
@input="setMotorSpeed(controlCommands.motorSpeed)"
|
||
:disabled="!bluetoothState.isConnected"
|
||
class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 温度控制 -->
|
||
<div>
|
||
<label class="block text-gray-600 dark:text-gray-300 mb-2">
|
||
温度设置: {{ controlCommands.temperature }}°C
|
||
</label>
|
||
<input
|
||
type="range"
|
||
min="0"
|
||
max="50"
|
||
v-model="controlCommands.temperature"
|
||
@input="setTemperature(controlCommands.temperature)"
|
||
:disabled="!bluetoothState.isConnected"
|
||
class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 湿度控制 -->
|
||
<div>
|
||
<label class="block text-gray-600 dark:text-gray-300 mb-2">
|
||
湿度设置: {{ controlCommands.humidity }}%
|
||
</label>
|
||
<input
|
||
type="range"
|
||
min="0"
|
||
max="100"
|
||
v-model="controlCommands.humidity"
|
||
@input="setHumidity(controlCommands.humidity)"
|
||
:disabled="!bluetoothState.isConnected"
|
||
class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- WiFi配置 -->
|
||
<div class="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm">
|
||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">
|
||
WiFi配置
|
||
</h2>
|
||
<div class="space-y-4">
|
||
<div>
|
||
<label class="block text-gray-600 dark:text-gray-300 mb-2">WiFi名称</label>
|
||
<input
|
||
type="text"
|
||
v-model="controlCommands.wifiSsid"
|
||
placeholder="请输入WiFi名称"
|
||
:disabled="!bluetoothState.isConnected"
|
||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label class="block text-gray-600 dark:text-gray-300 mb-2">WiFi密码</label>
|
||
<input
|
||
type="password"
|
||
v-model="controlCommands.wifiPassword"
|
||
placeholder="请输入WiFi密码"
|
||
:disabled="!bluetoothState.isConnected"
|
||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||
/>
|
||
</div>
|
||
<button
|
||
@click="sendWiFiConfig"
|
||
:disabled="!bluetoothState.isConnected"
|
||
class="w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white font-medium rounded-lg transition-colors"
|
||
>
|
||
发送WiFi配置
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 自定义数据 -->
|
||
<div class="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm">
|
||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">
|
||
自定义数据
|
||
</h2>
|
||
<div class="space-y-4">
|
||
<div>
|
||
<label class="block text-gray-600 dark:text-gray-300 mb-2">自定义命令</label>
|
||
<input
|
||
type="text"
|
||
v-model="controlCommands.customData"
|
||
placeholder="请输入自定义命令"
|
||
:disabled="!bluetoothState.isConnected"
|
||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||
/>
|
||
</div>
|
||
<button
|
||
@click="sendCustomCommand"
|
||
:disabled="!bluetoothState.isConnected"
|
||
class="w-full px-4 py-2 bg-purple-600 hover:bg-purple-700 disabled:bg-gray-400 text-white font-medium rounded-lg transition-colors"
|
||
>
|
||
发送自定义数据
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 日志面板 -->
|
||
<div class="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm">
|
||
<div class="flex items-center justify-between mb-4">
|
||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
|
||
操作日志
|
||
</h2>
|
||
<button
|
||
@click="clearLogs"
|
||
class="px-3 py-1 text-sm bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300 rounded transition-colors"
|
||
>
|
||
清除
|
||
</button>
|
||
</div>
|
||
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4 h-96 overflow-y-auto">
|
||
<div class="space-y-1">
|
||
<div
|
||
v-for="(log, index) in logs"
|
||
:key="index"
|
||
class="text-sm text-gray-700 dark:text-gray-300 font-mono"
|
||
>
|
||
{{ log }}
|
||
</div>
|
||
<div v-if="logs.length === 0" class="text-gray-500 dark:text-gray-400 text-center py-8">
|
||
暂无日志信息
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
/* 自定义滚动条 */
|
||
.overflow-y-auto::-webkit-scrollbar {
|
||
width: 6px;
|
||
}
|
||
|
||
.overflow-y-auto::-webkit-scrollbar-track {
|
||
background: #f1f1f1;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.overflow-y-auto::-webkit-scrollbar-thumb {
|
||
background: #c1c1c1;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.overflow-y-auto::-webkit-scrollbar-thumb:hover {
|
||
background: #a8a8a8;
|
||
}
|
||
|
||
.dark .overflow-y-auto::-webkit-scrollbar-track {
|
||
background: #374151;
|
||
}
|
||
|
||
.dark .overflow-y-auto::-webkit-scrollbar-thumb {
|
||
background: #6b7280;
|
||
}
|
||
|
||
.dark .overflow-y-auto::-webkit-scrollbar-thumb:hover {
|
||
background: #9ca3af;
|
||
}
|
||
|
||
/* 滑块样式 */
|
||
input[type="range"] {
|
||
-webkit-appearance: none;
|
||
appearance: none;
|
||
background: transparent;
|
||
cursor: pointer;
|
||
}
|
||
|
||
input[type="range"]::-webkit-slider-track {
|
||
background: #d1d5db;
|
||
height: 8px;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
input[type="range"]::-webkit-slider-thumb {
|
||
-webkit-appearance: none;
|
||
appearance: none;
|
||
background: #3b82f6;
|
||
height: 20px;
|
||
width: 20px;
|
||
border-radius: 50%;
|
||
cursor: pointer;
|
||
}
|
||
|
||
input[type="range"]::-moz-range-track {
|
||
background: #d1d5db;
|
||
height: 8px;
|
||
border-radius: 4px;
|
||
border: none;
|
||
}
|
||
|
||
input[type="range"]::-moz-range-thumb {
|
||
background: #3b82f6;
|
||
height: 20px;
|
||
width: 20px;
|
||
border-radius: 50%;
|
||
cursor: pointer;
|
||
border: none;
|
||
}
|
||
</style> |