- <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>
|