feat 配置蓝牙模块

This commit is contained in:
Ironsp
2026-09-14 10:55:01 +08:00
parent 5f848e6c78
commit 7e863f00f1
4 changed files with 1126 additions and 28 deletions

View File

@@ -1,33 +1,212 @@
<script setup lang="ts">
import { ref } from 'vue'
import type { BluetoothDevice } from '@/utils/bluetooth'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import bluetooth from '@/utils/bluetooth'
const bluetoothEnabled = ref(true)
interface DeviceItem {
name: string
mac: string
deviceId: string
rssi: number
connected: boolean
}
const bluetoothEnabled = ref(false)
const searchValue = ref('')
const scanning = ref(false)
const connectingId = ref('')
const deviceList = ref<DeviceItem[]>([])
const deviceList = ref([
{
name: 'XS-65H',
mac: 'MACC:AA:BB:CC:DD:01',
connected: false,
},
{
name: 'XS-65H',
mac: 'MACC:AA:BB:CC:DD:01',
connected: true,
},
])
let offAdapter: (() => void) | null = null
let offConnection: (() => void) | null = null
let offFound: (() => void) | null = null
function handleConnect(index: number) {
deviceList.value[index].connected = true
const filteredList = computed(() => {
const keyword = searchValue.value.trim().toLowerCase()
if (!keyword)
return deviceList.value
return deviceList.value.filter(item =>
item.name.toLowerCase().includes(keyword)
|| item.mac.toLowerCase().includes(keyword),
)
})
function upsertDevice(device: BluetoothDevice, connected = false) {
const name = device.name || device.localName || '未知设备'
const index = deviceList.value.findIndex(item => item.deviceId === device.deviceId)
const next: DeviceItem = {
name,
mac: device.deviceId,
deviceId: device.deviceId,
rssi: device.RSSI,
connected: connected || deviceList.value[index]?.connected || false,
}
if (index >= 0)
deviceList.value[index] = { ...deviceList.value[index], ...next, connected: deviceList.value[index].connected || connected }
else
deviceList.value.push(next)
}
function handleDisconnect(index: number) {
deviceList.value[index].connected = false
function syncConnectedFlag(deviceId: string, connected: boolean) {
deviceList.value = deviceList.value.map(item => ({
...item,
connected: item.deviceId === deviceId ? connected : (connected ? false : item.connected),
}))
}
function handleRefresh() {
console.log('刷新设备列表')
async function refreshAdapterState() {
try {
bluetoothEnabled.value = await bluetooth.isEnabled()
}
catch {
bluetoothEnabled.value = false
}
}
async function handleBluetoothToggle(event: { detail: { value: boolean } }) {
const wantEnable = !!event.detail.value
try {
if (wantEnable) {
await bluetooth.turnOn()
bluetoothEnabled.value = true
uni.showToast({ title: '蓝牙已开启', icon: 'none' })
}
else {
await bluetooth.turnOff()
bluetoothEnabled.value = false
deviceList.value = []
uni.showToast({ title: '蓝牙已关闭', icon: 'none' })
}
}
catch (err) {
bluetoothEnabled.value = !wantEnable
const message = err instanceof Error ? err.message : '蓝牙开关失败'
uni.showToast({ title: message, icon: 'none', duration: 3000 })
}
}
async function handleRefresh() {
if (scanning.value)
return
if (!bluetoothEnabled.value) {
uni.showToast({ title: '请先开启蓝牙', icon: 'none' })
return
}
scanning.value = true
try {
// 有关键词时优先搜特定设备;否则全量扫描
const keyword = searchValue.value.trim()
if (keyword) {
try {
const device = await bluetooth.scanForDevice({ name: keyword, timeout: 12000 })
upsertDevice(device)
uni.showToast({ title: `已找到 ${device.name || device.deviceId}`, icon: 'none' })
}
catch {
// 特定搜索失败时退回全量扫描,再本地过滤
const devices = await bluetooth.startScan({ timeout: 10000 })
deviceList.value = devices.map(item => ({
name: item.name || item.localName || '未知设备',
mac: item.deviceId,
deviceId: item.deviceId,
rssi: item.RSSI,
connected: bluetooth.getConnection()?.deviceId === item.deviceId,
}))
if (!filteredList.value.length)
uni.showToast({ title: '未找到匹配设备', icon: 'none' })
}
}
else {
const devices = await bluetooth.startScan({ timeout: 10000 })
const connectedId = bluetooth.getConnection()?.deviceId
deviceList.value = devices.map(item => ({
name: item.name || item.localName || '未知设备',
mac: item.deviceId,
deviceId: item.deviceId,
rssi: item.RSSI,
connected: connectedId === item.deviceId,
}))
if (!devices.length)
uni.showToast({ title: '未发现蓝牙设备', icon: 'none' })
}
}
catch (err) {
const message = err instanceof Error ? err.message : '扫描失败'
console.error('蓝牙扫描失败:', err)
uni.showToast({ title: message, icon: 'none', duration: 3000 })
}
finally {
scanning.value = false
}
}
async function handleConnect(device: DeviceItem) {
if (connectingId.value || scanning.value)
return
connectingId.value = device.deviceId
try {
await bluetooth.setupConnection(device.deviceId)
syncConnectedFlag(device.deviceId, true)
uni.showToast({ title: '连接成功', icon: 'success' })
}
catch (err) {
const message = err instanceof Error ? err.message : '连接失败'
console.error('蓝牙连接失败:', err)
uni.showToast({ title: message, icon: 'none', duration: 3000 })
}
finally {
connectingId.value = ''
}
}
async function handleDisconnect(device: DeviceItem) {
try {
await bluetooth.disconnect(device.deviceId)
syncConnectedFlag(device.deviceId, false)
uni.showToast({ title: '已断开', icon: 'none' })
}
catch (err) {
const message = err instanceof Error ? err.message : '断开失败'
uni.showToast({ title: message, icon: 'none', duration: 3000 })
}
}
onMounted(async () => {
offAdapter = bluetooth.onAdapterStateChange((available) => {
bluetoothEnabled.value = available
if (!available)
deviceList.value = []
})
offConnection = bluetooth.onConnectionStateChange((state) => {
syncConnectedFlag(state.deviceId, state.connected)
})
offFound = bluetooth.onDeviceFound((device) => {
if (scanning.value)
upsertDevice(device)
})
try {
await bluetooth.open()
await refreshAdapterState()
}
catch (err) {
bluetoothEnabled.value = false
console.warn('蓝牙初始化:', err)
}
})
onUnmounted(() => {
offAdapter?.()
offConnection?.()
offFound?.()
bluetooth.stopScan().catch(() => undefined)
})
</script>
<template>
@@ -43,12 +222,17 @@ function handleRefresh() {
<text class="switch-label">
蓝牙开关
</text>
<switch :checked="bluetoothEnabled" color="#4685FD" @change="bluetoothEnabled = !bluetoothEnabled" />
<switch :checked="bluetoothEnabled" color="#4685FD" @change="handleBluetoothToggle" />
</view>
<!-- 搜索框 -->
<view class="search-box">
<wd-search v-model="searchValue" placeholder="搜索蓝牙设备" hide-cancel placeholder-left />
<wd-search
v-model="searchValue"
placeholder="搜索蓝牙设备"
hide-cancel
placeholder-left
/>
</view>
<!-- 设备列表标题 -->
@@ -57,13 +241,22 @@ function handleRefresh() {
设备名称
</text>
<view class="refresh-btn" @tap="handleRefresh">
<wd-icon name="refresh" size="20px" color="#999999" />
<wd-icon
name="refresh"
size="20px"
color="#999999"
:class="{ spinning: scanning }"
/>
</view>
</view>
<view v-if="!filteredList.length && !scanning" class="empty-tip">
{{ bluetoothEnabled ? '点击右上角刷新扫描附近设备' : '请先开启蓝牙' }}
</view>
<!-- 设备列表 -->
<view class="device-list">
<view v-for="(device, index) in deviceList" :key="index" class="device-item">
<view v-for="device in filteredList" :key="device.deviceId" class="device-item">
<view class="device-info">
<text class="device-name">
{{ device.name }}
@@ -76,7 +269,9 @@ function handleRefresh() {
v-if="!device.connected"
size="small"
round
@tap="handleConnect(index)"
:loading="connectingId === device.deviceId"
:disabled="!!connectingId && connectingId !== device.deviceId"
@tap="handleConnect(device)"
>
连接
</wd-button>
@@ -85,7 +280,7 @@ function handleRefresh() {
type="danger"
size="small"
round
@tap="handleDisconnect(index)"
@tap="handleDisconnect(device)"
>
断开
</wd-button>
@@ -150,6 +345,13 @@ function handleRefresh() {
padding: 4px;
}
.empty-tip {
font-size: 14px;
color: #999999;
text-align: center;
padding: 16px 0;
}
.device-list {
display: flex;
flex-direction: column;
@@ -168,6 +370,9 @@ function handleRefresh() {
.device-info {
display: flex;
flex-direction: column;
min-width: 0;
flex: 1;
padding-right: 12px;
}
.device-name {
@@ -180,5 +385,15 @@ function handleRefresh() {
.device-mac {
font-size: 12px;
color: #999999;
word-break: break-all;
}
.spinning {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
</style>