<template>
|
|
<view class="hand-top">
|
|
<navbar
|
|
title="快捷下单"
|
|
leftClick
|
|
@leftClick="$utils.navigateBack"
|
|
/>
|
|
<view class="voice-top">
|
|
<view class="left-icon"></view>
|
|
<text>录制语音下单</text>
|
|
</view>
|
|
<view class="voice-upload">
|
|
<view class="long-speak"
|
|
@touchstart="startRecord"
|
|
@touchend="stopRecord"
|
|
@touchcancel="cancelRecord"
|
|
:class="{'recording': isRecording}">
|
|
<uv-icon name="mic" size="45rpx" color="#DC2828"></uv-icon>
|
|
<text>{{isRecording ? '松开结束录音' : '长按可说话'}}</text>
|
|
</view>
|
|
<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" @click="playVoice"></image>
|
|
<image src="../static/order/3.png" mode="" class="file-delete" @click="deleteVoice"></image>
|
|
<view class="file-top">
|
|
<p>{{audioName}}</p>
|
|
<p style="color: #A6ADBA;">{{audioSize}}</p>
|
|
</view>
|
|
<view class="file-bottom">
|
|
<view class="schedule" :style="{width: uploadProgress + '%'}"></view>
|
|
<text>{{uploadProgress}}%</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
<view class="text-upload">
|
|
<text>(录音上传你所需要识别的产品语音)</text>
|
|
</view>
|
|
</view>
|
|
<view class="fast-order">
|
|
<view class="voice-button" @click="submitVoiceOrder" :style="audioPath ? '' : 'opacity: 0.5;'">
|
|
<text>快捷下单</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script>
|
|
export default {
|
|
data() {
|
|
return {
|
|
recorderManager: null, // 录音管理器
|
|
innerAudioContext: null, // 音频播放器
|
|
isRecording: false, // 是否正在录音
|
|
isPlaying: false, // 是否正在播放
|
|
audioPath: '', // 录音文件路径
|
|
audioName: '录音文件01.mp3', // 录音文件名称
|
|
audioSize: '0KB', // 录音文件大小
|
|
uploadProgress: 0, // 上传进度
|
|
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.createVoiceOrder(url);
|
|
}).catch(err => {
|
|
// 上传失败
|
|
clearInterval(progressInterval);
|
|
console.error('音频上传失败', err);
|
|
this.handleUploadFailed('音频上传失败,请重试');
|
|
});
|
|
},
|
|
|
|
// 创建语音订单
|
|
createVoiceOrder(audioUrl) {
|
|
this.$api('index.addOrder', {
|
|
voiceUrl: audioUrl,
|
|
type: '2', // 2表示语音下单
|
|
userId: uni.getStorageSync('userId') || ''
|
|
}, res => {
|
|
uni.hideLoading();
|
|
this.isUploading = false;
|
|
|
|
if (res.code === 200) {
|
|
// 下单成功
|
|
uni.showToast({
|
|
title: '下单成功',
|
|
icon: 'success',
|
|
duration: 1500,
|
|
success: () => {
|
|
setTimeout(() => {
|
|
// 跳转到订单列表页
|
|
this.$utils.redirectTo('/pages_order/order/orderList');
|
|
}, 1500);
|
|
}
|
|
});
|
|
} else {
|
|
uni.showModal({
|
|
title: '提示',
|
|
content: res.message || '下单失败',
|
|
showCancel: false
|
|
});
|
|
}
|
|
}, 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>
|
|
|
|
<style scoped lang="scss">
|
|
.hand-top{
|
|
background-color: #ffffff;
|
|
.voice-top{
|
|
color: #333333;
|
|
height: 100rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
background-color: #ffffff;
|
|
.left-icon{
|
|
background-color: #D03F25;
|
|
display: inline-block;
|
|
width: 10rpx;
|
|
height: 30rpx;
|
|
border-radius: 100rpx;
|
|
margin-left: 50rpx;
|
|
margin-right: 20rpx;
|
|
padding-bottom: 5rpx;
|
|
}
|
|
}
|
|
.voice-upload{
|
|
.long-speak{
|
|
color: #DC2828;
|
|
border: 1rpx solid #DC2828;
|
|
width: 85%;
|
|
height: 80rpx;
|
|
margin: auto;
|
|
margin-top: 60rpx;
|
|
display: flex;
|
|
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;
|
|
align-items: center;
|
|
justify-content: center;
|
|
position: relative;
|
|
margin-top: 30rpx;
|
|
.file{
|
|
background-color: #F4F4F4;
|
|
width: 95%;
|
|
height: 200rpx;
|
|
border-radius: 20rpx;
|
|
.record{
|
|
position: absolute;
|
|
height: 80rpx;
|
|
width: 80rpx;
|
|
top: 65rpx;
|
|
left: 60rpx;
|
|
}
|
|
.file-start{
|
|
position: absolute;
|
|
height: 30rpx;
|
|
width: 30rpx;
|
|
top: 60rpx;
|
|
right: 100rpx;
|
|
}
|
|
.file-delete{
|
|
position: absolute;
|
|
height: 30rpx;
|
|
width: 30rpx;
|
|
top: 60rpx;
|
|
right: 50rpx;
|
|
}
|
|
.file-top{
|
|
position: absolute;
|
|
width: 280rpx;
|
|
top: 60rpx;
|
|
left: 160rpx;
|
|
}
|
|
.file-bottom{
|
|
position: absolute;
|
|
width: 88%;
|
|
height: 30rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
top: 160rpx;
|
|
left: 60rpx;
|
|
.schedule{
|
|
width: 80%;
|
|
height: 10rpx;
|
|
border-radius: 30rpx;
|
|
background-color: #D03F25;
|
|
margin-right: 15rpx;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.text-upload{
|
|
height: 100rpx;
|
|
text-align: center;
|
|
line-height: 100rpx;
|
|
color: #666666;
|
|
margin-bottom: 100rpx;
|
|
}
|
|
}
|
|
.voice-button{
|
|
color: #ffffff;
|
|
background-color: #DC2828;
|
|
width: 85%;
|
|
height: 100rpx;
|
|
margin: auto;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border-radius: 100rpx;
|
|
}
|
|
}
|
|
</style>
|