feat 配置蓝牙模块
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user