更新记录

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: 请检查以下几点:

  1. 确保蓝牙已开启
  2. 确保已授予位置权限(Android 6.0+)
  3. 确保目标设备处于可被发现状态

Q: 连接失败怎么办?

A: 可以尝试:

  1. 先配对设备再连接
  2. 调整 secure 参数(尝试安全/不安全连接)
  3. 增加连接超时时间
  4. 确保设备支持 SPP 协议

Q: iOS 为什么不支持?

A: iOS 系统限制了第三方 App 访问经典蓝牙功能,只能使用 BLE(低功耗蓝牙)。如果您的设备支持 BLE,建议使用 BLE 方案。

Q: 如何处理大数据量传输?

A: 建议:

  1. 分包发送,每包不超过 512 字节
  2. 添加发送间隔,避免拥堵
  3. 实现应答机制,确保数据可靠传输

参考资料

隐私、权限声明

1. 本插件需要申请的系统权限列表:

蓝牙相关权限,位置权限

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

插件不采集任何数据

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

暂无用户评论。