Files
smartDriveEEUniApp/pages-subpackage/bluetooth-recorder/bluetooth-recorder.vue
2026-02-01 10:55:35 +08:00

646 lines
15 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-recorder-page">
<!-- #ifdef APP -->
<statusBar></statusBar>
<!-- #endif -->
<view class="header">
<view class="header-title">蓝牙录音设备</view>
<view class="header-status" :class="{ connected: isConnected }">
{{ isConnected ? '已连接' : '未连接' }}
</view>
</view>
<!-- 设备搜索区域 -->
<view class="section">
<view class="section-title">设备搜索</view>
<view class="action-buttons">
<button
class="btn btn-primary"
:disabled="isScanning || isConnected"
@click="handleStartScan"
>
{{ isScanning ? '搜索中...' : '开始搜索' }}
</button>
<button
class="btn btn-secondary"
:disabled="!isScanning"
@click="handleStopScan"
>
停止搜索
</button>
</view>
</view>
<!-- 设备列表 -->
<view class="section" v-if="deviceList.length > 0">
<view class="section-title">发现的设备 ({{ deviceList.length }})</view>
<scroll-view class="device-list" scroll-y>
<view
v-for="device in deviceList"
:key="device.deviceId"
class="device-item"
:class="{ connected: connectedDeviceId === device.deviceId }"
@click="handleConnectDevice(device)"
>
<view class="device-info">
<text class="device-name">{{ device.name || '未知设备' }}</text>
<text class="device-id">{{ device.deviceId }}</text>
<text class="device-rssi">信号强度: {{ device.RSSI }} dBm</text>
</view>
<view class="device-action">
<text v-if="connectedDeviceId === device.deviceId" class="status-text connected">已连接</text>
<text v-else class="status-text">点击连接</text>
</view>
</view>
</scroll-view>
</view>
<!-- 连接状态 -->
<view class="section" v-if="isConnected">
<view class="section-title">连接状态</view>
<view class="connection-info">
<text class="info-item">设备ID: {{ connectedDeviceId }}</text>
<text class="info-item">接收状态: {{ isReceiving ? '接收中' : '未接收' }}</text>
<text class="info-item">数据包数: {{ audioDataCount }}</text>
<text class="info-item">缓冲区大小: {{ audioBufferSize }} KB</text>
</view>
<view class="action-buttons">
<button
class="btn btn-danger"
@click="handleDisconnect"
>
断开连接
</button>
<button
class="btn btn-secondary"
@click="handleClearBuffer"
>
清空缓冲区
</button>
</view>
</view>
<!-- 音频数据操作 -->
<view class="section" v-if="isConnected && audioDataCount > 0">
<view class="section-title">音频数据</view>
<view class="action-buttons">
<button
class="btn btn-primary"
@click="handleSaveAudio"
>
保存录音文件
</button>
<button
class="btn btn-secondary"
@click="handleUploadAudio"
>
上传到服务器
</button>
</view>
</view>
<!-- 日志区域 -->
<view class="section">
<view class="section-title">操作日志</view>
<scroll-view class="log-list" scroll-y scroll-top="{{ logScrollTop }}">
<view
v-for="(log, index) in logs"
:key="index"
class="log-item"
:class="log.type"
>
<text class="log-time">{{ log.time }}</text>
<text class="log-message">{{ log.message }}</text>
</view>
</scroll-view>
</view>
</view>
</template>
<script>
import bluetoothRecorder from './utils/bluetooth.js'
// #ifdef APP
import statusBar from '@/uni_modules/uni-nav-bar/components/uni-nav-bar/uni-status-bar'
// #endif
import { post } from '@/common/request.js'
import { getApiUrl } from '@/common/config.js'
export default {
// #ifdef APP
components: {
statusBar
},
// #endif
data() {
return {
isScanning: false,
isConnected: false,
isReceiving: false,
deviceList: [],
connectedDeviceId: null,
audioDataCount: 0,
audioBufferSize: 0,
logs: [],
logScrollTop: 0
}
},
onLoad() {
this.initBluetooth()
},
onUnload() {
this.cleanup()
},
methods: {
/**
* 初始化蓝牙
*/
async initBluetooth() {
try {
// 设置回调函数
bluetoothRecorder.onDeviceFound = (device) => {
this.addLog('info', `发现设备: ${device.name || device.deviceId}`)
this.deviceList = bluetoothRecorder.getDeviceList()
}
bluetoothRecorder.onDeviceConnected = (deviceId) => {
this.addLog('success', `设备连接成功: ${deviceId}`)
this.isConnected = true
this.connectedDeviceId = deviceId
}
bluetoothRecorder.onDeviceDisconnected = (deviceId) => {
this.addLog('warning', `设备断开连接: ${deviceId}`)
this.isConnected = false
this.isReceiving = false
this.connectedDeviceId = null
}
bluetoothRecorder.onAudioDataReceived = (data) => {
this.audioDataCount++
// 估算缓冲区大小Base64编码后约为原数据的4/3
const estimatedSize = (data.data.length * 3) / 4 / 1024
this.audioBufferSize = Math.round(estimatedSize * 100) / 100
this.isReceiving = true
}
bluetoothRecorder.onError = (error) => {
this.addLog('error', error.message || '发生错误')
uni.showToast({
title: error.message || '操作失败',
icon: 'none'
})
}
// 初始化蓝牙适配器
await bluetoothRecorder.initBluetoothAdapter()
this.addLog('success', '蓝牙适配器初始化成功')
} catch (error) {
this.addLog('error', `初始化失败: ${error.message || error.errMsg}`)
uni.showModal({
title: '提示',
content: '蓝牙初始化失败,请检查手机蓝牙是否开启',
showCancel: false
})
}
},
/**
* 开始搜索设备
*/
async handleStartScan() {
try {
this.deviceList = []
bluetoothRecorder.clearDeviceList()
await bluetoothRecorder.startScan({
allowDuplicatesKey: false,
interval: 0
})
this.isScanning = true
this.addLog('info', '开始搜索蓝牙设备...')
// 10秒后自动停止搜索
setTimeout(() => {
if (this.isScanning) {
this.handleStopScan()
}
}, 10000)
} catch (error) {
this.addLog('error', `搜索失败: ${error.message || error.errMsg}`)
}
},
/**
* 停止搜索
*/
async handleStopScan() {
try {
await bluetoothRecorder.stopScan()
this.isScanning = false
this.addLog('info', '停止搜索')
} catch (error) {
this.addLog('error', `停止搜索失败: ${error.message || error.errMsg}`)
}
},
/**
* 连接设备
*/
async handleConnectDevice(device) {
if (this.connectedDeviceId === device.deviceId) {
return
}
try {
uni.showLoading({ title: '连接中...' })
await bluetoothRecorder.connectDevice(device.deviceId)
this.addLog('success', `正在连接设备: ${device.name || device.deviceId}`)
} catch (error) {
this.addLog('error', `连接失败: ${error.message || error.errMsg}`)
} finally {
uni.hideLoading()
}
},
/**
* 断开连接
*/
async handleDisconnect() {
try {
uni.showLoading({ title: '断开中...' })
await bluetoothRecorder.disconnectDevice()
this.isConnected = false
this.isReceiving = false
this.connectedDeviceId = null
this.audioDataCount = 0
this.audioBufferSize = 0
this.addLog('info', '已断开连接')
} catch (error) {
this.addLog('error', `断开失败: ${error.message || error.errMsg}`)
} finally {
uni.hideLoading()
}
},
/**
* 清空缓冲区
*/
handleClearBuffer() {
bluetoothRecorder.clearAudioBuffer()
this.audioDataCount = 0
this.audioBufferSize = 0
this.addLog('info', '已清空音频缓冲区')
},
/**
* 保存录音文件
*/
async handleSaveAudio() {
try {
const audioData = bluetoothRecorder.mergeAudioData()
if (!audioData) {
uni.showToast({
title: '没有可保存的音频数据',
icon: 'none'
})
return
}
// 将Base64转换为ArrayBuffer
const arrayBuffer = uni.base64ToArrayBuffer(audioData)
// 生成文件名
const timestamp = new Date().getTime()
const fileName = `bluetooth_recording_${timestamp}.pcm`
// #ifdef APP-PLUS
// 保存到本地文件系统
const fs = uni.getFileSystemManager()
const filePath = `${uni.env.USER_DATA_PATH}/${fileName}`
fs.writeFile({
filePath: filePath,
data: arrayBuffer,
success: () => {
this.addLog('success', `文件已保存: ${filePath}`)
uni.showToast({
title: '保存成功',
icon: 'success'
})
},
fail: (err) => {
this.addLog('error', `保存失败: ${err.errMsg}`)
uni.showToast({
title: '保存失败',
icon: 'none'
})
}
})
// #endif
// #ifndef APP-PLUS
uni.showToast({
title: '当前平台不支持文件保存',
icon: 'none'
})
// #endif
} catch (error) {
this.addLog('error', `保存失败: ${error.message || error.errMsg}`)
uni.showToast({
title: '保存失败',
icon: 'none'
})
}
},
/**
* 上传音频到服务器
*/
async handleUploadAudio() {
try {
const audioData = bluetoothRecorder.mergeAudioData()
if (!audioData) {
uni.showToast({
title: '没有可上传的音频数据',
icon: 'none'
})
return
}
uni.showLoading({ title: '上传中...' })
// 将Base64转换为ArrayBuffer
const arrayBuffer = uni.base64ToArrayBuffer(audioData)
// 创建FormData
const formData = {
audio: arrayBuffer,
fileName: `bluetooth_recording_${new Date().getTime()}.pcm`,
deviceId: this.connectedDeviceId
}
// 上传到服务器需要根据实际API调整
const url = getApiUrl('/api/audioManagement/uploadBluetoothAudio')
const res = await post(url, formData, {
header: {
'Content-Type': 'multipart/form-data'
}
})
if (res.statusCode === 200 && res.data && res.data.success) {
this.addLog('success', '音频上传成功')
uni.showToast({
title: '上传成功',
icon: 'success'
})
} else {
throw new Error(res.data?.message || '上传失败')
}
} catch (error) {
this.addLog('error', `上传失败: ${error.message || error.errMsg}`)
uni.showToast({
title: '上传失败',
icon: 'none'
})
} finally {
uni.hideLoading()
}
},
/**
* 添加日志
*/
addLog(type, message) {
const time = new Date().toLocaleTimeString()
this.logs.unshift({
type,
message,
time
})
// 限制日志数量
if (this.logs.length > 50) {
this.logs = this.logs.slice(0, 50)
}
// 滚动到底部
this.$nextTick(() => {
this.logScrollTop = 9999
})
},
/**
* 清理资源
*/
async cleanup() {
try {
if (this.isConnected) {
await bluetoothRecorder.disconnectDevice()
}
if (this.isScanning) {
await bluetoothRecorder.stopScan()
}
await bluetoothRecorder.closeBluetoothAdapter()
} catch (error) {
console.error('清理资源失败', error)
}
}
}
}
</script>
<style scoped>
.bluetooth-recorder-page {
min-height: 100vh;
background: #f5f5f5;
padding-bottom: 20rpx;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
padding: 40rpx 30rpx 30rpx;
display: flex;
justify-content: space-between;
align-items: center;
}
.header-title {
font-size: 36rpx;
font-weight: bold;
}
.header-status {
font-size: 28rpx;
padding: 8rpx 20rpx;
background: rgba(255, 255, 255, 0.2);
border-radius: 20rpx;
}
.header-status.connected {
background: rgba(76, 175, 80, 0.8);
}
.section {
background: #fff;
margin: 20rpx 30rpx;
padding: 30rpx;
border-radius: 16rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.section-title {
font-size: 32rpx;
font-weight: bold;
margin-bottom: 20rpx;
color: #333;
}
.action-buttons {
display: flex;
gap: 20rpx;
}
.btn {
flex: 1;
padding: 20rpx;
border-radius: 8rpx;
font-size: 28rpx;
border: none;
}
.btn-primary {
background: #667eea;
color: #fff;
}
.btn-secondary {
background: #e0e0e0;
color: #666;
}
.btn-danger {
background: #f44336;
color: #fff;
}
.btn:disabled {
opacity: 0.5;
}
.device-list {
max-height: 600rpx;
}
.device-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx;
margin-bottom: 20rpx;
background: #f9f9f9;
border-radius: 12rpx;
border: 2rpx solid transparent;
}
.device-item.connected {
border-color: #667eea;
background: #f0f4ff;
}
.device-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 8rpx;
}
.device-name {
font-size: 30rpx;
font-weight: bold;
color: #333;
}
.device-id {
font-size: 24rpx;
color: #999;
}
.device-rssi {
font-size: 24rpx;
color: #666;
}
.device-action {
padding: 10rpx 20rpx;
}
.status-text {
font-size: 26rpx;
color: #999;
}
.status-text.connected {
color: #667eea;
font-weight: bold;
}
.connection-info {
display: flex;
flex-direction: column;
gap: 12rpx;
margin-bottom: 20rpx;
}
.info-item {
font-size: 28rpx;
color: #666;
}
.log-list {
max-height: 400rpx;
background: #f9f9f9;
border-radius: 8rpx;
padding: 20rpx;
}
.log-item {
display: flex;
gap: 20rpx;
padding: 12rpx 0;
border-bottom: 1rpx solid #eee;
font-size: 24rpx;
}
.log-item:last-child {
border-bottom: none;
}
.log-time {
color: #999;
min-width: 120rpx;
}
.log-message {
flex: 1;
color: #333;
}
.log-item.success .log-message {
color: #4caf50;
}
.log-item.error .log-message {
color: #f44336;
}
.log-item.warning .log-message {
color: #ff9800;
}
.log-item.info .log-message {
color: #2196f3;
}
</style>