From 20e32587ecbab27e4436f2f64a15faa3c89e4f41 Mon Sep 17 00:00:00 2001 From: pyt <626651354@qq.com> Date: 星期二, 20 五月 2025 21:39:59 +0800 Subject: [PATCH] Merge branch 'master' of http://120.76.84.145:10101/gitblit/r/H5/threeSide --- H5/components/voiceInputPopup.vue | 368 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 files changed, 346 insertions(+), 22 deletions(-) diff --git a/H5/components/voiceInputPopup.vue b/H5/components/voiceInputPopup.vue index 4737331..b0393ee 100644 --- a/H5/components/voiceInputPopup.vue +++ b/H5/components/voiceInputPopup.vue @@ -1,25 +1,32 @@ <template> - <u-popup :show="show" mode="bottom" :closeOnClickOverlay="false" @close="closePopup" zIndex="10071"> + <u-popup :show="show" mode="bottom" :closeOnClickOverlay="false" @close="closePopup" :zIndex="9999" :overlay="true" + :safeAreaInsetBottom="true"> <view class="voice-popup"> <view class="header"> <text class="pl-30">语音输入</text> <image src="/static/Appeal/close.png" class="w-34 h-34 mr-30" mode="" @click="closePopup"></image> </view> <view class="record-anim"> - <image src="/static/Appeal/step.png" class="w-153 h-119" mode="" @click="onPlay"></image> + <image src="/static/Appeal/step.png" class="w-153 h-119" mode=""></image> </view> <view class="timer">{{ time }}</view> <view class="btns"> - <image src="/static/Appeal/start.png" class="w-153 h-153 mr-14" mode="" @click="onPlay"></image> - <image src="/static/Appeal/stop.png" class="w-153 h-153 mr-14" mode="" @click="onPause"></image> + <image :src="getRecordButtonSrc" class="w-153 h-153 mr-14" mode="" @click="handlerOnCahnger"></image> <image src="/static/Appeal/cancel.png" class="w-153 h-153 mr-14" mode="" @click="onStop"></image> </view> </view> + <mumu-recorder ref="mumuRecorder" @success="handlerSuccess" @error="handleError"></mumu-recorder> </u-popup> </template> <script> +import { getSignature } from '../pages/index/service' +import MumuRecorder from '@/uni_modules/mumu-recorder/components/mumu-recorder/mumu-recorder.vue' export default { + name: 'VoiceInputPopup', + components: { + MumuRecorder + }, props: { show: { type: Boolean, @@ -29,16 +36,317 @@ data() { return { time: '00:00:00', - barHeights: [20, 30, 40, 30, 20], // 可做动画 + isRecording: false, + isPaused: false, + tempFilePath: '', + timer: null, + seconds: 0, + minutes: 0, + hours: 0, + recordSegments: [], // 存储录音片段 + status: false, + recorder: null, + currentSegment: null // 当前录音片段 } }, + computed: { + getRecordButtonSrc() { + if (this.isPaused) { + return '/static/Appeal/start.png' + } + return this.isRecording ? '/static/Appeal/stop.png' : '/static/Appeal/start.png' + } + }, + mounted() { + }, methods: { - closePopup() { - this.$emit('update:show', false) + handlerSave() { + let tag = document.createElement('a') + tag.href = this.recorder.localUrl + tag.download = '录音' + tag.click() }, - onPlay() {}, - onPause() {}, - onStop() {}, + handlerOnCahnger() { + if (!this.isRecording && !this.isPaused) { + // 开始新的录音 + this.startNewRecording() + } else if (this.isRecording) { + // 暂停当前录音 + this.pauseRecording() + } else if (this.isPaused) { + // 继续录音 + this.resumeRecording() + } + }, + startNewRecording() { + console.log('开始新的录音') + this.isRecording = true + this.isPaused = false + this.startTimer() + this.$refs.mumuRecorder.start() + }, + pauseRecording() { + console.log('暂停录音') + this.isRecording = false + this.isPaused = true + this.stopTimer() + this.$refs.mumuRecorder.stop() + }, + resumeRecording() { + console.log('继续录音') + this.isRecording = true + this.isPaused = false + this.startTimer() + this.$refs.mumuRecorder.start() + }, + handlerSuccess(res) { + console.log('录音成功回调:', res) + this.recorder = res + // 保存当前录音片段 + if (res.localUrl) { + console.log('当前录音片段URL:', res.localUrl) + console.log('当前已存储的片段数:', this.recordSegments.length) + console.log('当前片段:', this.currentSegment) + + // 只有当当前片段与上一个不同时才添加 + if (!this.currentSegment || this.currentSegment.url !== res.localUrl) { + console.log('添加新的录音片段') + this.currentSegment = { + url: res.localUrl, + data: res.data, + duration: this.seconds + this.minutes * 60 + this.hours * 3600 + } + this.recordSegments.push(this.currentSegment) + console.log('更新后的片段列表:', this.recordSegments) + } else { + console.log('跳过重复的录音片段') + } + } + }, + handlerError(code) { + switch (code) { + case '101': + uni.showModal({ + content: '当前浏览器版本较低,请更换浏览器使用,推荐在微信中打开。' + }) + break; + case '201': + uni.showModal({ + content: '麦克风权限被拒绝,请刷新页面后授权麦克风权限。' + }) + break + default: + uni.showModal({ + content: '未知错误,请刷新页面重试' + }) + break + } + }, + closePopup() { + // 清空录音片段 + this.recordSegments = [] + this.currentSegment = null + // 重置计时器 + this.seconds = 0 + this.minutes = 0 + this.hours = 0 + this.updateTimeDisplay() + // 关闭弹窗 + this.$emit('close') + }, + startTimer() { + this.stopTimer(); + this.timer = setInterval(() => { + this.seconds++; + if (this.seconds >= 60) { + this.seconds = 0; + this.minutes++; + if (this.minutes >= 60) { + this.minutes = 0; + this.hours++; + } + } + this.updateTimeDisplay(); + }, 1000); + }, + stopTimer() { + if (this.timer) { + clearInterval(this.timer); + this.timer = null; + } + }, + updateTimeDisplay() { + this.time = `${String(this.hours).padStart(2, '0')}:${String(this.minutes).padStart(2, '0')}:${String(this.seconds).padStart(2, '0')}`; + }, + // 合并音频文件 + async mergeAudioFiles(audioUrls) { + try { + const audioContext = new (window.AudioContext || window.webkitAudioContext)(); + const audioBuffers = []; + + // 加载所有音频文件 + for (const url of audioUrls) { + const response = await fetch(url); + const arrayBuffer = await response.arrayBuffer(); + const audioBuffer = await audioContext.decodeAudioData(arrayBuffer); + audioBuffers.push(audioBuffer); + } + + // 计算总时长 + const totalLength = audioBuffers.reduce((acc, buffer) => acc + buffer.length, 0); + + // 创建新的音频缓冲区 + const mergedBuffer = audioContext.createBuffer( + audioBuffers[0].numberOfChannels, + totalLength, + audioBuffers[0].sampleRate + ); + + // 合并音频数据 + let offset = 0; + for (const buffer of audioBuffers) { + for (let channel = 0; channel < buffer.numberOfChannels; channel++) { + const channelData = buffer.getChannelData(channel); + mergedBuffer.copyToChannel(channelData, channel, offset); + } + offset += buffer.length; + } + + // 将合并后的音频转换为 Blob + const wavBlob = await this.audioBufferToWav(mergedBuffer); + const mergedUrl = URL.createObjectURL(wavBlob); + + return mergedUrl; + } catch (error) { + console.error('合并音频失败:', error); + throw error; + } + }, + + // 将 AudioBuffer 转换为 WAV 格式 + audioBufferToWav(buffer) { + const numOfChan = buffer.numberOfChannels; + const length = buffer.length * numOfChan * 2; + const buffer2 = new ArrayBuffer(44 + length); + const view = new DataView(buffer2); + const channels = []; + let sample; + let offset = 0; + let pos = 0; + + // 写入 WAV 文件头 + setUint32(0x46464952); // "RIFF" + setUint32(36 + length); // 文件长度 + setUint32(0x45564157); // "WAVE" + setUint32(0x20746d66); // "fmt " chunk + setUint32(16); // 长度 = 16 + setUint16(1); // PCM (uncompressed) + setUint16(numOfChan); + setUint32(buffer.sampleRate); + setUint32(buffer.sampleRate * 2 * numOfChan); // avg. bytes/sec + setUint16(numOfChan * 2); // block-align + setUint16(16); // 16-bit + setUint32(0x61746164); // "data" - chunk + setUint32(length); // chunk length + + // 写入音频数据 + for (let i = 0; i < buffer.numberOfChannels; i++) { + channels.push(buffer.getChannelData(i)); + } + + while (pos < buffer.length) { + for (let i = 0; i < numOfChan; i++) { + sample = Math.max(-1, Math.min(1, channels[i][pos])); + sample = (0.5 + sample < 0 ? sample * 32768 : sample * 32767) | 0; + view.setInt16(44 + offset, sample, true); + offset += 2; + } + pos++; + } + + return new Blob([buffer2], { type: 'audio/wav' }); + + function setUint16(data) { + view.setUint16(pos, data, true); + pos += 2; + } + + function setUint32(data) { + view.setUint32(pos, data, true); + pos += 4; + } + }, + + // 修改 onStop 方法 + async onStop() { + console.log('停止录音') + // 如果正在录音,先停止当前录音 + if (this.isRecording) { + this.$refs.mumuRecorder.stop() + } + + // 停止计时 + this.stopTimer() + + // 重置状态 + this.isRecording = false + this.isPaused = false + this.currentSegment = null + + // 处理录音片段 + if (this.recordSegments.length > 0) { + try { + uni.showLoading({ + title: '正在处理音频...' + }) + + // 获取所有录音片段的URL + const audioUrls = this.recordSegments.map(segment => segment.url) + console.log('准备合并的音频片段列表:', audioUrls) + console.log('音频片段数量:', audioUrls.length) + + // 合并音频文件 + const mergedUrl = await this.mergeAudioFiles(audioUrls) + + // 创建合并后的音频数据对象 + const mergedAudioData = { + url: this.recordSegments[this.recordSegments.length - 1].url, + data: this.recordSegments[this.recordSegments.length - 1].data, + duration: this.seconds + this.minutes * 60 + this.hours * 3600 + } + + uni.showToast({ + title: '录制成功', + icon: 'success', + duration: 2000 + }) + + setTimeout(() => { + this.$emit('submit', mergedAudioData) + }, 2000) + + uni.hideLoading() + this.closePopup() + } catch (error) { + console.error('音频合并失败:', error) + uni.hideLoading() + uni.showToast({ + title: '音频合并失败,请重试', + icon: 'none', + duration: 2000 + }) + } + } else { + uni.showToast({ + title: '未检测到录音文件', + icon: 'none', + duration: 2000 + }) + } + } + }, + beforeDestroy() { + this.stopTimer(); } } </script> @@ -48,6 +356,9 @@ background: #fff; border-radius: 24rpx 24rpx 0 0; padding: 40rpx 0 30rpx 0; + position: relative; + z-index: 9999; + .header { display: flex; justify-content: center; @@ -56,25 +367,30 @@ font-size: 32rpx; font-weight: bold; margin-bottom: 80rpx; + text { flex: 1; text-align: center; } + .u-icon { position: absolute; right: 30rpx; top: 0; } } + .record-anim { display: flex; justify-content: center; align-items: center; margin-bottom: 57rpx; + .bars { display: flex; align-items: flex-end; height: 60rpx; + .bar { width: 10rpx; margin: 0 4rpx; @@ -84,26 +400,34 @@ } } } + .timer { text-align: center; font-size: 28rpx; color: #888; - // margin-bottom: 30rpx; } + .btns { display: flex; justify-content: space-around; align-items: center; - padding: 40rpx 67rpx 76rpx 67rpx; - // .u-button { - // width: 100rpx; - // height: 100rpx; - // border-radius: 50%; - // display: flex; - // justify-content: center; - // align-items: center; - // margin: 0 10rpx; - // } + padding: 40rpx 67rpx 6rpx 67rpx; } } -</style> \ No newline at end of file + +::v-deep .uni-toast { + z-index: 10099 !important; +} + +.uni-sample-toast { + z-index: 10099 !important; +} + +::v-deep .uni-toast .uni-sample-toast { + z-index: 10099 !important; +} + +/deep/ .u-transition.u-fade-enter-to.u-fade-enter-active { + z-index: 997 !important; +} +</style> \ No newline at end of file -- Gitblit v1.7.1