更新记录
1.0.2(2026-06-25)
下载此版本
新增支持vue2
1.0.1(2026-06-25)
下载此版本
1.0.1(2026-06-24)
- 支持低功耗蓝牙扫描、连接、发送、接收、断开
- 单例模式,跨页面自动保持连接状态
- 提供事件总线(on / off / once)
- 同时导出 Vue 3 组合式 API
useBluetooth
平台兼容性
uni-app(5.0)
| Vue2 |
Vue3 |
Chrome |
Safari |
app-vue |
app-nvue |
Android |
iOS |
鸿蒙 |
| √ |
√ |
× |
× |
√ |
- |
√ |
√ |
- |
| 微信小程序 |
支付宝小程序 |
抖音小程序 |
百度小程序 |
快手小程序 |
京东小程序 |
鸿蒙元服务 |
QQ小程序 |
飞书小程序 |
小红书小程序 |
快应用-华为 |
快应用-联盟 |
| √ |
√ |
- |
- |
- |
- |
- |
- |
- |
- |
- |
- |
uni-app x(5.0)
| Chrome |
Safari |
Android |
iOS |
鸿蒙 |
微信小程序 |
| - |
- |
- |
- |
- |
- |
其他
q-bluetooth
基于 uni-app 官方蓝牙 API 封装的低功耗蓝牙(BLE)插件,单例模式,跨页面保持连接。
特性
- ✅ 扫描附近 BLE 设备 / 实时去重列表
- ✅ 连接设备、自动发现服务与特征
- ✅ 发送数据(ArrayBuffer / hex 字符串)
- ✅ 接收数据(自动开启 notify / indicate)
- ✅ 断开 / 关闭适配器
- ✅ 全局单例 + 事件总线,任意页面 import 拿到的是同一份连接
- ✅ 自带 Vue 3 组合式 API
useBluetooth
- ✅ 全平台:App、微信 / 支付宝 / 百度 / 字节 / QQ 小程序
安装
- 把整个
uni_modules/q-bluetooth 目录放进项目根目录的 uni_modules/ 下,无需在 main.js 引入。
- App 端需在
manifest.json → App模块配置 → 蓝牙 勾选;小程序需在小程序后台配置蓝牙能力。
快速上手
// 任意页面
import qBluetooth from '@/uni_modules/q-bluetooth'
// 1. 初始化蓝牙适配器
await qBluetooth.init()
// 2. 监听扫描结果(增量去重列表)
qBluetooth.on('deviceFound', (devices) => {
console.log('附近设备:', devices)
})
// 3. 开始扫描
await qBluetooth.startScan()
// 4. 连接(自动发现服务、自动开启 notify)
await qBluetooth.connect(deviceId)
// 5. 监听接收的数据
qBluetooth.on('receive', ({ value, hex }) => {
console.log('收到:', hex)
})
// 6. 发送数据(支持 hex 字符串 / ArrayBuffer / Uint8Array)
await qBluetooth.send('A1B2C3D4')
// 7. 断开
await qBluetooth.disconnect()
跨页面使用
由于本插件采用 ES Module 单例 + 系统级蓝牙连接,因此:
- 在 A 页面
connect() 成功后,B 页面 import 进来直接 send() 即可,无需再次 connect。
- 通过
qBluetooth.state 可读取当前是否已连接、当前 deviceId 等信息。
- 事件监听同样跨页面共享,记得在
onUnload 中 off() 掉本页面注册的回调即可。
// B 页面
import qBluetooth from '@/uni_modules/q-bluetooth'
onShow(() => {
if (qBluetooth.state.connected) {
qBluetooth.send('B1B2')
}
})
Vue2 使用方式
Vue2 项目请使用默认导出的 qBluetooth 单例 API。核心蓝牙能力、跨页面连接保持、事件监听都支持 Vue2。
<template>
<view>
<view>状态:{{ connected ? '已连接' : '未连接' }}</view>
<view v-for="d in devices" :key="d.deviceId" @tap="connect(d.deviceId)">
{{ d.name || '(未命名)' }} - 信号强度 {{ d.signalStrength }}
</view>
</view>
</template>
<script>
import qBluetooth from '@/uni_modules/q-bluetooth'
export default {
data() {
return {
connected: false,
devices: [],
}
},
onLoad() {
this.onDeviceFound = (devices) => {
this.devices = devices
}
this.onConnectionChange = () => {
this.connected = qBluetooth.state.connected
}
qBluetooth.on('deviceFound', this.onDeviceFound)
qBluetooth.on('connectionChange', this.onConnectionChange)
},
onUnload() {
qBluetooth.off('deviceFound', this.onDeviceFound)
qBluetooth.off('connectionChange', this.onConnectionChange)
},
methods: {
async scan() {
await qBluetooth.init()
await qBluetooth.startScan()
},
async connect(deviceId) {
await qBluetooth.connect(deviceId)
this.connected = qBluetooth.state.connected
},
async send() {
await qBluetooth.send('A1B2C3D4')
},
},
}
</script>
Vue 3 组合式 API
<script setup>
import { useBluetooth } from '@/uni_modules/q-bluetooth'
const { state, devices, init, startScan, stopScan, connect, send, disconnect } = useBluetooth()
</script>
<template>
<view>状态:{{ state.connected ? '已连接' : '未连接' }}</view>
<view v-for="d in devices" :key="d.deviceId" @tap="connect(d.deviceId)">{{ d.name }}</view>
</template>
API
实例属性
| 属性 |
类型 |
说明 |
state.adapterReady |
boolean |
蓝牙适配器是否就绪 |
state.scanning |
boolean |
是否扫描中 |
state.connected |
boolean |
是否已连接 |
state.deviceId |
string |
当前连接的 deviceId |
state.serviceId |
string |
当前使用的 serviceId |
state.writeCharId |
string |
当前用于写入的特征 UUID |
state.notifyCharId |
string |
当前用于通知的特征 UUID |
state.RSSI |
number | null |
当前连接设备的原始 RSSI,单位 dBm |
state.signalStrength |
number | null |
当前连接设备的信号强度,范围 10 ~ 99 |
方法
| 方法 |
说明 |
init() |
打开蓝牙适配器(重复调用安全) |
startScan(options?) |
开始扫描;options 同 uni.startBluetoothDevicesDiscovery |
stopScan() |
停止扫描 |
getDevices() |
获取当前发现的设备列表 |
connect(deviceId, options?) |
连接设备,自动发现服务并开启 notify |
disconnect() |
断开当前连接 |
send(data, options?) |
发送数据,data 支持 hex 字符串 / ArrayBuffer / Uint8Array / number[] |
refreshSignal() |
刷新当前连接设备的 RSSI / signalStrength;平台不支持实时 RSSI 时返回连接前最后一次扫描值 |
destroy() |
关闭蓝牙适配器,清理所有状态与监听 |
on(event, handler) |
监听事件 |
once(event, handler) |
监听一次 |
off(event, handler?) |
取消监听 |
事件
| 事件 |
回调参数 |
说明 |
adapterStateChange |
{ available, discovering } |
适配器状态变化 |
deviceFound |
devices[] |
发现新设备(已合并去重) |
connectionChange |
{ deviceId, connected } |
连接状态变化 |
signalChange |
{ deviceId, RSSI, signalStrength } |
当前连接设备信号强度变化 |
receive |
{ deviceId, serviceId, characteristicId, value, hex } |
收到通知数据 |
error |
{ scene, errMsg, errCode } |
任意阶段的错误 |
devices[]
devices[] 是扫描到的附近蓝牙设备列表,来源于 deviceFound 事件和 getDevices() 方法。插件会按 deviceId 自动去重,并合并同一设备后续扫描到的新信息。
| 属性 |
类型 |
说明 |
deviceId |
string |
蓝牙设备 ID,连接时传给 connect(deviceId) 的值 |
name |
string |
设备名称;插件会优先使用 name,没有时使用 localName |
localName |
string |
广播包中的本地名称,部分设备只有该字段 |
RSSI |
number |
原始蓝牙信号强度,单位 dBm,通常是负数,例如 -40 强、-90 弱 |
signalStrength |
number |
插件根据 RSSI 换算后的信号强度,范围 10 ~ 99,数值越大信号越强 |
advertisData |
ArrayBuffer |
广播数据,不同平台和设备返回内容不同 |
advertisServiceUUIDs |
string[] |
广播中的服务 UUID 列表 |
serviceData |
object |
广播中的服务数据 |
示例:
qBluetooth.on('deviceFound', (devices) => {
devices.forEach((device) => {
console.log(device.deviceId)
console.log(device.name)
console.log(device.RSSI)
console.log(device.signalStrength)
})
})
注意事项
- Android 扫描需要定位权限,App 端请确保 manifest 勾选
ACCESS_FINE_LOCATION。
- 小程序需用户已开启系统蓝牙、定位。
- 默认会自动挑选一组
notify/indicate 与 write 特征;如需指定,向 connect(deviceId, { serviceId, writeCharId, notifyCharId }) 传入即可。
使用案例
q-bluetooth 演示
适配器:{{ state.adapterReady ? '✅' : '⛔️' }} |
扫描中:{{ state.scanning ? '🔍' : '—' }} |
连接:{{ state.connected ? state.deviceId : '未连接' }} |
信号:{{ state.connected && state.signalStrength ? state.signalStrength : '--' }}
<button size="mini" @tap="onScan">开始扫描</button>
<button size="mini" @tap="onStopScan">停止扫描</button>
<button size="mini" @tap="onRefreshSignal" :disabled="!state.connected">刷新信号</button>
<button size="mini" @tap="disconnect" :disabled="!state.connected">断开</button>
<button size="mini" @tap="goB" :disabled="!state.connected">B页面</button>
<input class="input" placeholder="输入要发送的 hex,例如 A1B2C3" v-model="hex" />
<button size="mini" @tap="onSend" :disabled="!state.connected">发送</button>
{{ l }}
{{ d.name || '(未命名)' }}
{{ d.deviceId }} 信号强度 {{ d.signalStrength }}
import { ref } from 'vue'
import { useBluetooth } from '@/uni_modules/q-bluetooth'
const { state, devices, init, startScan, stopScan, connect, send, disconnect, refreshSignal, on } = useBluetooth()
const hex = ref('A1B2C3D4')
const logs = ref([])
const log = (s) => { logs.value.unshift(`[${new Date().toLocaleTimeString()}] ${s}`); if (logs.value.length > 30) logs.value.pop() }
on('receive', ({ hex }) => log('📥 收到 ' + hex))
on('error', (e) => log('❌ ' + e.scene + ': ' + e.errMsg))
async function onScan() {
try {
await init()
await startScan()
log('开始扫描')
} catch (e) { log('扫描失败:' + (e.errMsg || e.message)) }
}
async function onStopScan() {
try {
await stopScan()
log('停止扫描')
} catch (e) { log('停止扫描失败:' + (e.errMsg || e.message)) }
}
async function onConnect(d) {
try {
log('连接中:' + d.deviceId)
await connect(d.deviceId)
log('✅ 连接成功')
} catch (e) { log('连接失败:' + (e.errMsg || e.message)) }
}
async function onSend() {
try {
const res = await send(hex.value)
log('📤 已发送 ' + res.bytes + 'B / ' + res.chunks + '包')
} catch (e) { log('发送失败:' + (e.errMsg || e.message)) }
}
async function onRefreshSignal() {
try {
const res = await refreshSignal()
log('当前信号强度:' + (res.signalStrength || '--'))
} catch (e) { log('刷新信号失败:' + (e.errMsg || e.message)) }
}
function goB() {
uni.navigateTo({ url: '/pages/b/b' })
}
<style>
.page { padding: 20rpx; }
.title { font-size: 36rpx; font-weight: bold; margin-bottom: 16rpx; }
.status { font-size: 24rpx; color: #555; margin-bottom: 16rpx; }
.row { display: flex; gap: 12rpx; margin-bottom: 16rpx; flex-wrap: wrap; }
.input { flex: 1; border: 1rpx solid #ddd; padding: 8rpx 16rpx; border-radius: 8rpx; }
.logs { background: #111; color: #0f0; font-size: 22rpx; padding: 16rpx; border-radius: 8rpx; min-height: 300rpx; margin-bottom: 16rpx; }
.log { line-height: 1.5; }
.device { padding: 16rpx; border-bottom: 1rpx solid #eee; }
.name { font-size: 28rpx; }
.sub { font-size: 22rpx; color: #888; }
</style>