更新记录

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. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

无

暂无用户评论。