|
|
@ -10,22 +10,35 @@ |
|
|
|
<text>录制语音下单</text> |
|
|
|
</view> |
|
|
|
<view class="voice-upload"> |
|
|
|
<view class="long-speak"> |
|
|
|
<view class="long-speak" |
|
|
|
@touchstart="startRecord" |
|
|
|
@touchend="stopRecord" |
|
|
|
@touchcancel="cancelRecord" |
|
|
|
:class="{'recording': isRecording}"> |
|
|
|
<uv-icon name="mic" size="45rpx" color="#DC2828"></uv-icon> |
|
|
|
<text>长按可说话</text> |
|
|
|
<text>{{isRecording ? '松开结束录音' : '长按可说话'}}</text> |
|
|
|
</view> |
|
|
|
<view class="recording-file"> |
|
|
|
<view class="recording-status" v-if="isRecording"> |
|
|
|
<text>正在录音中...</text> |
|
|
|
<view class="recording-time">{{recordingTime}}s</view> |
|
|
|
</view> |
|
|
|
<!-- 录音波形效果 --> |
|
|
|
<view class="voice-wave" v-if="isRecording"> |
|
|
|
<view class="wave-item" v-for="(item, index) in waveItems" :key="index" |
|
|
|
:style="{height: item + 'rpx'}"></view> |
|
|
|
</view> |
|
|
|
<view class="recording-file" v-if="audioPath"> |
|
|
|
<view class="file"> |
|
|
|
<image src="../static/order/1.png" mode="" class="record"></image> |
|
|
|
<image src="../static/order/2.png" mode="" class="file-start"></image> |
|
|
|
<image src="../static/order/3.png" mode="" class="file-delete"></image> |
|
|
|
<image src="../static/order/2.png" mode="" class="file-start" @click="playVoice"></image> |
|
|
|
<image src="../static/order/3.png" mode="" class="file-delete" @click="deleteVoice"></image> |
|
|
|
<view class="file-top"> |
|
|
|
<p>录音文件01.mp3</p> |
|
|
|
<p style="color: #A6ADBA;">12MB</p> |
|
|
|
<p>{{audioName}}</p> |
|
|
|
<p style="color: #A6ADBA;">{{audioSize}}</p> |
|
|
|
</view> |
|
|
|
<view class="file-bottom"> |
|
|
|
<view class="schedule"></view> |
|
|
|
<text>100%</text> |
|
|
|
<view class="schedule" :style="{width: uploadProgress + '%'}"></view> |
|
|
|
<text>{{uploadProgress}}%</text> |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
</view> |
|
|
@ -34,7 +47,7 @@ |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
<view class="fast-order"> |
|
|
|
<view class="voice-button" @click="$utils.redirectTo('/pages_order/order/firmOrder')"> |
|
|
|
<view class="voice-button" @click="submitVoiceOrder" :style="audioPath ? '' : 'opacity: 0.5;'"> |
|
|
|
<text>快捷下单</text> |
|
|
|
</view> |
|
|
|
</view> |
|
|
@ -45,8 +58,452 @@ |
|
|
|
export default { |
|
|
|
data() { |
|
|
|
return { |
|
|
|
|
|
|
|
recorderManager: null, // 录音管理器 |
|
|
|
innerAudioContext: null, // 音频播放器 |
|
|
|
isRecording: false, // 是否正在录音 |
|
|
|
isPlaying: false, // 是否正在播放 |
|
|
|
audioPath: '', // 录音文件路径 |
|
|
|
audioName: '录音文件01.mp3', // 录音文件名称 |
|
|
|
audioSize: '0KB', // 录音文件大小 |
|
|
|
uploadProgress: 100, // 上传进度 |
|
|
|
recordStartTime: 0, // 录音开始时间 |
|
|
|
recordTimeout: null, // 录音超时计时器 |
|
|
|
recordMaxDuration: 60000, // 最大录音时长(毫秒) |
|
|
|
recordingTime: 0, // 当前录音时长(秒) |
|
|
|
recordTimer: null, // 录音计时器 |
|
|
|
isUploading: false, // 是否正在上传 |
|
|
|
recognitionResult: null, // 语音识别结果 |
|
|
|
waveTimer: null, // 波形动画计时器 |
|
|
|
waveItems: [], // 波形数据 |
|
|
|
audioUrl: '', // 上传后的音频URL |
|
|
|
}; |
|
|
|
}, |
|
|
|
onLoad() { |
|
|
|
// 初始化录音管理器 |
|
|
|
this.recorderManager = uni.getRecorderManager(); |
|
|
|
|
|
|
|
// 初始化波形数据 |
|
|
|
this.initWaveItems(); |
|
|
|
|
|
|
|
// 监听录音开始事件 |
|
|
|
this.recorderManager.onStart(() => { |
|
|
|
console.log('录音开始'); |
|
|
|
this.isRecording = true; |
|
|
|
this.recordStartTime = Date.now(); |
|
|
|
this.recordingTime = 0; |
|
|
|
|
|
|
|
// 开始计时 |
|
|
|
this.recordTimer = setInterval(() => { |
|
|
|
this.recordingTime = Math.floor((Date.now() - this.recordStartTime) / 1000); |
|
|
|
}, 1000); |
|
|
|
|
|
|
|
// 开始波形动画 |
|
|
|
this.startWaveAnimation(); |
|
|
|
|
|
|
|
// 设置最大录音时长 |
|
|
|
this.recordTimeout = setTimeout(() => { |
|
|
|
if (this.isRecording) { |
|
|
|
this.stopRecord(); |
|
|
|
} |
|
|
|
}, this.recordMaxDuration); |
|
|
|
}); |
|
|
|
|
|
|
|
// 监听录音结束事件 |
|
|
|
this.recorderManager.onStop((res) => { |
|
|
|
console.log('录音结束', res); |
|
|
|
this.isRecording = false; |
|
|
|
if (this.recordTimeout) { |
|
|
|
clearTimeout(this.recordTimeout); |
|
|
|
this.recordTimeout = null; |
|
|
|
} |
|
|
|
|
|
|
|
if (this.recordTimer) { |
|
|
|
clearInterval(this.recordTimer); |
|
|
|
this.recordTimer = null; |
|
|
|
} |
|
|
|
|
|
|
|
// 停止波形动画 |
|
|
|
this.stopWaveAnimation(); |
|
|
|
|
|
|
|
if (res.tempFilePath) { |
|
|
|
this.audioPath = res.tempFilePath; |
|
|
|
// 计算录音文件大小并格式化 |
|
|
|
this.formatFileSize(res.fileSize || 0); |
|
|
|
// 生成录音文件名 |
|
|
|
this.audioName = '录音文件' + this.formatDate(new Date()) + '.mp3'; |
|
|
|
|
|
|
|
// 如果时长过短,提示用户 |
|
|
|
if (this.recordingTime < 1) { |
|
|
|
uni.showToast({ |
|
|
|
title: '录音时间太短,请重新录制', |
|
|
|
icon: 'none' |
|
|
|
}); |
|
|
|
this.audioPath = ''; |
|
|
|
} |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
// 监听录音错误事件 |
|
|
|
this.recorderManager.onError((err) => { |
|
|
|
console.error('录音错误', err); |
|
|
|
uni.showToast({ |
|
|
|
title: '录音失败: ' + err.errMsg, |
|
|
|
icon: 'none' |
|
|
|
}); |
|
|
|
this.isRecording = false; |
|
|
|
if (this.recordTimeout) { |
|
|
|
clearTimeout(this.recordTimeout); |
|
|
|
this.recordTimeout = null; |
|
|
|
} |
|
|
|
|
|
|
|
if (this.recordTimer) { |
|
|
|
clearInterval(this.recordTimer); |
|
|
|
this.recordTimer = null; |
|
|
|
} |
|
|
|
|
|
|
|
// 停止波形动画 |
|
|
|
this.stopWaveAnimation(); |
|
|
|
}); |
|
|
|
|
|
|
|
// 初始化音频播放器 |
|
|
|
this.innerAudioContext = uni.createInnerAudioContext(); |
|
|
|
|
|
|
|
// 监听播放结束事件 |
|
|
|
this.innerAudioContext.onEnded(() => { |
|
|
|
console.log('播放结束'); |
|
|
|
this.isPlaying = false; |
|
|
|
}); |
|
|
|
|
|
|
|
// 监听播放错误事件 |
|
|
|
this.innerAudioContext.onError((err) => { |
|
|
|
console.error('播放错误', err); |
|
|
|
uni.showToast({ |
|
|
|
title: '播放失败', |
|
|
|
icon: 'none' |
|
|
|
}); |
|
|
|
this.isPlaying = false; |
|
|
|
}); |
|
|
|
}, |
|
|
|
onUnload() { |
|
|
|
// 销毁录音管理器和音频播放器 |
|
|
|
if (this.innerAudioContext) { |
|
|
|
this.innerAudioContext.destroy(); |
|
|
|
} |
|
|
|
|
|
|
|
if (this.recordTimeout) { |
|
|
|
clearTimeout(this.recordTimeout); |
|
|
|
this.recordTimeout = null; |
|
|
|
} |
|
|
|
|
|
|
|
if (this.recordTimer) { |
|
|
|
clearInterval(this.recordTimer); |
|
|
|
this.recordTimer = null; |
|
|
|
} |
|
|
|
|
|
|
|
// 停止波形动画 |
|
|
|
this.stopWaveAnimation(); |
|
|
|
}, |
|
|
|
methods: { |
|
|
|
// 初始化波形数据 |
|
|
|
initWaveItems() { |
|
|
|
const itemCount = 16; // 波形柱状图数量 |
|
|
|
this.waveItems = Array(itemCount).fill(10); // 初始高度10rpx |
|
|
|
}, |
|
|
|
|
|
|
|
// 开始波形动画 |
|
|
|
startWaveAnimation() { |
|
|
|
// 停止可能已存在的动画 |
|
|
|
this.stopWaveAnimation(); |
|
|
|
|
|
|
|
// 随机生成波形高度 |
|
|
|
this.waveTimer = setInterval(() => { |
|
|
|
const newWaveItems = []; |
|
|
|
for (let i = 0; i < this.waveItems.length; i++) { |
|
|
|
// 生成10-60之间的随机高度 |
|
|
|
newWaveItems.push(Math.floor(Math.random() * 50) + 10); |
|
|
|
} |
|
|
|
this.waveItems = newWaveItems; |
|
|
|
}, 100); |
|
|
|
}, |
|
|
|
|
|
|
|
// 停止波形动画 |
|
|
|
stopWaveAnimation() { |
|
|
|
if (this.waveTimer) { |
|
|
|
clearInterval(this.waveTimer); |
|
|
|
this.waveTimer = null; |
|
|
|
} |
|
|
|
this.initWaveItems(); // 重置波形 |
|
|
|
}, |
|
|
|
|
|
|
|
// 开始录音 |
|
|
|
startRecord() { |
|
|
|
if (this.isRecording) return; |
|
|
|
|
|
|
|
const options = { |
|
|
|
duration: this.recordMaxDuration, // 最大录音时长 |
|
|
|
sampleRate: 44100, // 采样率 |
|
|
|
numberOfChannels: 1, // 录音通道数 |
|
|
|
encodeBitRate: 192000, // 编码码率 |
|
|
|
format: 'mp3', // 音频格式 |
|
|
|
frameSize: 50 // 指定帧大小 |
|
|
|
}; |
|
|
|
|
|
|
|
// 开始录音 |
|
|
|
this.recorderManager.start(options); |
|
|
|
|
|
|
|
// 震动反馈 |
|
|
|
uni.vibrateShort({ |
|
|
|
success: function () { |
|
|
|
console.log('震动成功'); |
|
|
|
} |
|
|
|
}); |
|
|
|
}, |
|
|
|
|
|
|
|
// 停止录音 |
|
|
|
stopRecord() { |
|
|
|
if (!this.isRecording) return; |
|
|
|
this.recorderManager.stop(); |
|
|
|
|
|
|
|
// 震动反馈 |
|
|
|
uni.vibrateShort({ |
|
|
|
success: function () { |
|
|
|
console.log('震动成功'); |
|
|
|
} |
|
|
|
}); |
|
|
|
}, |
|
|
|
|
|
|
|
// 取消录音 |
|
|
|
cancelRecord() { |
|
|
|
if (!this.isRecording) return; |
|
|
|
this.recorderManager.stop(); |
|
|
|
this.audioPath = ''; // 清空录音路径 |
|
|
|
|
|
|
|
uni.showToast({ |
|
|
|
title: '录音已取消', |
|
|
|
icon: 'none' |
|
|
|
}); |
|
|
|
}, |
|
|
|
|
|
|
|
// 播放录音 |
|
|
|
playVoice() { |
|
|
|
if (!this.audioPath) { |
|
|
|
uni.showToast({ |
|
|
|
title: '没有可播放的录音', |
|
|
|
icon: 'none' |
|
|
|
}); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
if (this.isPlaying) { |
|
|
|
// 如果正在播放,则停止播放 |
|
|
|
this.innerAudioContext.stop(); |
|
|
|
this.isPlaying = false; |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// 设置音频源 |
|
|
|
this.innerAudioContext.src = this.audioPath; |
|
|
|
this.innerAudioContext.play(); |
|
|
|
this.isPlaying = true; |
|
|
|
|
|
|
|
// 播放完成后自动停止 |
|
|
|
setTimeout(() => { |
|
|
|
if (this.isPlaying) { |
|
|
|
this.isPlaying = false; |
|
|
|
} |
|
|
|
}, this.recordingTime * 1000 + 500); // 增加500ms缓冲时间 |
|
|
|
}, |
|
|
|
|
|
|
|
// 删除录音 |
|
|
|
deleteVoice() { |
|
|
|
if (!this.audioPath) return; |
|
|
|
|
|
|
|
uni.showModal({ |
|
|
|
title: '提示', |
|
|
|
content: '确定要删除这段录音吗?', |
|
|
|
success: (res) => { |
|
|
|
if (res.confirm) { |
|
|
|
// 如果正在播放,先停止播放 |
|
|
|
if (this.isPlaying) { |
|
|
|
this.innerAudioContext.stop(); |
|
|
|
this.isPlaying = false; |
|
|
|
} |
|
|
|
|
|
|
|
this.audioPath = ''; |
|
|
|
this.audioName = '录音文件01.mp3'; |
|
|
|
this.audioSize = '0KB'; |
|
|
|
this.recordingTime = 0; |
|
|
|
this.audioUrl = ''; |
|
|
|
|
|
|
|
uni.showToast({ |
|
|
|
title: '录音已删除', |
|
|
|
icon: 'none' |
|
|
|
}); |
|
|
|
} |
|
|
|
} |
|
|
|
}); |
|
|
|
}, |
|
|
|
|
|
|
|
// 提交语音订单 |
|
|
|
submitVoiceOrder() { |
|
|
|
if (!this.audioPath) { |
|
|
|
uni.showToast({ |
|
|
|
title: '请先录制语音', |
|
|
|
icon: 'none' |
|
|
|
}); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
if (this.isUploading) { |
|
|
|
uni.showToast({ |
|
|
|
title: '正在上传中,请稍候', |
|
|
|
icon: 'none' |
|
|
|
}); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// 显示加载提示 |
|
|
|
uni.showLoading({ |
|
|
|
title: '语音识别中...' |
|
|
|
}); |
|
|
|
|
|
|
|
this.isUploading = true; |
|
|
|
this.uploadProgress = 0; |
|
|
|
|
|
|
|
// 上传音频文件 |
|
|
|
this.uploadAudioFile(); |
|
|
|
}, |
|
|
|
|
|
|
|
// 上传音频文件 |
|
|
|
uploadAudioFile() { |
|
|
|
// 模拟上传进度 |
|
|
|
const simulateProgress = () => { |
|
|
|
this.uploadProgress = 0; |
|
|
|
const interval = setInterval(() => { |
|
|
|
this.uploadProgress += 5; |
|
|
|
if (this.uploadProgress >= 90) { |
|
|
|
clearInterval(interval); |
|
|
|
} |
|
|
|
}, 100); |
|
|
|
return interval; |
|
|
|
}; |
|
|
|
|
|
|
|
const progressInterval = simulateProgress(); |
|
|
|
|
|
|
|
// 使用OSS上传服务上传音频文件 |
|
|
|
this.$Oss.ossUpload(this.audioPath).then(url => { |
|
|
|
// 上传成功 |
|
|
|
clearInterval(progressInterval); |
|
|
|
this.uploadProgress = 100; |
|
|
|
this.audioUrl = url; |
|
|
|
console.log('音频上传成功', url); |
|
|
|
|
|
|
|
// 调用语音识别 |
|
|
|
this.recognizeVoice(url); |
|
|
|
}).catch(err => { |
|
|
|
// 上传失败 |
|
|
|
clearInterval(progressInterval); |
|
|
|
console.error('音频上传失败', err); |
|
|
|
this.handleUploadFailed('音频上传失败,请重试'); |
|
|
|
}); |
|
|
|
}, |
|
|
|
|
|
|
|
// 语音识别 |
|
|
|
recognizeVoice(fileUrl) { |
|
|
|
// 使用统一的API调用方式 |
|
|
|
this.$api('order.recognizeVoice', { |
|
|
|
fileUrl: fileUrl, |
|
|
|
userId: uni.getStorageSync('userId') || '' |
|
|
|
}, res => { |
|
|
|
// 回调方式处理结果 |
|
|
|
if (res.code === 0) { |
|
|
|
this.recognitionResult = res.data.result; |
|
|
|
console.log('语音识别成功', this.recognitionResult); |
|
|
|
|
|
|
|
// 处理订单 |
|
|
|
this.processOrder(); |
|
|
|
} else { |
|
|
|
this.handleUploadFailed(res.msg || '语音识别失败'); |
|
|
|
} |
|
|
|
}, err => { |
|
|
|
// 错误处理 |
|
|
|
console.error('语音识别请求失败', err); |
|
|
|
this.handleUploadFailed('网络请求失败,请检查网络连接'); |
|
|
|
}); |
|
|
|
}, |
|
|
|
|
|
|
|
// 处理订单 |
|
|
|
processOrder() { |
|
|
|
// 使用统一的API调用方式 |
|
|
|
this.$api('order.createFromVoice', { |
|
|
|
userId: uni.getStorageSync('userId') || '', |
|
|
|
audioUrl: this.audioUrl, |
|
|
|
recognitionResult: this.recognitionResult |
|
|
|
}).then(res => { |
|
|
|
// Promise方式处理结果 |
|
|
|
uni.hideLoading(); |
|
|
|
this.isUploading = false; |
|
|
|
|
|
|
|
if (res.code === 0) { |
|
|
|
// 下单成功 |
|
|
|
const orderId = res.data.orderId; |
|
|
|
|
|
|
|
// 显示成功提示并跳转 |
|
|
|
uni.showToast({ |
|
|
|
title: '语音下单成功', |
|
|
|
icon: 'success', |
|
|
|
duration: 1500, |
|
|
|
success: () => { |
|
|
|
setTimeout(() => { |
|
|
|
this.$utils.redirectTo('/pages_order/order/firmOrder?orderId=' + orderId); |
|
|
|
}, 1500); |
|
|
|
} |
|
|
|
}); |
|
|
|
} else { |
|
|
|
uni.showModal({ |
|
|
|
title: '提示', |
|
|
|
content: res.msg || '创建订单失败', |
|
|
|
showCancel: false |
|
|
|
}); |
|
|
|
} |
|
|
|
}).catch(err => { |
|
|
|
// 错误处理 |
|
|
|
uni.hideLoading(); |
|
|
|
this.isUploading = false; |
|
|
|
console.error('创建订单请求失败', err); |
|
|
|
this.handleUploadFailed('网络请求失败,请检查网络连接'); |
|
|
|
}); |
|
|
|
}, |
|
|
|
|
|
|
|
// 处理上传失败情况 |
|
|
|
handleUploadFailed(message) { |
|
|
|
uni.hideLoading(); |
|
|
|
this.isUploading = false; |
|
|
|
this.uploadProgress = 0; |
|
|
|
|
|
|
|
uni.showModal({ |
|
|
|
title: '上传失败', |
|
|
|
content: message, |
|
|
|
showCancel: false |
|
|
|
}); |
|
|
|
}, |
|
|
|
|
|
|
|
// 格式化文件大小 |
|
|
|
formatFileSize(size) { |
|
|
|
if (size < 1024) { |
|
|
|
this.audioSize = size + 'B'; |
|
|
|
} else if (size < 1024 * 1024) { |
|
|
|
this.audioSize = (size / 1024).toFixed(2) + 'KB'; |
|
|
|
} else { |
|
|
|
this.audioSize = (size / (1024 * 1024)).toFixed(2) + 'MB'; |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
// 格式化日期为字符串 |
|
|
|
formatDate(date) { |
|
|
|
const pad = (n) => n < 10 ? '0' + n : n; |
|
|
|
return pad(date.getMonth() + 1) + pad(date.getDate()); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
</script> |
|
|
@ -83,7 +540,43 @@ |
|
|
|
align-items: center; |
|
|
|
justify-content: center; |
|
|
|
border-radius: 100rpx; |
|
|
|
|
|
|
|
&.recording { |
|
|
|
background-color: rgba(220, 40, 40, 0.1); |
|
|
|
border: 1rpx solid #DC2828; |
|
|
|
} |
|
|
|
} |
|
|
|
.recording-status { |
|
|
|
width: 85%; |
|
|
|
margin: auto; |
|
|
|
margin-top: 20rpx; |
|
|
|
text-align: center; |
|
|
|
color: #DC2828; |
|
|
|
font-size: 28rpx; |
|
|
|
display: flex; |
|
|
|
justify-content: center; |
|
|
|
align-items: center; |
|
|
|
|
|
|
|
.recording-time { |
|
|
|
margin-left: 10rpx; |
|
|
|
font-weight: bold; |
|
|
|
} |
|
|
|
} |
|
|
|
.voice-wave { |
|
|
|
width: 85%; |
|
|
|
height: 120rpx; |
|
|
|
margin: 20rpx auto; |
|
|
|
display: flex; |
|
|
|
justify-content: space-between; |
|
|
|
align-items: flex-end; |
|
|
|
|
|
|
|
.wave-item { |
|
|
|
width: 10rpx; |
|
|
|
background-color: #DC2828; |
|
|
|
border-radius: 10rpx; |
|
|
|
transition: height 0.1s ease-in-out; |
|
|
|
} |
|
|
|
} |
|
|
|
.recording-file{ |
|
|
|
height: 250rpx; |
|
|
|
display: flex; |
|
|
|