From dd076cee45ea103a43d6726a21d900f5484b3729 Mon Sep 17 00:00:00 2001
From: 13404089107 <puwei@sinata.cn>
Date: 星期二, 20 五月 2025 21:33:47 +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