口才和会议场景
This commit is contained in:
645
pages/bluetooth-recorder/bluetooth-recorder.vue
Normal file
645
pages/bluetooth-recorder/bluetooth-recorder.vue
Normal file
@@ -0,0 +1,645 @@
|
||||
<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 '@/common/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>
|
||||
|
||||
Reference in New Issue
Block a user