Files
intc-mixer-app/src/pages/bluetooth/bluetooth.vue
2025-12-18 22:11:37 +08:00

2115 lines
58 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="bluetooth-page">
<!-- 导航栏 -->
<uni-navbar title="搅拌器设备连接" :border="false" background-color="rgba(102, 126, 234, 0.9)" color="#fff" />
<view class="bluetooth-container">
<!-- 搜索状态和设置 -->
<view class="search-status">
<view class="status-row">
<view class="status-item">
<text class="status-label">蓝牙状态</text>
<text :class="['status-value', bluetoothEnabled ? 'status-on' : 'status-off']">
{{ bluetoothEnabled ? '已开启' : '未开启' }}
</text>
</view>
<view class="status-item">
<text class="status-label">搜索状态</text>
<text :class="['status-value', isSearching ? 'status-on' : 'status-off']">
{{ isSearching ? '搜索中...' : '未搜索' }}
</text>
</view>
</view>
</view>
<!-- 操作按钮 -->
<view class="action-buttons">
<u-button
type="primary"
:loading="isSearching"
@click="startBluetoothSearch"
:disabled="!bluetoothEnabled"
>
{{ isSearching ? '搜索中...' : '开始搜索' }}
</u-button>
<u-button
type="warning"
plain
@click="stopBluetoothSearch"
:disabled="!isSearching"
style="margin-left: 20rpx;"
>
停止搜索
</u-button>
</view>
<!-- 设备列表 -->
<view class="device-list">
<view class="list-header">
<text class="header-title">附近的设备</text>
<text class="device-count">({{ deviceList.length }})</text>
</view>
<view v-if="deviceList.length === 0" class="empty-state">
<view class="empty-icon">
<image
class="bluetooth-img"
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2MCIgaGVpZ2h0PSI2MCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSIjZmZmZmZmIj48cGF0aCBkPSJNMTcuNzEgNy43MUwxMiAyaC0xdjcuNTlMNi40MSA1IDUgNi40MSAxMC41OSAxMiA1IDE3LjU5IDYuNDEgMTkgMTEgMTQuNDFWMjJoMWw1LjcxLTUuNzEtNC4zLTQuMjkgNC4zLTQuMjl6TTEzIDUuODNsMS44OCAxLjg4TDEzIDkuNTlWNS44M3ptMS44OCAxMC40NkwxMyAxOC4xN3YtMy43NmwxLjg4IDEuODh6Ii8+PC9zdmc+"
mode="aspectFit"
/>
</view>
<text class="empty-text">暂无设备,点击上方按钮开始搜索</text>
</view>
<view v-else class="device-items">
<view
v-for="(device, index) in deviceList"
:key="device.deviceId"
class="device-wrapper"
>
<view
class="device-item"
@click="connectDevice(device)"
>
<view class="device-info">
<view class="device-name">
<view class="device-icon">
<image
class="bluetooth-img-small"
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSIjZmZmZmZmIj48cGF0aCBkPSJNMTcuNzEgNy43MUwxMiAyaC0xdjcuNTlMNi40MSA1IDUgNi40MSAxMC41OSAxMiA1IDE3LjU5IDYuNDEgMTkgMTEgMTQuNDFWMjJoMWw1LjcxLTUuNzEtNC4zLTQuMjkgNC4zLTQuMjl6TTEzIDUuODNsMS44OCAxLjg4TDEzIDkuNTlWNS44M3ptMS44OCAxMC40NkwxMyAxOC4xN3YtMy43NmwxLjg4IDEuODh6Ii8+PC9zdmc+"
mode="aspectFit"
/>
</view>
<text class="name-text">
{{ device.name || device.localName || `未命名设备 (${device.deviceId.slice(-8)})` }}
</text>
</view>
<view class="device-details">
<text class="detail-text">设备ID: {{ device.deviceId }}</text>
<text class="detail-text">信号强度: {{ device.RSSI }} dBm</text>
</view>
</view>
<view class="device-action">
<view v-if="device.deviceId === connectedDeviceId" class="connected-badge">
<text class="badge-text">已连接</text>
</view>
<view v-else-if="device.deviceId === connectingDeviceId" class="connecting-badge">
<text class="connecting-text">连接中...</text>
</view>
<u-button
v-else
type="primary"
size="small"
@click.stop="connectDevice(device)"
>
连接
</u-button>
</view>
</view>
<!-- 已连接设备的详细信息显示在对应设备下方 -->
<view v-if="device.deviceId === connectedDeviceId && connectedDevice" class="device-connected-detail">
<view class="detail-header">
<text class="detail-title">连接成功准备跳转...</text>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
// 状态变量
const bluetoothEnabled = ref(false)
const isSearching = ref(false)
const deviceList = ref([])
const connectedDevice = ref(null)
const connectedDeviceId = ref('')
const connectingDeviceId = ref('')
// 新增:服务和特征值相关
const services = ref([]) // 设备服务列表
const characteristics = ref([]) // 当前服务的特征值列表
const notifyCharacteristics = ref([]) // 可监听的特征值
const totalCharacteristicsCount = ref(0)
// 监听器标记 - 防止重复注册
let hasRegisteredCharacteristicListener = false
// 权限被拒绝提示标记 - 防止重复弹窗
let hasShownPermissionDenied = false
// 自动重连标记 - 防止重复尝试
let hasTriedAutoReconnect = false
// 本地存储key
const LAST_DEVICE_KEY = 'last_connected_bluetooth_device'
// 数据交互相关
const inputData = ref('') // 输入框数据
const sendHistory = ref([]) // 发送历史记录
const receiveHistory = ref([]) // 接收历史记录
const showHistory = ref(false) // 是否显示历史记录
const readStats = ref(null) // 读取统计信息 { total, success, fail }
// 计算合并的历史记录(用于显示)
const dataHistory = computed(() => {
return [...sendHistory.value, ...receiveHistory.value]
.sort((a, b) => b.timestamp - a.timestamp) // 按时间倒序排列
})
// 计算发送按钮是否禁用
const isSendDisabled = computed(() => {
const disabled = !inputData.value || inputData.value.trim() === ''
console.log('发送按钮状态:', {
inputData: inputData.value,
length: inputData.value?.length,
trimmed: inputData.value?.trim(),
disabled
})
return disabled
})
// 初始化蓝牙适配器
const initBluetooth = () => {
// 先检查和请求权限
checkAndRequestPermissions()
}
// 检查和请求蓝牙相关权限
const checkAndRequestPermissions = () => {
// #ifdef APP-PLUS
const permissions = []
// 获取 Android 版本
const platform = uni.getSystemInfoSync().platform
const system = uni.getSystemInfoSync().system
if (platform === 'android') {
// 解析 Android 版本号
const androidVersion = parseInt(system.replace(/[^0-9]/ig, ''))
if (androidVersion >= 12) {
// Android 12+ 需要新的蓝牙权限
permissions.push('android.permission.BLUETOOTH_SCAN')
permissions.push('android.permission.BLUETOOTH_CONNECT')
permissions.push('android.permission.BLUETOOTH_ADVERTISE')
} else {
// Android 6-11 需要位置权限
permissions.push('android.permission.ACCESS_FINE_LOCATION')
permissions.push('android.permission.ACCESS_COARSE_LOCATION')
}
// 先检查权限状态,避免重复提示
const checkPermissions = permissions.map(permission => {
return plus.android.checkPermission(permission)
})
// 如果所有权限都已被永久拒绝(返回-1表示被永久拒绝跳过请求
const allDeniedAlways = checkPermissions.every(status => status === -1)
if (allDeniedAlways) {
console.log('蓝牙权限已被永久拒绝,跳过请求')
// 不弹窗,静默失败,用户点击搜索时会提示
return
}
// 如果所有权限都已授予返回0表示已授予
const allGranted = checkPermissions.every(status => status === 0)
if (allGranted) {
console.log('蓝牙权限已授予')
openBluetoothAdapter()
return
}
// 请求权限
plus.android.requestPermissions(permissions,
(resultObj) => {
const granted = resultObj.granted
const deniedAlways = resultObj.deniedAlways
const deniedPresent = resultObj.deniedPresent
if (granted.length > 0) {
// 权限获取成功,初始化蓝牙
openBluetoothAdapter()
} else if (deniedPresent.length > 0) {
// 本次拒绝(但没有选择不再询问),不弹窗,静默失败
console.log('用户本次拒绝了权限,可稍后点击"开始搜索"时再次请求')
} else if (deniedAlways.length > 0) {
// 永久拒绝(选择了不再询问),也不弹窗
// 因为用户可能只是暂时不想授权,不应该每次进入都骚扰用户
console.log('用户永久拒绝了权限,可在需要时引导用户去设置')
}
},
(error) => {
console.error('权限请求失败', error)
}
)
} else {
// iOS 直接初始化
openBluetoothAdapter()
}
// #endif
// #ifndef APP-PLUS
openBluetoothAdapter()
// #endif
}
// 打开蓝牙适配器
const openBluetoothAdapter = () => {
uni.openBluetoothAdapter({
success: (res) => {
console.log('蓝牙适配器初始化成功', res)
bluetoothEnabled.value = true
uni.showToast({
title: '蓝牙已开启',
icon: 'success'
})
// 初始化成功后尝试自动重连上次设备
setTimeout(() => {
tryAutoReconnectLastDevice()
}, 500)
},
fail: (err) => {
console.error('蓝牙适配器初始化失败', err)
bluetoothEnabled.value = false
// 只在特定错误码时才提示,避免重复提示权限问题
if (err.errCode === 10001) {
// 蓝牙未开启
uni.showModal({
title: '提示',
content: '请在手机设置中开启蓝牙',
showCancel: false
})
} else if (err.errCode === -1) {
// 初始化失败,但不弹窗(可能是权限问题,已在权限检查时处理)
console.log('蓝牙初始化失败,可能是权限未授予')
}
}
})
}
// 检查位置服务GPS是否开启
const checkLocationService = () => {
return new Promise((resolve, reject) => {
// #ifdef APP-PLUS
const platform = uni.getSystemInfoSync().platform
const system = uni.getSystemInfoSync().system
if (platform === 'android') {
const androidVersion = parseInt(system.replace(/[^0-9]/ig, ''))
// Android 12+ 不需要检查GPS
if (androidVersion >= 12) {
resolve(true)
return
}
// Android 10-11 必须开启GPS否则搜索失败
// 使用 plus.android 检查位置服务状态
try {
const main = plus.android.runtimeMainActivity()
const Context = plus.android.importClass('android.content.Context')
const LocationManager = plus.android.importClass('android.location.LocationManager')
const locationManager = main.getSystemService(Context.LOCATION_SERVICE)
const isGpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
const isNetworkEnabled = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
const isLocationEnabled = isGpsEnabled || isNetworkEnabled
if (!isLocationEnabled) {
reject(false)
} else {
resolve(true)
}
} catch (e) {
console.error('检查GPS状态失败:', e)
// 检查失败时假设已开启
resolve(true)
}
} else {
// iOS 不需要检查
resolve(true)
}
// #endif
// #ifndef APP-PLUS
resolve(true)
// #endif
})
}
// 引导用户开启GPS
const guideToEnableGPS = () => {
const platform = uni.getSystemInfoSync().platform
const system = uni.getSystemInfoSync().system
const androidVersion = parseInt(system.replace(/[^0-9]/ig, ''))
let content = ''
if (androidVersion >= 12) {
content = '检测到您使用的是 Android 12+不需要开启GPS即可使用蓝牙功能。'
} else if (androidVersion >= 10) {
content = `检测到您使用的是 Android ${androidVersion}系统要求必须开启位置服务GPS才能搜索蓝牙设备。
这是 Android 系统的强制要求,即使不会真正定位您的位置。
是否前往设置开启?`
} else {
content = '建议开启位置服务GPS以确保蓝牙搜索功能正常使用。\n\n是否前往设置开启'
}
uni.showModal({
title: androidVersion >= 12 ? '提示' : '需要开启GPS',
content: content,
showCancel: androidVersion < 12,
confirmText: androidVersion >= 12 ? '知道了' : '去设置',
success: (res) => {
if (res.confirm && androidVersion < 12) {
// 跳转到位置设置页面
// #ifdef APP-PLUS
try {
const Intent = plus.android.importClass('android.content.Intent')
const Settings = plus.android.importClass('android.provider.Settings')
const intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)
const main = plus.android.runtimeMainActivity()
main.startActivity(intent)
} catch (e) {
console.error('跳转设置失败:', e)
uni.showToast({
title: '请手动前往设置开启GPS',
icon: 'none',
duration: 2000
})
}
// #endif
}
}
})
}
// 保存上次连接成功的设备信息
const saveLastDevice = (device) => {
try {
const deviceInfo = {
deviceId: device.deviceId,
name: device.name || device.localName || '未命名设备',
localName: device.localName,
RSSI: device.RSSI
}
uni.setStorageSync(LAST_DEVICE_KEY, JSON.stringify(deviceInfo))
console.log('已保存上次连接的设备:', deviceInfo)
} catch (e) {
console.error('保存设备信息失败:', e)
}
}
// 获取上次连接的设备信息
const getLastDevice = () => {
try {
const deviceStr = uni.getStorageSync(LAST_DEVICE_KEY)
if (deviceStr) {
const device = JSON.parse(deviceStr)
console.log('读取到上次连接的设备:', device)
return device
}
} catch (e) {
console.error('读取设备信息失败:', e)
}
return null
}
// 尝试自动重连上次的设备
const tryAutoReconnectLastDevice = async () => {
// 防止重复尝试
if (hasTriedAutoReconnect) {
console.log('已经尝试过自动重连,跳过')
startBluetoothSearch()
return
}
hasTriedAutoReconnect = true
// 获取上次连接的设备
const lastDevice = getLastDevice()
if (!lastDevice) {
console.log('没有上次连接的设备记录,开始正常搜索')
startBluetoothSearch()
return
}
console.log('尝试自动重连上次的设备:', lastDevice.name)
uni.showLoading({
title: `正在连接 ${lastDevice.name}...`,
mask: true
})
// 检查GPS是否开启Android 10-11 必需)
try {
await checkLocationService()
} catch (e) {
uni.hideLoading()
console.log('GPS未开启无法自动重连')
guideToEnableGPS()
return
}
// 开始搜索设备
try {
await new Promise((resolve, reject) => {
uni.startBluetoothDevicesDiscovery({
allowDuplicatesKey: false,
success: resolve,
fail: reject
})
})
console.log('开始搜索上次连接的设备...')
// 等待3秒搜索设备
await new Promise(resolve => setTimeout(resolve, 3000))
// 获取搜索到的设备列表
const devices = await new Promise((resolve, reject) => {
uni.getBluetoothDevices({
success: (res) => resolve(res.devices),
fail: reject
})
})
// 查找上次连接的设备
const targetDevice = devices.find(d => d.deviceId === lastDevice.deviceId)
if (targetDevice) {
console.log('找到上次连接的设备,尝试连接:', targetDevice)
// 尝试连接
try {
await new Promise((resolve, reject) => {
uni.createBLEConnection({
deviceId: targetDevice.deviceId,
timeout: 8000,
success: resolve,
fail: reject
})
})
// 连接成功
console.log('自动重连成功')
connectingDeviceId.value = ''
connectedDeviceId.value = targetDevice.deviceId
connectedDevice.value = targetDevice
// 停止搜索
uni.stopBluetoothDevicesDiscovery()
// 获取蓝牙设备服务
const bleInfo = await getBLEDeviceServices(targetDevice.deviceId)
uni.hideLoading()
uni.showToast({
title: '自动连接成功',
icon: 'success',
duration: 1500
})
// 自动跳转到控制页面
setTimeout(() => {
const url = buildMixerPageUrl(targetDevice, bleInfo)
console.log('🚀 跳转到搅拌器页面:', url)
uni.navigateTo({ url })
}, 1500)
} catch (connectErr) {
console.error('自动重连失败:', connectErr)
uni.hideLoading()
uni.showToast({
title: '自动连接失败,请手动选择',
icon: 'none',
duration: 2000
})
// 连接失败,继续正常搜索流程
setTimeout(() => {
// 设备列表已经有了,直接显示
isSearching.value = true
getBluetoothDevices()
}, 2000)
}
} else {
console.log('未找到上次连接的设备,继续正常搜索')
uni.hideLoading()
uni.showToast({
title: '未找到上次设备',
icon: 'none',
duration: 1500
})
// 继续正常搜索
setTimeout(() => {
isSearching.value = true
getBluetoothDevices()
}, 1500)
}
} catch (err) {
console.error('搜索设备失败:', err)
uni.hideLoading()
uni.showToast({
title: '搜索失败,请手动操作',
icon: 'none',
duration: 2000
})
// 搜索失败,开始正常搜索
setTimeout(() => {
startBluetoothSearch()
}, 2000)
}
}
// 开始搜索蓝牙设备
const startBluetoothSearch = async () => {
if (!bluetoothEnabled.value) {
uni.showToast({
title: '请先开启蓝牙',
icon: 'none'
})
return
}
// 检查GPS是否开启Android 10-11 必需)
try {
await checkLocationService()
} catch (e) {
// GPS未开启引导用户开启
guideToEnableGPS()
return
}
// 清空设备列表
deviceList.value = []
isSearching.value = true
// 开始搜寻附近的蓝牙外围设备
uni.startBluetoothDevicesDiscovery({
allowDuplicatesKey: false,
success: (res) => {
console.log('开始搜索蓝牙设备', res)
uni.showToast({
title: '开始搜索设备',
icon: 'success'
})
// 监听设备发现
getBluetoothDevices()
},
fail: (err) => {
console.error('搜索蓝牙设备失败', err)
isSearching.value = false
// 根据错误码提供详细提示
let errorMsg = '搜索失败,请重试'
if (err.errCode === 10001) {
errorMsg = '搜索失败:未开启蓝牙适配器'
} else if (err.errCode === 10002) {
errorMsg = '搜索失败:未找到蓝牙设备'
} else if (err.errCode === 10004) {
errorMsg = '搜索失败请检查是否已开启位置服务GPS'
// 可能是GPS未开启导致的
guideToEnableGPS()
return
}
uni.showToast({
title: errorMsg,
icon: 'none',
duration: 2500
})
}
})
}
// 停止搜索蓝牙设备
const stopBluetoothSearch = () => {
uni.stopBluetoothDevicesDiscovery({
success: (res) => {
console.log('停止搜索蓝牙设备', res)
isSearching.value = false
uni.showToast({
title: '已停止搜索',
icon: 'success'
})
},
fail: (err) => {
console.error('停止搜索失败', err)
}
})
}
// 获取已发现的蓝牙设备
const getBluetoothDevices = () => {
uni.getBluetoothDevices({
success: (res) => {
console.log('获取到的蓝牙设备', res.devices)
// 显示所有设备,包括未命名设备
deviceList.value = res.devices
},
fail: (err) => {
console.error('获取设备列表失败', err)
}
})
}
// 监听新设备发现
const onBluetoothDeviceFound = () => {
uni.onBluetoothDeviceFound((res) => {
const devices = res.devices
devices.forEach(device => {
// 检查设备是否已存在
const existIndex = deviceList.value.findIndex(d => d.deviceId === device.deviceId)
if (existIndex === -1) {
// 新设备,直接添加
deviceList.value.push(device)
} else {
// 更新已存在设备的信息
deviceList.value[existIndex] = device
}
})
})
}
// 连接蓝牙设备
const connectDevice = (device) => {
// 如果正在连接中,不允许再次连接
if (connectingDeviceId.value) {
uni.showToast({
title: '正在连接中,请稍候',
icon: 'none'
})
return
}
// 如果已经连接了同一个设备,不需要重复连接
if (connectedDeviceId.value === device.deviceId) {
uni.showToast({
title: '该设备已连接',
icon: 'none'
})
return
}
// 如果已经连接了其他设备,先提示用户
if (connectedDeviceId.value && connectedDeviceId.value !== device.deviceId) {
uni.showModal({
title: '提示',
content: '已连接其他设备,是否断开并连接新设备?',
success: (res) => {
if (res.confirm) {
// 先断开当前连接
uni.closeBLEConnection({
deviceId: connectedDeviceId.value,
complete: () => {
// 无论断开成功与否,都清空状态并尝试连接新设备
connectedDeviceId.value = ''
connectedDevice.value = null
// 连接新设备
doConnect(device)
}
})
}
}
})
return
}
// 直接连接
doConnect(device)
}
// 执行连接操作
const doConnect = (device) => {
connectingDeviceId.value = device.deviceId
console.log('开始连接设备:', device)
uni.createBLEConnection({
deviceId: device.deviceId,
timeout: 10000, // 设置10秒超时
success: async (res) => {
console.log('连接成功', res)
connectingDeviceId.value = ''
connectedDeviceId.value = device.deviceId
connectedDevice.value = device
uni.showToast({
title: '连接成功',
icon: 'success'
})
// 停止搜索
stopBluetoothSearch()
// 保存上次连接的设备信息
saveLastDevice(device)
// 获取蓝牙设备服务
const bleInfo = await getBLEDeviceServices(device.deviceId)
// 连接成功后跳转到控制页面
setTimeout(() => {
const url = buildMixerPageUrl(device, bleInfo)
console.log('🚀 跳转到搅拌器页面:', url)
uni.navigateTo({ url })
}, 800)
},
fail: (err) => {
console.error('连接失败', err)
connectingDeviceId.value = ''
// 根据错误码提供更详细的错误信息
let errorMsg = '无法连接到该设备'
if (err.errCode === 10003) {
errorMsg = '连接失败:设备未找到或已关闭'
} else if (err.errCode === 10012) {
errorMsg = '连接失败:连接超时,设备可能距离太远'
} else if (err.errCode === 10004) {
errorMsg = '连接失败:设备不支持连接'
} else if (err.errCode === -1) {
errorMsg = '连接失败:系统错误,请重启蓝牙后重试'
} else if (err.errMsg) {
errorMsg = `连接失败:${err.errMsg}`
}
uni.showModal({
title: '连接失败',
content: `${errorMsg}
错误码: ${err.errCode || '未知'}
提示:
1. 确保设备在可连接范围内
2. 确保设备未被其他应用连接
3. 尝试重启设备蓝牙`,
showCancel: false,
confirmText: '知道了'
})
}
})
}
// 断开蓝牙连接
const disconnectDevice = () => {
if (!connectedDeviceId.value) {
uni.showToast({
title: '没有已连接的设备',
icon: 'none'
})
return
}
const deviceId = connectedDeviceId.value
// 关闭notify监听
if (notifyCharacteristics.value.length > 0) {
notifyCharacteristics.value.forEach(char => {
try {
uni.notifyBLECharacteristicValueChange({
deviceId: deviceId,
serviceId: char.serviceId,
characteristicId: char.uuid,
state: false,
success: () => {
console.log('关闭notify成功:', char.uuid)
},
fail: (err) => {
console.error('关闭notify失败:', char.uuid, err)
}
})
} catch (err) {
console.error('关闭notify异常:', err)
}
})
}
uni.closeBLEConnection({
deviceId: deviceId,
success: (res) => {
console.log('断开连接成功', res)
// 清空连接状态和服务信息
cleanupConnection()
uni.showToast({
title: '已断开连接',
icon: 'success'
})
},
fail: (err) => {
console.error('断开连接失败', err)
// 即使断开失败,也清空状态(可能设备已经离线)
cleanupConnection()
uni.showToast({
title: '断开连接',
icon: 'success'
})
}
})
}
// 清理连接状态
const cleanupConnection = () => {
connectedDeviceId.value = ''
connectedDevice.value = null
services.value = []
characteristics.value = []
notifyCharacteristics.value = []
totalCharacteristicsCount.value = 0
// 断开连接时不清空历史记录,保留数据
}
// 获取蓝牙设备服务
const getBLEDeviceServices = async (deviceId) => {
try {
const res = await new Promise((resolve, reject) => {
uni.getBLEDeviceServices({
deviceId: deviceId,
success: resolve,
fail: reject
})
})
console.log('获取到的服务列表', res.services)
services.value = res.services
// 计算所有服务的特征值总数
let totalCount = 0
let writeCharacteristicId = '' // 可写特征值
let notifyCharacteristicId = '' // 可监听特征值
let mainServiceId = '' // 主服务ID
// 自动获取第一个服务的特征值(用于展示)
if (res.services.length > 0) {
mainServiceId = res.services[0].uuid
const chars = await getBLEDeviceCharacteristicsSync(deviceId, mainServiceId)
characteristics.value = chars
// 遍历所有服务统计特征值总数
for (const service of res.services) {
try {
const serviceChars = await getBLEDeviceCharacteristicsSync(deviceId, service.uuid)
totalCount += serviceChars.length
// 找出可以监听的特征值
serviceChars.forEach(characteristic => {
// 查找可写特征值(优先选择 write
if (characteristic.properties.write && !writeCharacteristicId) {
writeCharacteristicId = characteristic.uuid
if (!mainServiceId) mainServiceId = service.uuid
console.log('找到可写特征值(write):', writeCharacteristicId)
} else if (characteristic.properties.writeNoResponse && !writeCharacteristicId) {
writeCharacteristicId = characteristic.uuid
if (!mainServiceId) mainServiceId = service.uuid
console.log('找到可写特征值(writeNoResponse):', writeCharacteristicId)
}
// 查找可监听特征值
if (characteristic.properties.notify || characteristic.properties.indicate) {
if (!notifyCharacteristicId) {
notifyCharacteristicId = characteristic.uuid
console.log('找到可监听特征值:', notifyCharacteristicId)
}
notifyCharacteristics.value.push({
...characteristic,
serviceId: service.uuid
})
enableNotify(deviceId, service.uuid, characteristic.uuid)
}
})
} catch (err) {
console.error(`获取服务 ${service.uuid} 的特征值失败`, err)
}
}
totalCharacteristicsCount.value = totalCount
console.log(`总共 ${totalCount} 个特征值`)
}
// 返回蓝牙信息(用于跳转)
return {
serviceId: mainServiceId,
characteristicId: writeCharacteristicId,
notifyCharacteristicId: notifyCharacteristicId
}
} catch (err) {
console.error('获取服务列表失败', err)
return {
serviceId: '',
characteristicId: '',
notifyCharacteristicId: ''
}
}
}
// 同步获取特征值不启用notify
const getBLEDeviceCharacteristicsSync = (deviceId, serviceId) => {
return new Promise((resolve, reject) => {
uni.getBLEDeviceCharacteristics({
deviceId: deviceId,
serviceId: serviceId,
success: (res) => {
resolve(res.characteristics)
},
fail: reject
})
})
}
// 预览控制页面(模拟设备数据)
const previewMixerPage = () => {
const fakeDevice = {
deviceId: 'preview-fake-id',
name: '预览测试设备',
localName: 'Preview Device',
RSSI: -50
}
const fakeBleInfo = {
serviceId: 'fake-service-id',
characteristicId: 'fake-char-id',
notifyCharacteristicId: 'fake-notify-id'
}
const url = buildMixerPageUrl(fakeDevice, fakeBleInfo)
console.log('🎨 预览跳转:', url)
uni.navigateTo({ url })
}
// 构造跳转到 mixer 页面的 URL带蓝牙信息
const buildMixerPageUrl = (device, bleInfo) => {
const params = {
deviceId: device.deviceId,
deviceName: encodeURIComponent(device.name || device.localName || '未命名设备')
}
// 添加蓝牙服务和特征值信息
if (bleInfo && bleInfo.serviceId) {
params.serviceId = bleInfo.serviceId
}
if (bleInfo && bleInfo.characteristicId) {
params.characteristicId = bleInfo.characteristicId
}
if (bleInfo && bleInfo.notifyCharacteristicId) {
params.notifyCharacteristicId = bleInfo.notifyCharacteristicId
}
// 拼接 URL
const queryString = Object.keys(params)
.map(key => `${key}=${params[key]}`)
.join('&')
return `/pages/mixer/mixer?${queryString}`
}
// 启用特征值变化监听
const enableNotify = (deviceId, serviceId, characteristicId) => {
// 第一次启用notify时注册监听器
if (!hasRegisteredCharacteristicListener) {
onBLECharacteristicValueChange()
hasRegisteredCharacteristicListener = true
}
uni.notifyBLECharacteristicValueChange({
deviceId: deviceId,
serviceId: serviceId,
characteristicId: characteristicId,
state: true,
success: (res) => {
console.log('启用notify成功', characteristicId)
},
fail: (err) => {
console.error('启用notify失败', err)
}
})
}
// 监听特征值变化(接收数据)
const onBLECharacteristicValueChange = () => {
uni.onBLECharacteristicValueChange((res) => {
console.log('收到蓝牙数据:', res)
console.log('特征值UUID:', res.characteristicId)
// 将 ArrayBuffer 转换为16进制字符串
const hexString = ab2hex(res.value)
console.log('16进制数据:', hexString)
// 尝试转换为文本数据
const textData = ab2str(res.value)
console.log('文本数据:', textData)
// 显示数据的字节长度
console.log('数据长度:', res.value.byteLength, '字节')
// 优先显示可读的文本如果文本不可读则显示16进制
const displayData = textData && textData.trim() ? textData : hexString
// 添加到历史记录包含UUID
addToHistory('receive', displayData, hexString, res.characteristicId)
// 显示接收到的数据
uni.showToast({
title: `收到数据: ${displayData.slice(0, 20)}${displayData.length > 20 ? '...' : ''}`,
icon: 'none',
duration: 2000
})
})
}
// 向蓝牙设备写入数据 - 遍历所有服务查找可写特征值
const writeBLEData = async (data) => {
if (!connectedDeviceId.value) {
uni.showToast({
title: '请先连接设备',
icon: 'none'
})
return
}
if (!services.value || services.value.length === 0) {
uni.showToast({
title: '设备服务未就绪,请稍后重试',
icon: 'none'
})
return
}
uni.showLoading({
title: '正在发送数据...',
mask: true
})
try {
let writeCharacteristic = null
let writeServiceId = null
// 遍历所有服务,查找可写的特征值
for (const service of services.value) {
try {
const chars = await getBLEServiceCharacteristics(connectedDeviceId.value, service.uuid)
// 优先查找支持 write 的特征值
const writableChar = chars.find(c => c.properties.write)
if (writableChar) {
writeCharacteristic = writableChar
writeServiceId = service.uuid
console.log('找到可写特征值(write):', writableChar.uuid, '在服务:', service.uuid)
break
}
// 如果没有 write查找 writeNoResponse
const writeNoResponseChar = chars.find(c => c.properties.writeNoResponse)
if (writeNoResponseChar && !writeCharacteristic) {
writeCharacteristic = writeNoResponseChar
writeServiceId = service.uuid
console.log('找到可写特征值(writeNoResponse):', writeNoResponseChar.uuid, '在服务:', service.uuid)
}
} catch (err) {
console.error('获取服务特征值失败:', service.uuid, err)
}
}
uni.hideLoading()
if (!writeCharacteristic) {
uni.showModal({
title: '无法发送',
content: '该设备没有可写入的特征值\n\n可能原因\n1. 设备不支持写入操作\n2. 设备固件限制\n\n建议查看设备文档或联系设备厂商',
showCancel: false
})
return
}
console.log('准备写入数据:', data)
console.log('写入特征值:', writeCharacteristic.uuid)
console.log('所属服务:', writeServiceId)
// 将字符串转换为 ArrayBuffer
const buffer = str2ab(data)
console.log('转换后的buffer长度:', buffer.byteLength)
console.log('Buffer内容(HEX):', ab2hex(buffer))
// 检查数据长度蓝牙一般限制在20字节以内
if (buffer.byteLength > 20) {
uni.showModal({
title: '数据过长',
content: `数据长度${buffer.byteLength}字节,超过蓝牙限制(20字节)\n\n是否分包发送`,
success: async (res) => {
if (res.confirm) {
await sendDataInChunks(data, writeServiceId, writeCharacteristic.uuid)
}
}
})
return
}
uni.writeBLECharacteristicValue({
deviceId: connectedDeviceId.value,
serviceId: writeServiceId,
characteristicId: writeCharacteristic.uuid,
value: buffer,
success: (res) => {
console.log('写入数据成功', res)
// 添加到历史记录
const hexString = ab2hex(buffer)
addToHistory('send', data, hexString)
uni.showToast({
title: `发送成功: ${data}`,
icon: 'success',
duration: 2000
})
},
fail: (err) => {
console.error('写入数据失败', err)
let errorMsg = '发送失败'
let errorDetail = ''
if (err.errCode === 10008) {
errorMsg = '发送失败:数据格式错误'
errorDetail = '请检查输入的数据格式'
} else if (err.errCode === 10007) {
errorMsg = '发送失败:特征值不支持写入'
errorDetail = '该特征值不支持写入操作'
} else if (err.errCode === 10006) {
errorMsg = '发送失败:设备连接已断开'
errorDetail = '请重新连接设备'
} else if (err.errMsg) {
errorMsg = '发送失败'
errorDetail = err.errMsg
}
uni.showModal({
title: errorMsg,
content: errorDetail + `\n\n错误码: ${err.errCode || '未知'}`,
showCancel: false
})
}
})
} catch (err) {
uni.hideLoading()
console.error('发送数据异常', err)
uni.showToast({
title: '发送失败',
icon: 'none',
duration: 2000
})
}
}
// 分包发送数据
const sendDataInChunks = async (data, serviceId, characteristicId) => {
const buffer = str2ab(data)
const chunkSize = 20 // 每次发送20字节
const chunks = []
// 分割数据
for (let i = 0; i < buffer.byteLength; i += chunkSize) {
const chunk = buffer.slice(i, Math.min(i + chunkSize, buffer.byteLength))
chunks.push(chunk)
}
console.log(`数据分为 ${chunks.length} 包发送`)
uni.showLoading({
title: `发送中 0/${chunks.length}`,
mask: true
})
try {
for (let i = 0; i < chunks.length; i++) {
await new Promise((resolve, reject) => {
uni.writeBLECharacteristicValue({
deviceId: connectedDeviceId.value,
serviceId: serviceId,
characteristicId: characteristicId,
value: chunks[i],
success: () => {
console.log(`${i + 1}/${chunks.length} 包发送成功`)
uni.showLoading({
title: `发送中 ${i + 1}/${chunks.length}`,
mask: true
})
resolve()
},
fail: (err) => {
reject(err)
}
})
})
// 每包之间延迟100ms
if (i < chunks.length - 1) {
await new Promise(resolve => setTimeout(resolve, 100))
}
}
uni.hideLoading()
// 添加到历史记录
const hexString = ab2hex(buffer)
addToHistory('send', data, hexString)
uni.showToast({
title: `分包发送成功 (${chunks.length}包)`,
icon: 'success',
duration: 2000
})
} catch (err) {
uni.hideLoading()
console.error('分包发送失败', err)
uni.showToast({
title: '分包发送失败',
icon: 'none',
duration: 2000
})
}
}
// 读取蓝牙设备数据 - 遍历所有服务和特征值
const readBLEData = async () => {
if (!connectedDeviceId.value || services.value.length === 0) {
uni.showToast({
title: '请先连接设备',
icon: 'none'
})
return
}
// 只清空接收历史,保留发送历史
receiveHistory.value = []
showHistory.value = false
// 开始读取
await performRead()
}
// 执行读取操作
const performRead = async () => {
uni.showLoading({
title: '正在读取数据...',
mask: true
})
let readCount = 0
let totalReadable = 0
let successCount = 0 // 成功返回数据的次数
let failCount = 0 // 读取失败的次数
try {
// 遍历所有服务
for (let i = 0; i < services.value.length; i++) {
const service = services.value[i]
console.log(`正在检查服务 ${i + 1}/${services.value.length}:`, service.uuid)
// 获取该服务的所有特征值
const chars = await getBLEServiceCharacteristics(connectedDeviceId.value, service.uuid)
// 找出所有可读的特征值
const readableChars = chars.filter(c => c.properties.read)
totalReadable += readableChars.length
console.log(`服务 ${service.uuid}${readableChars.length} 个可读特征值`)
// 读取所有可读的特征值
for (const char of readableChars) {
readCount++ // 总数统计(不管成功失败)
try {
await readCharacteristicValue(connectedDeviceId.value, service.uuid, char.uuid)
successCount++
console.log(`成功读取特征值: ${char.uuid}`)
} catch (err) {
failCount++
console.error(`读取特征值失败: ${char.uuid}`, err)
}
// 添加延迟,避免读取过快
await new Promise(resolve => setTimeout(resolve, 100))
}
}
uni.hideLoading()
if (totalReadable === 0) {
uni.showToast({
title: '该设备没有可读特征值\n请等待设备推送数据',
icon: 'none',
duration: 2500
})
readStats.value = null
} else {
// 保存统计信息,不弹窗
readStats.value = {
total: readCount,
success: successCount,
fail: failCount
}
// 自动显示历史记录
setTimeout(() => {
if (dataHistory.value.length > 0) {
showHistory.value = true
}
}, 500)
}
} catch (err) {
uni.hideLoading()
console.error('读取数据失败', err)
uni.showToast({
title: '读取数据失败',
icon: 'none',
duration: 2000
})
}
}
// 获取服务的特征值Promise封装
const getBLEServiceCharacteristics = (deviceId, serviceId) => {
return new Promise((resolve, reject) => {
uni.getBLEDeviceCharacteristics({
deviceId: deviceId,
serviceId: serviceId,
success: (res) => {
resolve(res.characteristics)
},
fail: (err) => {
reject(err)
}
})
})
}
// 读取特征值Promise封装
const readCharacteristicValue = (deviceId, serviceId, characteristicId) => {
return new Promise((resolve, reject) => {
uni.readBLECharacteristicValue({
deviceId: deviceId,
serviceId: serviceId,
characteristicId: characteristicId,
success: (res) => {
resolve(res)
},
fail: (err) => {
reject(err)
}
})
})
}
// ArrayBuffer 转 16进制字符串
const ab2hex = (buffer) => {
const hexArr = Array.prototype.map.call(
new Uint8Array(buffer),
bit => ('00' + bit.toString(16)).slice(-2)
)
return hexArr.join('')
}
// ArrayBuffer 转 字符串
const ab2str = (buffer) => {
try {
return String.fromCharCode.apply(null, new Uint8Array(buffer))
} catch (e) {
return ''
}
}
// 字符串 转 ArrayBuffer支持UTF-8编码
const str2ab = (str) => {
// 使用TextEncoder支持UTF-8编码支持中文
if (typeof TextEncoder !== 'undefined') {
const encoder = new TextEncoder()
return encoder.encode(str).buffer
}
// 备用方案使用charCodeAt仅支持ASCII
const buffer = new ArrayBuffer(str.length)
const dataView = new DataView(buffer)
for (let i = 0; i < str.length; i++) {
dataView.setUint8(i, str.charCodeAt(i) & 0xFF)
}
return buffer
}
// 监听蓝牙连接状态变化
const onBLEConnectionStateChange = () => {
uni.onBLEConnectionStateChange((res) => {
console.log('蓝牙连接状态变化', res)
if (!res.connected && res.deviceId === connectedDeviceId.value) {
cleanupConnection()
uni.showToast({
title: '设备已断开',
icon: 'none'
})
}
})
}
// 发送自定义数据
const sendCustomData = () => {
// 支持中文全角空格的trim
const trimmedData = inputData.value.replace(/^[\s\u3000]+|[\s\u3000]+$/g, '')
if (!trimmedData) {
uni.showToast({
title: '请输入数据',
icon: 'none'
})
return
}
writeBLEData(trimmedData)
inputData.value = '' // 发送后清空输入框
}
// 添加到历史记录
const addToHistory = (type, data, hex, characteristicId = '') => {
const now = new Date()
const time = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`
const record = {
type, // 'send' 或 'receive'
data,
hex,
characteristicId, // 特征值UUID
time,
timestamp: now.getTime()
}
// 根据类型添加到不同的历史记录
if (type === 'send') {
sendHistory.value.unshift(record)
// 最多保留100条发送记录
if (sendHistory.value.length > 100) {
sendHistory.value = sendHistory.value.slice(0, 100)
}
} else if (type === 'receive') {
receiveHistory.value.unshift(record)
// 最多保留100条接收记录
if (receiveHistory.value.length > 100) {
receiveHistory.value = receiveHistory.value.slice(0, 100)
}
}
}
// 切换历史记录显示
const toggleHistory = () => {
if (dataHistory.value.length === 0) {
uni.showToast({
title: '暂无历史记录',
icon: 'none'
})
return
}
showHistory.value = !showHistory.value
}
// 清空历史记录
const clearHistory = () => {
uni.showModal({
title: '确认',
content: '确定要清空所有历史记录吗?',
success: (res) => {
if (res.confirm) {
sendHistory.value = []
receiveHistory.value = []
showHistory.value = false
uni.showToast({
title: '已清空',
icon: 'success'
})
}
}
})
}
// 监听蓝牙适配器状态变化
const onBluetoothAdapterStateChange = () => {
uni.onBluetoothAdapterStateChange((res) => {
console.log('蓝牙适配器状态变化', res)
bluetoothEnabled.value = res.available
if (!res.available) {
isSearching.value = false
uni.showToast({
title: '蓝牙已关闭',
icon: 'none'
})
}
})
}
// 页面加载时初始化
onMounted(() => {
// 判断是否为H5环境
// #ifdef H5
uni.showModal({
title: '提示',
content: 'H5平台不支持蓝牙功能请使用App或小程序版本',
showCancel: false,
confirmText: '我知道了'
})
bluetoothEnabled.value = false
// #endif
// #ifndef H5
initBluetooth()
onBluetoothDeviceFound()
onBLEConnectionStateChange()
onBluetoothAdapterStateChange()
// #endif
})
// 页面卸载时清理
const cleanup = () => {
// 移除监听器
if (hasRegisteredCharacteristicListener) {
uni.offBLECharacteristicValueChange()
hasRegisteredCharacteristicListener = false
}
uni.offBluetoothDeviceFound()
uni.offBLEConnectionStateChange()
uni.offBluetoothAdapterStateChange()
// 停止搜索
if (isSearching.value) {
stopBluetoothSearch()
}
// 断开连接
if (connectedDeviceId.value) {
disconnectDevice()
}
// 关闭蓝牙适配器
uni.closeBluetoothAdapter({
success: (res) => {
console.log('关闭蓝牙适配器成功', res)
},
fail: (err) => {
console.error('关闭蓝牙适配器失败', err)
}
})
}
onUnmounted(() => {
// #ifndef H5
cleanup()
// #endif
})
</script>
<style lang="scss" scoped>
page {
width: 100%;
height: 100%;
overflow: hidden;
background-color: #667eea;
}
.bluetooth-page {
width: 100%;
height: 100vh;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
overflow: hidden;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.bluetooth-container {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding-bottom: 20rpx;
}
.search-status {
background: rgba(255, 255, 255, 0.95);
padding: 30rpx;
margin: 20rpx;
border-radius: 20rpx;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1);
.status-row {
display: flex;
justify-content: space-between;
}
.status-item {
display: flex;
align-items: center;
.status-label {
font-size: 28rpx;
color: #666;
}
.status-value {
font-size: 28rpx;
font-weight: 600;
&.status-on {
color: #52c41a;
}
&.status-off {
color: #999;
}
}
}
}
.action-buttons {
display: flex;
padding: 0 20rpx;
margin: 20rpx 0;
:deep(.u-button) {
flex: 1;
height: 88rpx;
border-radius: 16rpx;
font-size: 28rpx;
font-weight: 600;
}
}
.device-list {
background: rgba(255, 255, 255, 0.95);
margin: 20rpx;
border-radius: 20rpx;
padding: 30rpx;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1);
.list-header {
display: flex;
align-items: center;
margin-bottom: 20rpx;
padding-bottom: 20rpx;
border-bottom: 2rpx solid rgba(102, 126, 234, 0.1);
.header-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.device-count {
font-size: 28rpx;
color: #999;
margin-left: 10rpx;
}
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80rpx 0;
.empty-icon {
width: 120rpx;
height: 120rpx;
background: linear-gradient(135deg, #2196F3 0%, #1976D2 100%);
border-radius: 60rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(33, 150, 243, 0.4);
margin-bottom: 30rpx;
.bluetooth-img {
width: 60rpx;
height: 60rpx;
}
}
.empty-text {
font-size: 28rpx;
color: #999;
margin-top: 20rpx;
}
}
.device-items {
.device-wrapper {
margin-bottom: 20rpx;
&:last-child {
margin-bottom: 0;
}
}
.device-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx;
background: linear-gradient(135deg, #f8f9ff 0%, #ffffff 100%);
border-radius: 16rpx;
border: 2rpx solid rgba(102, 126, 234, 0.1);
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
&:active {
transform: scale(0.98);
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
}
.device-info {
flex: 1;
min-width: 0; // 允许内容缩小
.device-name {
display: flex;
align-items: flex-start; // 顶部对齐,适配多行文本
margin-bottom: 12rpx;
.device-icon {
width: 64rpx;
height: 64rpx;
background: linear-gradient(135deg, #2196F3 0%, #1976D2 100%);
border-radius: 32rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
box-shadow: 0 4rpx 12rpx rgba(33, 150, 243, 0.3);
margin-top: 4rpx; // 与文本顶部对齐
.bluetooth-img-small {
width: 32rpx;
height: 32rpx;
}
}
.name-text {
flex: 1;
min-width: 0; // 允许文本缩小
font-size: 30rpx;
font-weight: 600;
color: #333;
margin-left: 12rpx;
word-wrap: break-word; // 允许单词内换行
word-break: break-all; // 允许任意位置换行
line-height: 1.4; // 设置行高
}
}
.device-details {
display: flex;
flex-direction: column;
padding-left: 76rpx; // 与设备名称对齐
.detail-text {
font-size: 24rpx;
color: #999;
line-height: 36rpx;
word-wrap: break-word;
word-break: break-all;
}
}
}
.device-action {
flex-shrink: 0; // 防止按钮区域被压缩
margin-left: 16rpx; // 与设备信息保持间距
align-self: flex-start; // 顶部对齐
.connected-badge {
background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
padding: 12rpx 24rpx;
border-radius: 30rpx;
box-shadow: 0 4rpx 12rpx rgba(82, 196, 26, 0.3);
.badge-text {
font-size: 24rpx;
color: #fff;
font-weight: 600;
}
}
.connecting-badge {
padding: 12rpx 24rpx;
background: rgba(24, 144, 255, 0.1);
border-radius: 30rpx;
.connecting-text {
font-size: 24rpx;
color: #1890ff;
font-weight: 600;
}
}
}
}
// 已连接设备的详细信息(嵌入式)
.device-connected-detail {
margin-top: 16rpx;
padding: 20rpx;
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
border-radius: 12rpx;
border: 2rpx solid rgba(14, 165, 233, 0.2);
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
padding-bottom: 12rpx;
border-bottom: 1rpx solid rgba(14, 165, 233, 0.15);
.detail-title {
font-size: 28rpx;
font-weight: 600;
color: #0284c7;
}
}
.detail-content {
.detail-row {
display: flex;
align-items: center;
margin-bottom: 10rpx;
&:last-child {
margin-bottom: 0;
}
.detail-label {
font-size: 26rpx;
color: #666;
min-width: 140rpx;
}
.detail-value {
font-size: 26rpx;
color: #333;
font-weight: 500;
}
}
}
.data-section {
margin-top: 16rpx;
padding-top: 16rpx;
border-top: 1rpx solid rgba(14, 165, 233, 0.15);
.section-title {
font-size: 28rpx;
font-weight: 600;
color: #0284c7;
margin-bottom: 16rpx;
}
.input-area {
display: flex;
align-items: center;
margin-bottom: 16rpx;
.data-input {
flex: 1;
min-width: 0;
height: 80rpx;
padding: 16rpx 24rpx;
background: rgba(255, 255, 255, 0.98);
border-radius: 12rpx;
border: 3rpx solid rgba(14, 165, 233, 0.5);
font-size: 30rpx;
color: #333;
box-sizing: border-box;
&:focus {
border-color: rgba(14, 165, 233, 1);
background: rgba(255, 255, 255, 1);
box-shadow: 0 0 0 4rpx rgba(14, 165, 233, 0.1);
}
}
}
.data-actions {
display: flex;
justify-content: space-between;
gap: 12rpx;
:deep(.u-button) {
flex: 1;
height: 64rpx;
font-size: 24rpx;
}
}
}
// 内联历史记录
.history-section-inline {
margin-top: 16rpx;
padding-top: 16rpx;
border-top: 1rpx solid rgba(14, 165, 233, 0.15);
.history-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12rpx;
.history-title {
font-size: 28rpx;
font-weight: 600;
color: #0284c7;
}
.read-stats {
display: flex;
flex-direction: column;
align-items: flex-end;
.stats-text {
font-size: 22rpx;
color: #0284c7;
font-weight: 600;
}
.stats-detail {
font-size: 20rpx;
color: #f59e0b;
margin-top: 2rpx;
}
}
}
.history-list {
max-height: 400rpx;
.history-item {
padding: 16rpx;
margin-bottom: 12rpx;
background: rgba(255, 255, 255, 0.9);
border-radius: 10rpx;
border: 1rpx solid rgba(14, 165, 233, 0.15);
&:last-child {
margin-bottom: 0;
}
.history-info {
display: flex;
align-items: center;
margin-bottom: 10rpx;
.history-type {
padding: 4rpx 12rpx;
border-radius: 16rpx;
font-size: 20rpx;
font-weight: 600;
margin-right: 12rpx;
&.send {
background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
color: #fff;
}
&.receive {
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%);
color: #fff;
}
}
.history-time {
font-size: 20rpx;
color: #999;
}
}
.history-uuid {
display: flex;
margin-bottom: 8rpx;
.uuid-label {
font-size: 20rpx;
color: #8b5cf6;
width: 70rpx;
font-weight: 600;
}
.uuid-value {
flex: 1;
font-size: 20rpx;
color: #8b5cf6;
word-break: break-all;
font-family: monospace;
}
}
.history-data {
display: flex;
margin-bottom: 6rpx;
.data-label {
font-size: 22rpx;
color: #666;
width: 70rpx;
font-weight: 600;
}
.data-value {
flex: 1;
font-size: 22rpx;
color: #333;
word-break: break-all;
}
}
.history-hex {
display: flex;
.hex-label {
font-size: 20rpx;
color: #999;
width: 70rpx;
}
.hex-value {
flex: 1;
font-size: 20rpx;
color: #999;
word-break: break-all;
font-family: monospace;
}
}
}
}
}
}
}
}
</style>