更新记录
1.0.1(2025-12-18)
文档更新,给了完整文档地址,可丢给AI一键生成代码
1.0.0(2025-12-18)
V1.0.0
平台兼容性
uni-app(4.0)
| Vue2 | Vue3 | Chrome | Safari | app-vue | app-nvue | Android | iOS | 鸿蒙 |
|---|---|---|---|---|---|---|---|---|
| × | × | - | - | × | × | 5.0 | × | 2.0.0 |
| 微信小程序 | 支付宝小程序 | 抖音小程序 | 百度小程序 | 快手小程序 | 京东小程序 | 鸿蒙元服务 | QQ小程序 | 飞书小程序 | 快应用-华为 | 快应用-联盟 |
|---|---|---|---|---|---|---|---|---|---|---|
| × | × | - | - | - | - | - | - | - | - | - |
uni-app x(4.0)
| Chrome | Safari | Android | iOS | 鸿蒙 | 微信小程序 |
|---|---|---|---|---|---|
| - | - | 5.0 | × | 2.0.0 | - |
gp-bluetooth
https://ext.dcloud.net.cn/plugin?id=26236
简介
gp-bluetooth 是一个基于 UTS (Unified TypeScript) 开发的经典蓝牙 (Classic Bluetooth / SPP) 插件,专为 uni-app 和 uni-app x 项目设计。该插件提供了完整的经典蓝牙功能支持,包括设备搜索、配对、连接、数据收发等功能,特别适用于需要通过 SPP (Serial Port Profile) 协议进行通信的设备。
注意事项
- 本插件现有功能优先满足自用,如有新需求会酌情新增
- 仅支持安卓或设备!因个人设备有限,仅测试过 红米 K40s、一台鸿蒙 3.0.0 的荣耀手机、一台鸿蒙 2.0.0 的华为平板
- 插件支持试用!插件支持试用!插件支持试用!试用后再决定是否进行购买
- 插件使用(真机调试)前务必确认先下载插件后,打一个自定义安卓基座后再真机调试(UTS 插件需要先打基座包)
- 当前文档的完整文件内容放在了 Github https://github.com/uni-ext/gp-bluetooth-doc,如果您使用 Cursor 或 Trae 等 AI 工具进行开发, 可以将当前文档当做上下文内容。
特性
- 纯 UTS 实现:完全使用 UTS 语言编写,无需额外的原生开发环境
- 标准 SPP 协议:支持经典蓝牙 SPP (Serial Port Profile) 串口通信协议
- 完整的蓝牙管理:设备搜索、配对管理、连接管理、数据收发
- 事件驱动:提供丰富的事件回调,支持实时监听设备状态和数据
- TypeScript 支持:完整的类型定义,提供良好的开发体验
- 兼容性强:支持 Android 5.0 (API 21) 及以上版本
平台支持
| 平台 | 支持状态 |
|---|---|
| Android | ✅ 支持 |
| iOS | ❌ 不支持 |
| 鸿蒙 | ❌ 不支持 |
| H5 | ❌ 不支持 |
| 小程序 | ❌ 不支持 |
注意:iOS 平台不支持经典蓝牙 SPP 协议,如需 iOS 支持,请使用 BLE (低功耗蓝牙) 方案。
权限配置
插件已在 AndroidManifest.xml 中声明所需权限,但在 Android 6.0+ 设备上,您需要在运行时动态请求位置权限,参考代码
function requestPermissions() {
const permissions = [
"android.permission.ACCESS_FINE_LOCATION",
"android.permission.ACCESS_COARSE_LOCATION",
"android.permission.BLUETOOTH",
"android.permission.BLUETOOTH_ADMIN",
"android.permission.BLUETOOTH_SCAN",
"android.permission.BLUETOOTH_CONNECT",
];
plus.android.requestPermissions(
permissions,
(result) => {
console.log("权限请求结果:", result);
this.checkBluetoothState();
},
(err) => {
console.log("权限请求失败:", err);
this.toast("需要位置权限才能搜索蓝牙设备");
this.checkBluetoothState();
}
);
this.checkBluetoothState();
}
权限列表
| 权限 | 说明 |
|---|---|
BLUETOOTH |
基础蓝牙权限 |
BLUETOOTH_ADMIN |
蓝牙管理权限(搜索、配对) |
BLUETOOTH_SCAN |
Android 12+ 蓝牙扫描权限 |
BLUETOOTH_CONNECT |
Android 12+ 蓝牙连接权限 |
ACCESS_FINE_LOCATION |
精确位置权限(蓝牙扫描需要) |
ACCESS_COARSE_LOCATION |
粗略位置权限 |
API 文档
蓝牙适配器
openBluetoothAdapter(options)
初始化蓝牙适配器,在使用其他蓝牙 API 前必须先调用此方法。
参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| success | Function | 否 | 成功回调 |
| fail | Function | 否 | 失败回调 |
| complete | Function | 否 | 完成回调 |
示例
import { openBluetoothAdapter } from '@/uni_modules/gp-bluetooth';
openBluetoothAdapter({
success: res => {
console.log('蓝牙适配器初始化成功');
},
fail: err => {
console.error('初始化失败:', err.errMsg);
},
});
closeBluetoothAdapter(options)
关闭蓝牙适配器,释放资源。
示例
import { closeBluetoothAdapter } from '@/uni_modules/gp-bluetooth';
closeBluetoothAdapter({
success: () => {
console.log('蓝牙适配器已关闭');
},
});
getBluetoothAdapterState(options)
获取蓝牙适配器状态。
返回参数
| 参数 | 类型 | 说明 |
|---|---|---|
| available | boolean | 蓝牙适配器是否可用 |
| enabled | boolean | 蓝牙是否已开启 |
| discovering | boolean | 是否正在搜索设备 |
示例
import { getBluetoothAdapterState } from '@/uni_modules/gp-bluetooth';
getBluetoothAdapterState({
success: res => {
console.log('蓝牙是否可用:', res.available);
console.log('蓝牙是否开启:', res.enabled);
console.log('是否正在搜索:', res.discovering);
},
});
设备搜索
startDiscovery(options)
开始搜索附近的蓝牙设备。
参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| timeout | number | 否 | 搜索超时时间(毫秒),默认 12000ms |
| success | Function | 否 | 成功回调 |
| fail | Function | 否 | 失败回调 |
| complete | Function | 否 | 完成回调 |
示例
import { startDiscovery } from '@/uni_modules/gp-bluetooth';
startDiscovery({
timeout: 15000,
success: () => {
console.log('开始搜索设备');
},
fail: err => {
console.error('搜索启动失败:', err.errMsg);
},
});
stopDiscovery(options)
停止搜索蓝牙设备。
示例
import { stopDiscovery } from '@/uni_modules/gp-bluetooth';
stopDiscovery({
success: () => {
console.log('已停止搜索');
},
});
getBondedDevices(options)
获取已配对的蓝牙设备列表。
返回参数
| 参数 | 类型 | 说明 |
|---|---|---|
| devices | Array | 已配对设备列表 |
设备对象属性
| 属性 | 类型 | 说明 |
|---|---|---|
| deviceId | string | 设备 MAC 地址 |
| name | string | 设备名称 |
| bondState | number | 配对状态 |
示例
import { getBondedDevices } from '@/uni_modules/gp-bluetooth';
getBondedDevices({
success: res => {
console.log('已配对设备:', res.devices);
res.devices.forEach(device => {
console.log(`设备: ${device.name} (${device.deviceId})`);
});
},
});
设备配对
createBond(options)
配对蓝牙设备。
参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| deviceId | string | 是 | 设备 MAC 地址 |
| success | Function | 否 | 成功回调 |
| fail | Function | 否 | 失败回调 |
| complete | Function | 否 | 完成回调 |
示例
import { createBond } from '@/uni_modules/gp-bluetooth';
createBond({
deviceId: 'AA:BB:CC:DD:EE:FF',
success: () => {
console.log('配对请求已发送');
},
fail: err => {
console.error('配对失败:', err.errMsg);
},
});
removeBond(options)
取消设备配对。
参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| deviceId | string | 是 | 设备 MAC 地址 |
| success | Function | 否 | 成功回调 |
| fail | Function | 否 | 失败回调 |
| complete | Function | 否 | 完成回调 |
示例
import { removeBond } from '@/uni_modules/gp-bluetooth';
removeBond({
deviceId: 'AA:BB:CC:DD:EE:FF',
success: () => {
console.log('已取消配对');
},
});
设备连接
connectDevice(options)
连接蓝牙设备。
参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| deviceId | string | 是 | 设备 MAC 地址 |
| timeout | number | 否 | 连接超时时间(毫秒),默认 10000ms |
| secure | boolean | 否 | 是否安全连接,默认 false |
| success | Function | 否 | 成功回调 |
| fail | Function | 否 | 失败回调 |
| complete | Function | 否 | 完成回调 |
示例
import { connectDevice } from '@/uni_modules/gp-bluetooth';
connectDevice({
deviceId: 'AA:BB:CC:DD:EE:FF',
timeout: 15000,
secure: false,
success: () => {
console.log('连接请求已发送');
},
fail: err => {
console.error('连接失败:', err.errMsg);
},
});
disconnectDevice(options)
断开蓝牙连接。
参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| deviceId | string | 否 | 设备 MAC 地址,可选 |
| success | Function | 否 | 成功回调 |
| fail | Function | 否 | 失败回调 |
| complete | Function | 否 | 完成回调 |
示例
import { disconnectDevice } from '@/uni_modules/gp-bluetooth';
disconnectDevice({
success: () => {
console.log('已断开连接');
},
});
getConnectionState(options)
获取当前连接状态。
返回参数
| 参数 | 类型 | 说明 |
|---|---|---|
| connected | boolean | 是否已连接 |
| deviceId | string | 当前连接的设备 MAC 地址 |
示例
import { getConnectionState } from '@/uni_modules/gp-bluetooth';
getConnectionState({
success: res => {
console.log('连接状态:', res.connected);
console.log('连接设备:', res.deviceId);
},
});
数据收发
sendData(options)
发送字节数组数据。
参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| data | number[] | 是 | 要发送的字节数组 |
| success | Function | 否 | 成功回调 |
| fail | Function | 否 | 失败回调 |
| complete | Function | 否 | 完成回调 |
示例
import { sendData } from '@/uni_modules/gp-bluetooth';
// 发送 NMEA 指令示例
const data = [0x24, 0x47, 0x50, 0x47, 0x47, 0x41]; // $GPGGA
sendData({
data: data,
success: () => {
console.log('数据发送成功');
},
fail: err => {
console.error('发送失败:', err.errMsg);
},
});
sendString(options)
发送字符串数据。
参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| data | string | 是 | 要发送的字符串 |
| encoding | string | 否 | 字符编码,默认 UTF-8 |
| success | Function | 否 | 成功回调 |
| fail | Function | 否 | 失败回调 |
| complete | Function | 否 | 完成回调 |
示例
import { sendString } from '@/uni_modules/gp-bluetooth';
sendString({
data: 'Hello World\r\n',
encoding: 'UTF-8',
success: () => {
console.log('字符串发送成功');
},
});
事件监听
onDeviceFound(callback)
监听发现新设备事件。
回调参数
| 参数 | 类型 | 说明 |
|---|---|---|
| device | Object | 发现的设备信息 |
| device.deviceId | string | 设备 MAC 地址 |
| device.name | string | 设备名称 |
| device.rssi | number | 信号强度 |
| device.bondState | number | 配对状态 |
示例
import { onDeviceFound, offDeviceFound } from '@/uni_modules/gp-bluetooth';
onDeviceFound(res => {
console.log('发现设备:', res.device.name, res.device.deviceId);
});
// 取消监听
offDeviceFound();
onDiscoveryFinished(callback)
监听搜索完成事件。
示例
import { onDiscoveryFinished, offDiscoveryFinished } from '@/uni_modules/gp-bluetooth';
onDiscoveryFinished(() => {
console.log('搜索完成');
});
// 取消监听
offDiscoveryFinished();
onBondStateChange(callback)
监听配对状态变化事件。
回调参数
| 参数 | 类型 | 说明 |
|---|---|---|
| deviceId | string | 设备 MAC 地址 |
| bondState | number | 配对状态 (10-未配对, 11-配对中, 12-已配对) |
示例
import { onBondStateChange, offBondStateChange } from '@/uni_modules/gp-bluetooth';
onBondStateChange(res => {
console.log('设备:', res.deviceId);
switch (res.bondState) {
case 10:
console.log('未配对');
break;
case 11:
console.log('配对中...');
break;
case 12:
console.log('已配对');
break;
}
});
// 取消监听
offBondStateChange();
onConnectionStateChange(callback)
监听连接状态变化事件。
回调参数
| 参数 | 类型 | 说明 |
|---|---|---|
| deviceId | string | 设备 MAC 地址 |
| connected | boolean | 是否已连接 |
示例
import { onConnectionStateChange, offConnectionStateChange } from '@/uni_modules/gp-bluetooth';
onConnectionStateChange(res => {
console.log('设备:', res.deviceId);
console.log('连接状态:', res.connected ? '已连接' : '已断开');
});
// 取消监听
offConnectionStateChange();
onDataReceived(callback)
监听接收数据事件。
回调参数
| 参数 | 类型 | 说明 |
|---|---|---|
| deviceId | string | 设备 MAC 地址 |
| data | number[] | 接收到的字节数组 |
| text | string | 转换为字符串的数据 |
示例
import { onDataReceived, offDataReceived } from '@/uni_modules/gp-bluetooth';
onDataReceived(res => {
console.log('收到数据:', res.text);
console.log('原始字节:', res.data);
// 处理 NMEA 数据
if (res.text.startsWith('$')) {
// 解析 NMEA 语句
parseNMEA(res.text);
}
});
// 取消监听
offDataReceived();
onBluetoothAdapterStateChange(callback)
监听蓝牙适配器状态变化事件。
回调参数
| 参数 | 类型 | 说明 |
|---|---|---|
| available | boolean | 蓝牙适配器是否可用 |
| discovering | boolean | 是否正在搜索 |
示例
import { onBluetoothAdapterStateChange, offBluetoothAdapterStateChange } from '@/uni_modules/gp-bluetooth';
onBluetoothAdapterStateChange(res => {
console.log('蓝牙可用:', res.available);
console.log('正在搜索:', res.discovering);
});
// 取消监听
offBluetoothAdapterStateChange();
错误码
| 错误码 | 说明 |
|---|---|
| 0 | 操作成功 |
| 10000 | 蓝牙适配器未初始化 |
| 10001 | 蓝牙适配器不可用 |
| 10002 | 没有找到指定设备 |
| 10003 | 连接失败 |
| 10006 | 设备未连接 |
| 10008 | 系统错误 |
| 10012 | 连接超时 |
| 10013 | 无效参数 |
| 10014 | 正在连接中 |
| 10015 | 设备已连接 |
| 10016 | 配对失败 |
| 10017 | 发送数据失败 |
完整使用示例
setup ts 语法
<template>
<view class="container">
<!-- 顶部状态栏 -->
<view class="header">
<view class="header-content">
<view class="header-left">
<view class="bluetooth-icon" :class="{ active: bluetoothEnabled }">
<image
class="bt-icon-img"
:src="bluetoothEnabled ? '/static/icons/bluetooth-green.svg' : '/static/icons/bluetooth-white.svg'"
mode="aspectFit"
/>
</view>
<view class="header-info">
<text class="header-title">经典蓝牙管理</text>
<text class="header-subtitle">{{ statusText }}</text>
</view>
</view>
<view class="header-right">
<switch
:checked="adapterOpened"
@change="toggleAdapter"
color="#00b894"
/>
</view>
</view>
</view>
<!-- 蓝牙状态卡片 -->
<view class="status-card" v-if="adapterOpened">
<view class="status-row">
<view class="status-item">
<text class="status-label">适配器状态</text>
<text class="status-value" :class="{ success: bluetoothEnabled }">
{{ bluetoothEnabled ? "已开启" : "已关闭" }}
</text>
</view>
<view class="status-item">
<text class="status-label">搜索状态</text>
<text class="status-value" :class="{ active: isDiscovering }">
{{ isDiscovering ? "搜索中..." : "空闲" }}
</text>
</view>
<view class="status-item">
<text class="status-label">连接状态</text>
<text class="status-value" :class="{ success: isConnected }">
{{ isConnected ? "已连接" : "未连接" }}
</text>
</view>
</view>
</view>
<!-- 主要内容区域 -->
<scroll-view class="main-content" scroll-y v-if="adapterOpened">
<!-- 已连接设备 -->
<view class="section" v-if="isConnected">
<view class="section-header">
<text class="section-title">已连接设备</text>
<view class="action-btn disconnect-btn" @tap="handleDisconnect">
<text class="btn-text">断开连接</text>
</view>
</view>
<view class="device-card connected">
<view class="device-icon connected">
<image class="bt-icon-img" src="/static/icons/bluetooth-white.svg" mode="aspectFit" />
<view class="online-dot"></view>
</view>
<view class="device-info">
<text class="device-name">{{
connectedDevice.name || "未知设备"
}}</text>
<text class="device-id">{{ connectedDevice.deviceId }}</text>
</view>
<view class="device-status connected">
<text class="status-dot"></text>
<text class="status-text">已连接</text>
</view>
</view>
</view>
<!-- 数据收发区域 -->
<view class="section" v-if="isConnected">
<view class="section-header">
<text class="section-title">数据通信</text>
<view class="action-btn clear-btn" @tap="clearReceivedData">
<text class="btn-text">清空</text>
</view>
</view>
<!-- 接收数据显示 -->
<view class="data-card">
<text class="data-label">接收数据</text>
<scroll-view class="data-content receive" scroll-y>
<text class="data-text" v-if="receivedData">{{
receivedData
}}</text>
<text class="data-placeholder" v-else>等待接收数据...</text>
</scroll-view>
</view>
<!-- 发送数据 -->
<view class="send-area">
<view class="input-wrapper">
<input
class="send-input"
v-model="sendText"
placeholder="输入要发送的数据"
placeholder-class="input-placeholder"
/>
</view>
<view
class="send-btn"
@tap="handleSendData"
:class="{ disabled: !sendText }"
>
<text class="send-btn-text">发送</text>
</view>
</view>
</view>
<!-- 已配对设备 -->
<view class="section">
<view class="section-header">
<text class="section-title">已配对设备</text>
<view class="action-btn" @tap="loadBondedDevices">
<text class="btn-text">刷新</text>
</view>
</view>
<view v-if="bondedDevices.length === 0" class="empty-state">
<text class="empty-text">暂无已配对设备</text>
</view>
<view v-else class="device-list">
<view
class="device-card"
v-for="device in bondedDevices"
:key="device.deviceId"
@tap="handleDeviceTap(device, 'bonded')"
>
<view class="device-icon bonded">
<image class="bt-icon-img" src="/static/icons/bluetooth-white.svg" mode="aspectFit" />
</view>
<view class="device-info">
<text class="device-name">{{ device.name || "未知设备" }}</text>
<text class="device-id">{{ device.deviceId }}</text>
</view>
<view class="device-actions">
<view
class="action-icon connect"
@tap.stop="handleConnect(device)"
>
<text class="action-text">连接</text>
</view>
<view
class="action-icon remove"
@tap.stop="handleRemoveBond(device)"
>
<text class="action-text">取消</text>
</view>
</view>
</view>
</view>
</view>
<!-- 搜索设备 -->
<view class="section">
<view class="section-header">
<text class="section-title">搜索设备</text>
<view
class="action-btn primary"
@tap="toggleDiscovery"
:class="{ discovering: isDiscovering }"
>
<text class="btn-text">{{
isDiscovering ? "停止搜索" : "开始搜索"
}}</text>
</view>
</view>
<!-- 搜索进度 -->
<view class="search-progress" v-if="isDiscovering">
<view class="progress-bar">
<view class="progress-inner"></view>
</view>
<text class="progress-text">正在搜索附近的蓝牙设备...</text>
</view>
<view
v-if="discoveredDevices.length === 0 && !isDiscovering"
class="empty-state"
>
<text class="empty-text">点击"开始搜索"查找附近设备</text>
</view>
<view v-else class="device-list">
<view
class="device-card"
v-for="device in discoveredDevices"
:key="device.deviceId"
@tap="handleDeviceTap(device, 'discovered')"
>
<view class="device-icon">
<image class="bt-icon-img" src="/static/icons/bluetooth-gray.svg" mode="aspectFit" />
</view>
<view class="device-info">
<text class="device-name">{{ device.name || "未知设备" }}</text>
<text class="device-id">{{ device.deviceId }}</text>
<view class="device-meta">
<text class="meta-item rssi">{{ device.rssi }}dBm</text>
</view>
</view>
<view class="device-actions">
<view
class="action-icon pair"
@tap.stop="handleCreateBond(device)"
>
<text class="action-text">配对</text>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
<!-- 未初始化提示 -->
<view class="init-prompt" v-else>
<view class="init-icon">
<image class="bt-icon-img large" src="/static/icons/bluetooth-white.svg" mode="aspectFit" />
</view>
<text class="init-title">蓝牙未开启</text>
<text class="init-desc">请打开蓝牙适配器以使用蓝牙功能</text>
<view class="init-btn" @tap="initBluetooth">
<text class="init-btn-text">开启蓝牙</text>
</view>
</view>
<!-- Toast 提示 -->
<view class="toast" :class="{ show: showToast }">
<text class="toast-text">{{ toastMessage }}</text>
</view>
<!-- Loading 遮罩 -->
<view class="loading-mask" v-if="isLoading">
<view class="loading-content">
<view class="loading-spinner"></view>
<text class="loading-text">{{ loadingText }}</text>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from "vue";
import {
openBluetoothAdapter,
closeBluetoothAdapter,
getBluetoothAdapterState,
startDiscovery,
stopDiscovery,
getBondedDevices,
createBond,
removeBond,
connectDevice,
disconnectDevice,
sendString,
getConnectionState,
onDeviceFound,
offDeviceFound,
onDiscoveryFinished,
offDiscoveryFinished,
onBondStateChange,
offBondStateChange,
onConnectionStateChange,
offConnectionStateChange,
onDataReceived,
offDataReceived,
onBluetoothAdapterStateChange,
offBluetoothAdapterStateChange,
} from "@/uni_modules/gp-bluetooth";
// ==================== 类型定义 ====================
interface BluetoothDevice {
deviceId: string;
name: string;
rssi?: number;
bondState?: number;
}
interface ConnectedDevice {
deviceId: string;
name: string;
}
// ==================== 响应式状态 ====================
// 蓝牙状态
const adapterOpened = ref<boolean>(false);
const bluetoothEnabled = ref<boolean>(false);
const isDiscovering = ref<boolean>(false);
const isConnected = ref<boolean>(false);
// 设备列表
const bondedDevices = ref<BluetoothDevice[]>([]);
const discoveredDevices = ref<BluetoothDevice[]>([]);
const connectedDevice = ref<ConnectedDevice>({
deviceId: "",
name: "",
});
// 数据收发
const sendText = ref<string>("");
const receivedData = ref<string>("");
// UI状态
const showToast = ref<boolean>(false);
const toastMessage = ref<string>("");
const isLoading = ref<boolean>(false);
const loadingText = ref<string>("");
// ==================== 计算属性 ====================
const statusText = computed<string>(() => {
if (!adapterOpened.value) return "蓝牙适配器未开启";
if (isConnected.value)
return `已连接: ${connectedDevice.value.name || connectedDevice.value.deviceId}`;
if (isDiscovering.value) return "正在搜索设备...";
return "就绪";
});
// ==================== 生命周期 ====================
onMounted(() => {
requestPermissions();
});
onUnmounted(() => {
cleanup();
});
// ==================== 权限请求 ====================
const requestPermissions = (): void => {
// #ifdef APP-PLUS
const permissions: string[] = [
"android.permission.ACCESS_FINE_LOCATION",
"android.permission.ACCESS_COARSE_LOCATION",
"android.permission.BLUETOOTH",
"android.permission.BLUETOOTH_ADMIN",
"android.permission.BLUETOOTH_SCAN",
"android.permission.BLUETOOTH_CONNECT",
];
plus.android.requestPermissions(
permissions,
(result: any) => {
console.log("权限请求结果:", result);
checkBluetoothState();
},
(err: any) => {
console.log("权限请求失败:", err);
toast("需要位置权限才能搜索蓝牙设备");
checkBluetoothState();
}
);
// #endif
// #ifndef APP-PLUS
checkBluetoothState();
// #endif
};
// ==================== 蓝牙适配器管理 ====================
const checkBluetoothState = (): void => {
getBluetoothAdapterState({
success: (res: any) => {
bluetoothEnabled.value = res.enabled;
isDiscovering.value = res.discovering;
if (res.available && res.enabled) {
adapterOpened.value = true;
setupListeners();
loadBondedDevices();
checkConnectionState();
}
},
fail: (err: any) => {
console.log("获取蓝牙状态失败:", err);
},
});
};
const initBluetooth = (): void => {
showLoading("正在初始化蓝牙...");
openBluetoothAdapter({
success: () => {
adapterOpened.value = true;
bluetoothEnabled.value = true;
setupListeners();
loadBondedDevices();
toast("蓝牙已开启");
},
fail: (err: any) => {
toast("蓝牙开启失败: " + err.errMsg);
},
complete: () => {
hideLoading();
},
});
};
const toggleAdapter = (e: any): void => {
if (e.detail.value) {
initBluetooth();
} else {
closeBluetooth();
}
};
const closeBluetooth = (): void => {
showLoading("正在关闭蓝牙...");
closeBluetoothAdapter({
success: () => {
adapterOpened.value = false;
bluetoothEnabled.value = false;
isDiscovering.value = false;
isConnected.value = false;
bondedDevices.value = [];
discoveredDevices.value = [];
cleanup();
toast("蓝牙已关闭");
},
fail: (err: any) => {
toast("关闭蓝牙失败: " + err.errMsg);
},
complete: () => {
hideLoading();
},
});
};
// ==================== 事件监听 ====================
const setupListeners = (): void => {
// 监听设备发现
onDeviceFound((res: any) => {
const device: BluetoothDevice = res.device;
// 忽略 name 为空、Unknown 的设备
if (!device.name || device.name === "Unknown" || device.name.toLowerCase() === "unknown") {
console.log("忽略未知设备:", device.deviceId);
return;
}
// 忽略已配对的设备 (bondState === 12)
if (device.bondState === 12) {
console.log("忽略已配对设备:", device.name);
return;
}
// 检查是否已在已配对列表中
const isBonded = bondedDevices.value.some((d) => d.deviceId === device.deviceId);
if (isBonded) {
console.log("忽略已在配对列表的设备:", device.name);
return;
}
console.log("发现新设备:", device.name, device.deviceId);
const index = discoveredDevices.value.findIndex((d) => d.deviceId === device.deviceId);
if (index === -1) {
discoveredDevices.value.push(device);
} else {
discoveredDevices.value[index] = device;
}
});
// 监听搜索完成
onDiscoveryFinished(() => {
isDiscovering.value = false;
toast("搜索完成");
});
// 监听配对状态变化
onBondStateChange((res: any) => {
console.log("配对状态变化:", res);
if (res.bondState === 12) {
toast("配对成功,正在连接...");
loadBondedDevices();
// 配对成功后自动连接
setTimeout(() => {
const device = discoveredDevices.value.find((d) => d.deviceId === res.deviceId);
connectToDevice({
deviceId: res.deviceId,
name: device?.name || "",
});
}, 500);
} else if (res.bondState === 10) {
toast("已取消配对");
loadBondedDevices();
}
// 更新发现设备列表中的配对状态
const index = discoveredDevices.value.findIndex((d) => d.deviceId === res.deviceId);
if (index !== -1) {
discoveredDevices.value[index].bondState = res.bondState;
}
});
// 监听连接状态变化
onConnectionStateChange((res: any) => {
console.log("连接状态变化:", res);
isConnected.value = res.connected;
if (res.connected) {
connectedDevice.value.deviceId = res.deviceId;
// 查找设备名称
const bonded = bondedDevices.value.find((d) => d.deviceId === res.deviceId);
const discovered = discoveredDevices.value.find((d) => d.deviceId === res.deviceId);
connectedDevice.value.name = bonded?.name || discovered?.name || "";
toast("连接成功");
} else {
connectedDevice.value = { deviceId: "", name: "" };
toast("连接已断开");
}
hideLoading();
});
// 监听数据接收
onDataReceived((res: any) => {
console.log("收到数据:", res);
const timestamp = new Date().toLocaleTimeString();
receivedData.value += `[${timestamp}] ${res.text}\n`;
});
// 监听蓝牙适配器状态变化
onBluetoothAdapterStateChange((res: any) => {
console.log("适配器状态变化:", res);
bluetoothEnabled.value = res.available;
isDiscovering.value = res.discovering;
});
};
const cleanup = (): void => {
offDeviceFound();
offDiscoveryFinished();
offBondStateChange();
offConnectionStateChange();
offDataReceived();
offBluetoothAdapterStateChange();
};
// ==================== 设备搜索 ====================
const toggleDiscovery = (): void => {
if (isDiscovering.value) {
stopSearch();
} else {
startSearch();
}
};
const startSearch = (): void => {
discoveredDevices.value = [];
startDiscovery({
timeout: 15000,
success: () => {
isDiscovering.value = true;
toast("开始搜索设备");
},
fail: (err: any) => {
toast("搜索失败: " + err.errMsg);
},
});
};
const stopSearch = (): void => {
stopDiscovery({
success: () => {
isDiscovering.value = false;
toast("已停止搜索");
},
fail: (err: any) => {
toast("停止搜索失败: " + err.errMsg);
},
});
};
// ==================== 设备管理 ====================
const loadBondedDevices = (): void => {
getBondedDevices({
success: (res: any) => {
bondedDevices.value = res.devices;
},
fail: (err: any) => {
console.log("获取已配对设备失败:", err);
},
});
};
const checkConnectionState = (): void => {
getConnectionState({
success: (res: any) => {
isConnected.value = res.connected;
if (res.connected) {
connectedDevice.value.deviceId = res.deviceId;
const bonded = bondedDevices.value.find((d) => d.deviceId === res.deviceId);
connectedDevice.value.name = bonded?.name || "";
}
},
});
};
const handleDeviceTap = (device: BluetoothDevice, type: string): void => {
console.log("设备点击:", device, type);
};
const handleCreateBond = (device: BluetoothDevice): void => {
showLoading("正在配对...");
createBond({
deviceId: device.deviceId,
success: () => {
console.log("发起配对请求成功");
},
fail: (err: any) => {
toast("配对失败: " + err.errMsg);
hideLoading();
},
});
// 配对结果由 onBondStateChange 回调处理
setTimeout(() => hideLoading(), 3000);
};
const handleRemoveBond = (device: BluetoothDevice): void => {
uni.showModal({
title: "取消配对",
content: `确定要取消与 "${device.name || device.deviceId}" 的配对吗?`,
success: (res: UniApp.ShowModalRes) => {
if (res.confirm) {
showLoading("正在取消配对...");
removeBond({
deviceId: device.deviceId,
success: () => {
loadBondedDevices();
toast("已取消配对");
},
fail: (err: any) => {
toast("取消配对失败: " + err.errMsg);
},
complete: () => {
hideLoading();
},
});
}
},
});
};
const handleConnect = (device: BluetoothDevice): void => {
if (isConnected.value) {
uni.showModal({
title: "提示",
content: "当前已有设备连接,是否断开并连接新设备?",
success: (res: UniApp.ShowModalRes) => {
if (res.confirm) {
disconnectAndConnect(device);
}
},
});
} else {
connectToDevice(device);
}
};
const disconnectAndConnect = (device: BluetoothDevice): void => {
showLoading("正在切换连接...");
disconnectDevice({
success: () => {
setTimeout(() => {
connectToDevice(device);
}, 500);
},
fail: () => {
toast("断开连接失败");
hideLoading();
},
});
};
const connectToDevice = (device: BluetoothDevice | ConnectedDevice): void => {
showLoading("正在连接...");
connectDevice({
deviceId: device.deviceId,
timeout: 15000,
secure: false,
success: () => {
console.log("连接请求已发送");
},
fail: (err: any) => {
toast("连接失败: " + err.errMsg);
hideLoading();
},
});
};
const handleDisconnect = (): void => {
uni.showModal({
title: "断开连接",
content: "确定要断开当前连接吗?",
success: (res: UniApp.ShowModalRes) => {
if (res.confirm) {
showLoading("正在断开连接...");
disconnectDevice({
success: () => {
isConnected.value = false;
connectedDevice.value = { deviceId: "", name: "" };
toast("已断开连接");
},
fail: (err: any) => {
toast("断开连接失败: " + err.errMsg);
},
complete: () => {
hideLoading();
},
});
}
},
});
};
// ==================== 数据收发 ====================
const handleSendData = (): void => {
if (!sendText.value.trim()) {
toast("请输入要发送的数据");
return;
}
sendString({
data: sendText.value,
encoding: "UTF-8",
success: () => {
const timestamp = new Date().toLocaleTimeString();
receivedData.value += `[${timestamp}] 发送: ${sendText.value}\n`;
sendText.value = "";
toast("发送成功");
},
fail: (err: any) => {
toast("发送失败: " + err.errMsg);
},
});
};
const clearReceivedData = (): void => {
receivedData.value = "";
toast("已清空");
};
// ==================== 工具方法 ====================
const toast = (message: string): void => {
toastMessage.value = message;
showToast.value = true;
setTimeout(() => {
showToast.value = false;
}, 2000);
};
const showLoading = (text: string): void => {
loadingText.value = text;
isLoading.value = true;
};
const hideLoading = (): void => {
isLoading.value = false;
};
</script>
<style>
/* 全局变量 */
page {
background-color: #f5f7fa;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, sans-serif;
}
.container {
min-height: 100vh;
background: linear-gradient(180deg, #00b894 0%, #00cec9 100%);
padding-bottom: env(safe-area-inset-bottom);
overflow-x: hidden;
}
/* 顶部头部 */
.header {
padding: 40rpx 24rpx 30rpx;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.header-left {
display: flex;
align-items: center;
flex: 1;
overflow: hidden;
}
.bluetooth-icon {
width: 64rpx;
height: 64rpx;
background: rgba(255, 255, 255, 0.2);
border-radius: 18rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16rpx;
flex-shrink: 0;
}
.bluetooth-icon.active {
background: rgba(255, 255, 255, 0.95);
}
/* CSS 蓝牙图标 */
.bt-icon-img {
width: 32rpx;
height: 32rpx;
}
.bt-icon-img.large {
width: 56rpx;
height: 56rpx;
}
/* 在线状态小圆点 */
.online-dot {
position: absolute;
right: 4rpx;
top: 4rpx;
width: 16rpx;
height: 16rpx;
background: #7FE08A;
border-radius: 50%;
border: 2rpx solid #fff;
}
.bluetooth-icon .bt-icon-img {
width: 32rpx;
height: 32rpx;
}
.header-info {
display: flex;
flex-direction: column;
overflow: hidden;
}
.header-title {
font-size: 30rpx;
font-weight: 600;
color: #fff;
margin-bottom: 2rpx;
}
.header-subtitle {
font-size: 20rpx;
color: rgba(255, 255, 255, 0.8);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.header-right {
display: flex;
align-items: center;
flex-shrink: 0;
}
/* 状态卡片 */
.status-card {
margin: 0 24rpx 20rpx;
background: rgba(255, 255, 255, 0.15);
border-radius: 20rpx;
padding: 16rpx 20rpx;
backdrop-filter: blur(10px);
}
.status-row {
display: flex;
justify-content: space-between;
}
.status-item {
display: flex;
flex-direction: column;
align-items: center;
}
.status-label {
font-size: 18rpx;
color: rgba(255, 255, 255, 0.7);
margin-bottom: 6rpx;
}
.status-value {
font-size: 20rpx;
font-weight: 500;
color: rgba(255, 255, 255, 0.9);
padding: 4rpx 12rpx;
background: rgba(255, 255, 255, 0.1);
border-radius: 16rpx;
}
.status-value.success {
background: rgba(253, 203, 110, 0.4);
color: #ffeaa7;
}
.status-value.active {
background: rgba(255, 234, 167, 0.4);
color: #ffeaa7;
}
/* 主要内容区 */
.main-content {
background: #f5f7fa;
border-radius: 32rpx 32rpx 0 0;
min-height: calc(100vh - 260rpx);
padding: 24rpx;
width: 100%;
box-sizing: border-box;
}
/* 区块样式 */
.section {
margin-bottom: 24rpx;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 14rpx;
}
.section-title {
font-size: 26rpx;
font-weight: 600;
color: #1a1a2e;
}
.action-btn {
padding: 8rpx 16rpx;
background: #f0f2f5;
border-radius: 14rpx;
display: flex;
align-items: center;
}
.action-btn.primary {
background: linear-gradient(135deg, #00b894 0%, #00cec9 100%);
}
.action-btn.primary .btn-text {
color: #fff;
}
.action-btn.discovering {
background: linear-gradient(135deg, #e17055 0%, #fdcb6e 100%);
}
.action-btn.disconnect-btn {
background: linear-gradient(135deg, #e17055 0%, #fdcb6e 100%);
}
.action-btn.disconnect-btn .btn-text {
color: #fff;
}
.action-btn.clear-btn {
background: #e8eaed;
}
.btn-text {
font-size: 20rpx;
color: #666;
font-weight: 500;
}
/* 设备卡片 */
.device-list {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.device-card {
background: #fff;
border-radius: 16rpx;
padding: 16rpx;
display: flex;
align-items: center;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
overflow: hidden;
}
.device-card.connected {
background: linear-gradient(135deg, #00b894 0%, #00cec9 100%);
}
.device-icon {
width: 56rpx;
height: 56rpx;
background: #f0f2f5;
border-radius: 14rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 14rpx;
flex-shrink: 0;
position: relative;
}
.device-icon.bonded {
background: linear-gradient(135deg, #00b894 0%, #00cec9 100%);
}
.device-icon.bonded .bt-icon-img {
width: 28rpx;
height: 28rpx;
}
.device-icon.connected {
background: rgba(255, 255, 255, 0.2);
}
.device-icon.connected .bt-icon-img {
width: 28rpx;
height: 28rpx;
}
.device-icon .bt-icon-img {
width: 28rpx;
height: 28rpx;
}
.device-info {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
min-width: 0;
}
.device-name {
font-size: 24rpx;
font-weight: 600;
color: #1a1a2e;
margin-bottom: 2rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.device-card.connected .device-name {
color: #fff;
}
.device-id {
font-size: 18rpx;
color: #999;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.device-card.connected .device-id {
color: rgba(255, 255, 255, 0.8);
}
.device-meta {
display: flex;
gap: 8rpx;
margin-top: 6rpx;
flex-wrap: wrap;
}
.meta-item {
font-size: 16rpx;
padding: 2rpx 8rpx;
border-radius: 8rpx;
background: #f0f2f5;
color: #666;
}
.meta-item.rssi {
background: #e8f8f5;
color: #00b894;
}
.meta-item.bond-state {
background: #fef9e7;
color: #f39c12;
}
.device-status {
display: flex;
align-items: center;
gap: 6rpx;
margin-left: 10rpx;
flex-shrink: 0;
}
.device-status.connected .status-dot {
width: 10rpx;
height: 10rpx;
background: #ffeaa7;
border-radius: 50%;
}
.device-status.connected .status-text {
font-size: 18rpx;
color: rgba(255, 255, 255, 0.9);
}
.device-actions {
display: flex;
gap: 8rpx;
margin-left: 10rpx;
flex-shrink: 0;
}
.action-icon {
padding: 8rpx 12rpx;
border-radius: 10rpx;
background: #f0f2f5;
display: flex;
align-items: center;
}
.action-icon.connect {
background: linear-gradient(135deg, #00b894 0%, #00cec9 100%);
}
.action-icon.connect .action-text {
color: #fff;
}
.action-icon.pair {
background: linear-gradient(135deg, #fdcb6e 0%, #f39c12 100%);
}
.action-icon.pair .action-text {
color: #fff;
}
.action-icon.remove {
background: #fdecea;
}
.action-icon.remove .action-text {
color: #e74c3c;
}
.action-text {
font-size: 18rpx;
font-weight: 500;
color: #666;
}
/* 搜索进度 */
.search-progress {
margin-bottom: 14rpx;
}
.progress-bar {
height: 4rpx;
background: #e0e0e0;
border-radius: 2rpx;
overflow: hidden;
margin-bottom: 8rpx;
}
.progress-inner {
height: 100%;
background: linear-gradient(90deg, #00b894, #00cec9);
border-radius: 2rpx;
animation: progress 2s ease-in-out infinite;
}
@keyframes progress {
0% {
width: 0%;
margin-left: 0%;
}
50% {
width: 60%;
margin-left: 20%;
}
100% {
width: 0%;
margin-left: 100%;
}
}
.progress-text {
font-size: 20rpx;
color: #999;
}
/* 空状态 */
.empty-state {
padding: 40rpx 0;
text-align: center;
}
.empty-text {
font-size: 22rpx;
color: #999;
}
/* 数据通信区域 */
.data-card {
background: #fff;
border-radius: 14rpx;
padding: 14rpx;
margin-bottom: 14rpx;
}
.data-label {
font-size: 20rpx;
color: #999;
margin-bottom: 8rpx;
display: block;
}
.data-content {
background: #f8f9fa;
border-radius: 10rpx;
padding: 12rpx;
min-height: 160rpx;
max-height: 240rpx;
}
.data-text {
font-size: 20rpx;
color: #333;
font-family: "Courier New", monospace;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-all;
}
.data-placeholder {
font-size: 20rpx;
color: #ccc;
}
.send-area {
display: flex;
gap: 12rpx;
}
.input-wrapper {
flex: 1;
background: #fff;
border-radius: 14rpx;
padding: 0 16rpx;
border: 2rpx solid #e0e0e0;
}
.send-input {
height: 64rpx;
font-size: 24rpx;
color: #333;
}
.input-placeholder {
color: #ccc;
}
.send-btn {
width: 100rpx;
height: 64rpx;
background: linear-gradient(135deg, #00b894 0%, #00cec9 100%);
border-radius: 14rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.send-btn.disabled {
background: #e0e0e0;
}
.send-btn-text {
font-size: 24rpx;
font-weight: 600;
color: #fff;
}
.send-btn.disabled .send-btn-text {
color: #999;
}
/* 未初始化提示 */
.init-prompt {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80rpx 40rpx;
}
.init-icon {
width: 120rpx;
height: 120rpx;
background: rgba(255, 255, 255, 0.15);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 30rpx;
}
.init-icon .bt-icon-img {
width: 60rpx;
height: 60rpx;
}
.init-title {
font-size: 32rpx;
font-weight: 600;
color: #fff;
margin-bottom: 12rpx;
}
.init-desc {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.7);
text-align: center;
margin-bottom: 40rpx;
}
.init-btn {
padding: 18rpx 60rpx;
background: #fff;
border-radius: 40rpx;
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.15);
}
.init-btn-text {
font-size: 26rpx;
font-weight: 600;
color: #00b894;
}
/* Toast */
.toast {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.8);
background: rgba(0, 0, 0, 0.75);
padding: 18rpx 36rpx;
border-radius: 12rpx;
opacity: 0;
transition: all 0.3s ease;
pointer-events: none;
z-index: 1000;
}
.toast.show {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
.toast-text {
font-size: 24rpx;
color: #fff;
}
/* Loading */
.loading-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1001;
}
.loading-content {
background: #fff;
padding: 36rpx 48rpx;
border-radius: 18rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.loading-spinner {
width: 48rpx;
height: 48rpx;
border: 4rpx solid #f0f0f0;
border-top-color: #00b894;
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin-bottom: 18rpx;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.loading-text {
font-size: 24rpx;
color: #666;
}
</style>
options API
<template>
<view class="container">
<!-- 顶部状态栏 -->
<view class="header">
<view class="header-content">
<view class="header-left">
<view class="bluetooth-icon" :class="{ active: bluetoothEnabled }">
<image
class="bt-icon-img"
:src="bluetoothEnabled ? '/static/icons/bluetooth-green.svg' : '/static/icons/bluetooth-white.svg'"
mode="aspectFit"
/>
</view>
<view class="header-info">
<text class="header-title">经典蓝牙管理</text>
<text class="header-subtitle">{{ statusText }}</text>
</view>
</view>
<view class="header-right">
<switch
:checked="adapterOpened"
@change="toggleAdapter"
color="#00b894"
/>
</view>
</view>
</view>
<!-- 蓝牙状态卡片 -->
<view class="status-card" v-if="adapterOpened">
<view class="status-row">
<view class="status-item">
<text class="status-label">适配器状态</text>
<text class="status-value" :class="{ success: bluetoothEnabled }">
{{ bluetoothEnabled ? "已开启" : "已关闭" }}
</text>
</view>
<view class="status-item">
<text class="status-label">搜索状态</text>
<text class="status-value" :class="{ active: isDiscovering }">
{{ isDiscovering ? "搜索中..." : "空闲" }}
</text>
</view>
<view class="status-item">
<text class="status-label">连接状态</text>
<text class="status-value" :class="{ success: isConnected }">
{{ isConnected ? "已连接" : "未连接" }}
</text>
</view>
</view>
</view>
<!-- 主要内容区域 -->
<scroll-view class="main-content" scroll-y v-if="adapterOpened">
<!-- 已连接设备 -->
<view class="section" v-if="isConnected">
<view class="section-header">
<text class="section-title">已连接设备</text>
<view class="action-btn disconnect-btn" @tap="handleDisconnect">
<text class="btn-text">断开连接</text>
</view>
</view>
<view class="device-card connected">
<view class="device-icon connected">
<image class="bt-icon-img" src="/static/icons/bluetooth-white.svg" mode="aspectFit" />
<view class="online-dot"></view>
</view>
<view class="device-info">
<text class="device-name">{{
connectedDevice.name || "未知设备"
}}</text>
<text class="device-id">{{ connectedDevice.deviceId }}</text>
</view>
<view class="device-status connected">
<text class="status-dot"></text>
<text class="status-text">已连接</text>
</view>
</view>
</view>
<!-- 数据收发区域 -->
<view class="section" v-if="isConnected">
<view class="section-header">
<text class="section-title">数据通信</text>
<view class="action-btn clear-btn" @tap="clearReceivedData">
<text class="btn-text">清空</text>
</view>
</view>
<!-- 接收数据显示 -->
<view class="data-card">
<text class="data-label">接收数据</text>
<scroll-view class="data-content receive" scroll-y>
<text class="data-text" v-if="receivedData">{{
receivedData
}}</text>
<text class="data-placeholder" v-else>等待接收数据...</text>
</scroll-view>
</view>
<!-- 发送数据 -->
<view class="send-area">
<view class="input-wrapper">
<input
class="send-input"
v-model="sendText"
placeholder="输入要发送的数据"
placeholder-class="input-placeholder"
/>
</view>
<view
class="send-btn"
@tap="handleSendData"
:class="{ disabled: !sendText }"
>
<text class="send-btn-text">发送</text>
</view>
</view>
</view>
<!-- 已配对设备 -->
<view class="section">
<view class="section-header">
<text class="section-title">已配对设备</text>
<view class="action-btn" @tap="loadBondedDevices">
<text class="btn-text">刷新</text>
</view>
</view>
<view v-if="bondedDevices.length === 0" class="empty-state">
<text class="empty-text">暂无已配对设备</text>
</view>
<view v-else class="device-list">
<view
class="device-card"
v-for="device in bondedDevices"
:key="device.deviceId"
@tap="handleDeviceTap(device, 'bonded')"
>
<view class="device-icon bonded">
<image class="bt-icon-img" src="/static/icons/bluetooth-white.svg" mode="aspectFit" />
</view>
<view class="device-info">
<text class="device-name">{{ device.name || "未知设备" }}</text>
<text class="device-id">{{ device.deviceId }}</text>
</view>
<view class="device-actions">
<view
class="action-icon connect"
@tap.stop="handleConnect(device)"
>
<text class="action-text">连接</text>
</view>
<view
class="action-icon remove"
@tap.stop="handleRemoveBond(device)"
>
<text class="action-text">取消</text>
</view>
</view>
</view>
</view>
</view>
<!-- 搜索设备 -->
<view class="section">
<view class="section-header">
<text class="section-title">搜索设备</text>
<view
class="action-btn primary"
@tap="toggleDiscovery"
:class="{ discovering: isDiscovering }"
>
<text class="btn-text">{{
isDiscovering ? "停止搜索" : "开始搜索"
}}</text>
</view>
</view>
<!-- 搜索进度 -->
<view class="search-progress" v-if="isDiscovering">
<view class="progress-bar">
<view class="progress-inner"></view>
</view>
<text class="progress-text">正在搜索附近的蓝牙设备...</text>
</view>
<view
v-if="discoveredDevices.length === 0 && !isDiscovering"
class="empty-state"
>
<text class="empty-text">点击"开始搜索"查找附近设备</text>
</view>
<view v-else class="device-list">
<view
class="device-card"
v-for="device in discoveredDevices"
:key="device.deviceId"
@tap="handleDeviceTap(device, 'discovered')"
>
<view class="device-icon">
<image class="bt-icon-img" src="/static/icons/bluetooth-gray.svg" mode="aspectFit" />
</view>
<view class="device-info">
<text class="device-name">{{ device.name || "未知设备" }}</text>
<text class="device-id">{{ device.deviceId }}</text>
<view class="device-meta">
<text class="meta-item rssi">{{ device.rssi }}dBm</text>
</view>
</view>
<view class="device-actions">
<view
class="action-icon pair"
@tap.stop="handleCreateBond(device)"
>
<text class="action-text">配对</text>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
<!-- 未初始化提示 -->
<view class="init-prompt" v-else>
<view class="init-icon">
<image class="bt-icon-img large" src="/static/icons/bluetooth-white.svg" mode="aspectFit" />
</view>
<text class="init-title">蓝牙未开启</text>
<text class="init-desc">请打开蓝牙适配器以使用蓝牙功能</text>
<view class="init-btn" @tap="initBluetooth">
<text class="init-btn-text">开启蓝牙</text>
</view>
</view>
<!-- Toast 提示 -->
<view class="toast" :class="{ show: showToast }">
<text class="toast-text">{{ toastMessage }}</text>
</view>
<!-- Loading 遮罩 -->
<view class="loading-mask" v-if="isLoading">
<view class="loading-content">
<view class="loading-spinner"></view>
<text class="loading-text">{{ loadingText }}</text>
</view>
</view>
</view>
</template>
<script>
import {
openBluetoothAdapter,
closeBluetoothAdapter,
getBluetoothAdapterState,
startDiscovery,
stopDiscovery,
getBondedDevices,
createBond,
removeBond,
connectDevice,
disconnectDevice,
sendString,
getConnectionState,
onDeviceFound,
offDeviceFound,
onDiscoveryFinished,
offDiscoveryFinished,
onBondStateChange,
offBondStateChange,
onConnectionStateChange,
offConnectionStateChange,
onDataReceived,
offDataReceived,
onBluetoothAdapterStateChange,
offBluetoothAdapterStateChange,
} from "@/uni_modules/gp-bluetooth";
export default {
data() {
return {
// 蓝牙状态
adapterOpened: false,
bluetoothEnabled: false,
isDiscovering: false,
isConnected: false,
// 设备列表
bondedDevices: [],
discoveredDevices: [],
connectedDevice: {
deviceId: "",
name: "",
},
// 数据收发
sendText: "",
receivedData: "",
// UI状态
showToast: false,
toastMessage: "",
isLoading: false,
loadingText: "",
};
},
computed: {
statusText() {
if (!this.adapterOpened) return "蓝牙适配器未开启";
if (this.isConnected)
return `已连接: ${
this.connectedDevice.name || this.connectedDevice.deviceId
}`;
if (this.isDiscovering) return "正在搜索设备...";
return "就绪";
},
},
onLoad() {
this.requestPermissions();
},
onUnload() {
this.cleanup();
},
methods: {
// ==================== 权限请求 ====================
requestPermissions() {
// #ifdef APP-PLUS
const permissions = [
"android.permission.ACCESS_FINE_LOCATION",
"android.permission.ACCESS_COARSE_LOCATION",
"android.permission.BLUETOOTH",
"android.permission.BLUETOOTH_ADMIN",
"android.permission.BLUETOOTH_SCAN",
"android.permission.BLUETOOTH_CONNECT",
];
plus.android.requestPermissions(
permissions,
(result) => {
console.log("权限请求结果:", result);
this.checkBluetoothState();
},
(err) => {
console.log("权限请求失败:", err);
this.toast("需要位置权限才能搜索蓝牙设备");
this.checkBluetoothState();
}
);
// #endif
// #ifndef APP-PLUS
this.checkBluetoothState();
// #endif
},
// ==================== 蓝牙适配器管理 ====================
checkBluetoothState() {
getBluetoothAdapterState({
success: (res) => {
this.bluetoothEnabled = res.enabled;
this.isDiscovering = res.discovering;
if (res.available && res.enabled) {
this.adapterOpened = true;
this.setupListeners();
this.loadBondedDevices();
this.checkConnectionState();
}
},
fail: (err) => {
console.log("获取蓝牙状态失败:", err);
},
});
},
initBluetooth() {
this.showLoading("正在初始化蓝牙...");
openBluetoothAdapter({
success: () => {
this.adapterOpened = true;
this.bluetoothEnabled = true;
this.setupListeners();
this.loadBondedDevices();
this.toast("蓝牙已开启");
},
fail: (err) => {
this.toast("蓝牙开启失败: " + err.errMsg);
},
complete: () => {
this.hideLoading();
},
});
},
toggleAdapter(e) {
if (e.detail.value) {
this.initBluetooth();
} else {
this.closeBluetooth();
}
},
closeBluetooth() {
this.showLoading("正在关闭蓝牙...");
closeBluetoothAdapter({
success: () => {
this.adapterOpened = false;
this.bluetoothEnabled = false;
this.isDiscovering = false;
this.isConnected = false;
this.bondedDevices = [];
this.discoveredDevices = [];
this.cleanup();
this.toast("蓝牙已关闭");
},
fail: (err) => {
this.toast("关闭蓝牙失败: " + err.errMsg);
},
complete: () => {
this.hideLoading();
},
});
},
// ==================== 事件监听 ====================
setupListeners() {
// 监听设备发现
onDeviceFound((res) => {
const device = res.device;
// 忽略 name 为空、Unknown 的设备
if (!device.name || device.name === 'Unknown' || device.name.toLowerCase() === 'unknown') {
console.log('忽略未知设备:', device.deviceId);
return;
}
// 忽略已配对的设备 (bondState === 12)
if (device.bondState === 12) {
console.log('忽略已配对设备:', device.name);
return;
}
// 检查是否已在已配对列表中
const isBonded = this.bondedDevices.some(d => d.deviceId === device.deviceId);
if (isBonded) {
console.log('忽略已在配对列表的设备:', device.name);
return;
}
console.log('发现新设备:', device.name, device.deviceId);
const index = this.discoveredDevices.findIndex(
(d) => d.deviceId === device.deviceId
);
if (index === -1) {
this.discoveredDevices.push(device);
} else {
this.discoveredDevices[index] = device;
}
});
// 监听搜索完成
onDiscoveryFinished(() => {
this.isDiscovering = false;
this.toast("搜索完成");
});
// 监听配对状态变化
onBondStateChange((res) => {
console.log("配对状态变化:", res);
if (res.bondState === 12) {
this.toast("配对成功,正在连接...");
this.loadBondedDevices();
// 配对成功后自动连接
setTimeout(() => {
const device = this.discoveredDevices.find(
(d) => d.deviceId === res.deviceId
);
this.connectToDevice({
deviceId: res.deviceId,
name: device?.name || ""
});
}, 500);
} else if (res.bondState === 10) {
this.toast("已取消配对");
this.loadBondedDevices();
}
// 更新发现设备列表中的配对状态
const index = this.discoveredDevices.findIndex(
(d) => d.deviceId === res.deviceId
);
if (index !== -1) {
this.discoveredDevices[index].bondState = res.bondState;
}
});
// 监听连接状态变化
onConnectionStateChange((res) => {
console.log("连接状态变化:", res);
this.isConnected = res.connected;
if (res.connected) {
this.connectedDevice.deviceId = res.deviceId;
// 查找设备名称
const bonded = this.bondedDevices.find(
(d) => d.deviceId === res.deviceId
);
const discovered = this.discoveredDevices.find(
(d) => d.deviceId === res.deviceId
);
this.connectedDevice.name = bonded?.name || discovered?.name || "";
this.toast("连接成功");
} else {
this.connectedDevice = { deviceId: "", name: "" };
this.toast("连接已断开");
}
this.hideLoading();
});
// 监听数据接收
onDataReceived((res) => {
console.log("收到数据:", res);
const timestamp = new Date().toLocaleTimeString();
this.receivedData += `[${timestamp}] ${res.text}\n`;
});
// 监听蓝牙适配器状态变化
onBluetoothAdapterStateChange((res) => {
console.log("适配器状态变化:", res);
this.bluetoothEnabled = res.available;
this.isDiscovering = res.discovering;
});
},
cleanup() {
offDeviceFound();
offDiscoveryFinished();
offBondStateChange();
offConnectionStateChange();
offDataReceived();
offBluetoothAdapterStateChange();
},
// ==================== 设备搜索 ====================
toggleDiscovery() {
if (this.isDiscovering) {
this.stopSearch();
} else {
this.startSearch();
}
},
startSearch() {
this.discoveredDevices = [];
startDiscovery({
timeout: 15000,
success: () => {
this.isDiscovering = true;
this.toast("开始搜索设备");
},
fail: (err) => {
this.toast("搜索失败: " + err.errMsg);
},
});
},
stopSearch() {
stopDiscovery({
success: () => {
this.isDiscovering = false;
this.toast("已停止搜索");
},
fail: (err) => {
this.toast("停止搜索失败: " + err.errMsg);
},
});
},
// ==================== 设备管理 ====================
loadBondedDevices() {
getBondedDevices({
success: (res) => {
this.bondedDevices = res.devices;
},
fail: (err) => {
console.log("获取已配对设备失败:", err);
},
});
},
checkConnectionState() {
getConnectionState({
success: (res) => {
this.isConnected = res.connected;
if (res.connected) {
this.connectedDevice.deviceId = res.deviceId;
const bonded = this.bondedDevices.find(
(d) => d.deviceId === res.deviceId
);
this.connectedDevice.name = bonded?.name || "";
}
},
});
},
handleDeviceTap(device, type) {
console.log("设备点击:", device, type);
},
handleCreateBond(device) {
this.showLoading("正在配对...");
createBond({
deviceId: device.deviceId,
success: () => {
console.log("发起配对请求成功");
},
fail: (err) => {
this.toast("配对失败: " + err.errMsg);
this.hideLoading();
},
});
// 配对结果由 onBondStateChange 回调处理
setTimeout(() => this.hideLoading(), 3000);
},
handleRemoveBond(device) {
uni.showModal({
title: "取消配对",
content: `确定要取消与 "${device.name || device.deviceId}" 的配对吗?`,
success: (res) => {
if (res.confirm) {
this.showLoading("正在取消配对...");
removeBond({
deviceId: device.deviceId,
success: () => {
this.loadBondedDevices();
this.toast("已取消配对");
},
fail: (err) => {
this.toast("取消配对失败: " + err.errMsg);
},
complete: () => {
this.hideLoading();
},
});
}
},
});
},
handleConnect(device) {
if (this.isConnected) {
uni.showModal({
title: "提示",
content: "当前已有设备连接,是否断开并连接新设备?",
success: (res) => {
if (res.confirm) {
this.disconnectAndConnect(device);
}
},
});
} else {
this.connectToDevice(device);
}
},
disconnectAndConnect(device) {
this.showLoading("正在切换连接...");
disconnectDevice({
success: () => {
setTimeout(() => {
this.connectToDevice(device);
}, 500);
},
fail: () => {
this.toast("断开连接失败");
this.hideLoading();
},
});
},
connectToDevice(device) {
this.showLoading("正在连接...");
connectDevice({
deviceId: device.deviceId,
timeout: 15000,
secure: false,
success: () => {
console.log("连接请求已发送");
},
fail: (err) => {
this.toast("连接失败: " + err.errMsg);
this.hideLoading();
},
});
},
handleDisconnect() {
uni.showModal({
title: "断开连接",
content: "确定要断开当前连接吗?",
success: (res) => {
if (res.confirm) {
this.showLoading("正在断开连接...");
disconnectDevice({
success: () => {
this.isConnected = false;
this.connectedDevice = { deviceId: "", name: "" };
this.toast("已断开连接");
},
fail: (err) => {
this.toast("断开连接失败: " + err.errMsg);
},
complete: () => {
this.hideLoading();
},
});
}
},
});
},
// ==================== 数据收发 ====================
handleSendData() {
if (!this.sendText.trim()) {
this.toast("请输入要发送的数据");
return;
}
sendString({
data: this.sendText,
encoding: "UTF-8",
success: () => {
const timestamp = new Date().toLocaleTimeString();
this.receivedData += `[${timestamp}] 发送: ${this.sendText}\n`;
this.sendText = "";
this.toast("发送成功");
},
fail: (err) => {
this.toast("发送失败: " + err.errMsg);
},
});
},
clearReceivedData() {
this.receivedData = "";
this.toast("已清空");
},
// ==================== 工具方法 ====================
getBondStateText(state) {
switch (state) {
case 10:
return "未配对";
case 11:
return "配对中";
case 12:
return "已配对";
default:
return "未知";
}
},
toast(message) {
this.toastMessage = message;
this.showToast = true;
setTimeout(() => {
this.showToast = false;
}, 2000);
},
showLoading(text) {
this.loadingText = text;
this.isLoading = true;
},
hideLoading() {
this.isLoading = false;
},
},
};
</script>
<style>
/* 全局变量 */
page {
background-color: #f5f7fa;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, sans-serif;
}
.container {
min-height: 100vh;
background: linear-gradient(180deg, #00b894 0%, #00cec9 100%);
padding-bottom: env(safe-area-inset-bottom);
overflow-x: hidden;
}
/* 顶部头部 */
.header {
padding: 40rpx 24rpx 30rpx;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.header-left {
display: flex;
align-items: center;
flex: 1;
overflow: hidden;
}
.bluetooth-icon {
width: 64rpx;
height: 64rpx;
background: rgba(255, 255, 255, 0.2);
border-radius: 18rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16rpx;
flex-shrink: 0;
}
.bluetooth-icon.active {
background: rgba(255, 255, 255, 0.95);
}
/* CSS 蓝牙图标 */
.bt-icon-img {
width: 32rpx;
height: 32rpx;
}
.bt-icon-img.large {
width: 56rpx;
height: 56rpx;
}
/* 在线状态小圆点 */
.online-dot {
position: absolute;
right: 4rpx;
top: 4rpx;
width: 16rpx;
height: 16rpx;
background: #7FE08A;
border-radius: 50%;
border: 2rpx solid #fff;
}
.bluetooth-icon .bt-icon-img {
width: 32rpx;
height: 32rpx;
}
.header-info {
display: flex;
flex-direction: column;
overflow: hidden;
}
.header-title {
font-size: 30rpx;
font-weight: 600;
color: #fff;
margin-bottom: 2rpx;
}
.header-subtitle {
font-size: 20rpx;
color: rgba(255, 255, 255, 0.8);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.header-right {
display: flex;
align-items: center;
flex-shrink: 0;
}
/* 状态卡片 */
.status-card {
margin: 0 24rpx 20rpx;
background: rgba(255, 255, 255, 0.15);
border-radius: 20rpx;
padding: 16rpx 20rpx;
backdrop-filter: blur(10px);
}
.status-row {
display: flex;
justify-content: space-between;
}
.status-item {
display: flex;
flex-direction: column;
align-items: center;
}
.status-label {
font-size: 18rpx;
color: rgba(255, 255, 255, 0.7);
margin-bottom: 6rpx;
}
.status-value {
font-size: 20rpx;
font-weight: 500;
color: rgba(255, 255, 255, 0.9);
padding: 4rpx 12rpx;
background: rgba(255, 255, 255, 0.1);
border-radius: 16rpx;
}
.status-value.success {
background: rgba(253, 203, 110, 0.4);
color: #ffeaa7;
}
.status-value.active {
background: rgba(255, 234, 167, 0.4);
color: #ffeaa7;
}
/* 主要内容区 */
.main-content {
background: #f5f7fa;
border-radius: 32rpx 32rpx 0 0;
min-height: calc(100vh - 260rpx);
padding: 24rpx;
width: 100%;
box-sizing: border-box;
}
/* 区块样式 */
.section {
margin-bottom: 24rpx;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 14rpx;
}
.section-title {
font-size: 26rpx;
font-weight: 600;
color: #1a1a2e;
}
.action-btn {
padding: 8rpx 16rpx;
background: #f0f2f5;
border-radius: 14rpx;
display: flex;
align-items: center;
}
.action-btn.primary {
background: linear-gradient(135deg, #00b894 0%, #00cec9 100%);
}
.action-btn.primary .btn-text {
color: #fff;
}
.action-btn.discovering {
background: linear-gradient(135deg, #e17055 0%, #fdcb6e 100%);
}
.action-btn.disconnect-btn {
background: linear-gradient(135deg, #e17055 0%, #fdcb6e 100%);
}
.action-btn.disconnect-btn .btn-text {
color: #fff;
}
.action-btn.clear-btn {
background: #e8eaed;
}
.btn-text {
font-size: 20rpx;
color: #666;
font-weight: 500;
}
/* 设备卡片 */
.device-list {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.device-card {
background: #fff;
border-radius: 16rpx;
padding: 16rpx;
display: flex;
align-items: center;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
overflow: hidden;
}
.device-card.connected {
background: linear-gradient(135deg, #00b894 0%, #00cec9 100%);
}
.device-icon {
width: 56rpx;
height: 56rpx;
background: #f0f2f5;
border-radius: 14rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 14rpx;
flex-shrink: 0;
position: relative;
}
.device-icon.bonded {
background: linear-gradient(135deg, #00b894 0%, #00cec9 100%);
}
.device-icon.bonded .bt-icon-img {
width: 28rpx;
height: 28rpx;
}
.device-icon.connected {
background: rgba(255, 255, 255, 0.2);
}
.device-icon.connected .bt-icon-img {
width: 28rpx;
height: 28rpx;
}
.device-icon .bt-icon-img {
width: 28rpx;
height: 28rpx;
}
.device-info {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
min-width: 0;
}
.device-name {
font-size: 24rpx;
font-weight: 600;
color: #1a1a2e;
margin-bottom: 2rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.device-card.connected .device-name {
color: #fff;
}
.device-id {
font-size: 18rpx;
color: #999;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.device-card.connected .device-id {
color: rgba(255, 255, 255, 0.8);
}
.device-meta {
display: flex;
gap: 8rpx;
margin-top: 6rpx;
flex-wrap: wrap;
}
.meta-item {
font-size: 16rpx;
padding: 2rpx 8rpx;
border-radius: 8rpx;
background: #f0f2f5;
color: #666;
}
.meta-item.rssi {
background: #e8f8f5;
color: #00b894;
}
.meta-item.bond-state {
background: #fef9e7;
color: #f39c12;
}
.device-status {
display: flex;
align-items: center;
gap: 6rpx;
margin-left: 10rpx;
flex-shrink: 0;
}
.device-status.connected .status-dot {
width: 10rpx;
height: 10rpx;
background: #ffeaa7;
border-radius: 50%;
}
.device-status.connected .status-text {
font-size: 18rpx;
color: rgba(255, 255, 255, 0.9);
}
.device-actions {
display: flex;
gap: 8rpx;
margin-left: 10rpx;
flex-shrink: 0;
}
.action-icon {
padding: 8rpx 12rpx;
border-radius: 10rpx;
background: #f0f2f5;
display: flex;
align-items: center;
}
.action-icon.connect {
background: linear-gradient(135deg, #00b894 0%, #00cec9 100%);
}
.action-icon.connect .action-text {
color: #fff;
}
.action-icon.pair {
background: linear-gradient(135deg, #fdcb6e 0%, #f39c12 100%);
}
.action-icon.pair .action-text {
color: #fff;
}
.action-icon.remove {
background: #fdecea;
}
.action-icon.remove .action-text {
color: #e74c3c;
}
.action-text {
font-size: 18rpx;
font-weight: 500;
color: #666;
}
/* 搜索进度 */
.search-progress {
margin-bottom: 14rpx;
}
.progress-bar {
height: 4rpx;
background: #e0e0e0;
border-radius: 2rpx;
overflow: hidden;
margin-bottom: 8rpx;
}
.progress-inner {
height: 100%;
background: linear-gradient(90deg, #00b894, #00cec9);
border-radius: 2rpx;
animation: progress 2s ease-in-out infinite;
}
@keyframes progress {
0% {
width: 0%;
margin-left: 0%;
}
50% {
width: 60%;
margin-left: 20%;
}
100% {
width: 0%;
margin-left: 100%;
}
}
.progress-text {
font-size: 20rpx;
color: #999;
}
/* 空状态 */
.empty-state {
padding: 40rpx 0;
text-align: center;
}
.empty-text {
font-size: 22rpx;
color: #999;
}
/* 数据通信区域 */
.data-card {
background: #fff;
border-radius: 14rpx;
padding: 14rpx;
margin-bottom: 14rpx;
}
.data-label {
font-size: 20rpx;
color: #999;
margin-bottom: 8rpx;
display: block;
}
.data-content {
background: #f8f9fa;
border-radius: 10rpx;
padding: 12rpx;
min-height: 160rpx;
max-height: 240rpx;
}
.data-text {
font-size: 20rpx;
color: #333;
font-family: "Courier New", monospace;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-all;
}
.data-placeholder {
font-size: 20rpx;
color: #ccc;
}
.send-area {
display: flex;
gap: 12rpx;
}
.input-wrapper {
flex: 1;
background: #fff;
border-radius: 14rpx;
padding: 0 16rpx;
border: 2rpx solid #e0e0e0;
}
.send-input {
height: 64rpx;
font-size: 24rpx;
color: #333;
}
.input-placeholder {
color: #ccc;
}
.send-btn {
width: 100rpx;
height: 64rpx;
background: linear-gradient(135deg, #00b894 0%, #00cec9 100%);
border-radius: 14rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.send-btn.disabled {
background: #e0e0e0;
}
.send-btn-text {
font-size: 24rpx;
font-weight: 600;
color: #fff;
}
.send-btn.disabled .send-btn-text {
color: #999;
}
/* 未初始化提示 */
.init-prompt {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80rpx 40rpx;
}
.init-icon {
width: 120rpx;
height: 120rpx;
background: rgba(255, 255, 255, 0.15);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 30rpx;
}
.init-icon .bt-icon-img {
width: 60rpx;
height: 60rpx;
}
.init-title {
font-size: 32rpx;
font-weight: 600;
color: #fff;
margin-bottom: 12rpx;
}
.init-desc {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.7);
text-align: center;
margin-bottom: 40rpx;
}
.init-btn {
padding: 18rpx 60rpx;
background: #fff;
border-radius: 40rpx;
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.15);
}
.init-btn-text {
font-size: 26rpx;
font-weight: 600;
color: #00b894;
}
/* Toast */
.toast {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.8);
background: rgba(0, 0, 0, 0.75);
padding: 18rpx 36rpx;
border-radius: 12rpx;
opacity: 0;
transition: all 0.3s ease;
pointer-events: none;
z-index: 1000;
}
.toast.show {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
.toast-text {
font-size: 24rpx;
color: #fff;
}
/* Loading */
.loading-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1001;
}
.loading-content {
background: #fff;
padding: 36rpx 48rpx;
border-radius: 18rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.loading-spinner {
width: 48rpx;
height: 48rpx;
border: 4rpx solid #f0f0f0;
border-top-color: #00b894;
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin-bottom: 18rpx;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.loading-text {
font-size: 24rpx;
color: #666;
}
</style>
常见问题
Q: 为什么搜索不到设备?
A: 请检查以下几点:
- 确保蓝牙已开启
- 确保已授予位置权限(Android 6.0+)
- 确保目标设备处于可被发现状态
Q: 连接失败怎么办?
A: 可以尝试:
- 先配对设备再连接
- 调整
secure参数(尝试安全/不安全连接) - 增加连接超时时间
- 确保设备支持 SPP 协议
Q: iOS 为什么不支持?
A: iOS 系统限制了第三方 App 访问经典蓝牙功能,只能使用 BLE(低功耗蓝牙)。如果您的设备支持 BLE,建议使用 BLE 方案。
Q: 如何处理大数据量传输?
A: 建议:
- 分包发送,每包不超过 512 字节
- 添加发送间隔,避免拥堵
- 实现应答机制,确保数据可靠传输

收藏人数:
购买源码授权版(
试用
赞赏(0)
下载 141
赞赏 0
下载 12419757
赞赏 1829
赞赏
京公网安备:11010802035340号