董国庆
2 天以前 de19681d9a193d86b940a2f20d3263940273d725
录音
3个文件已修改
253 ■■■■■ 已修改文件
H5/components/voiceInputPopup.vue 179 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
H5/pages/Appeal/Appeal.vue 66 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
management/src/pages/setting/user/components/addAndEdit.jsx 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
H5/components/voiceInputPopup.vue
@@ -44,9 +44,9 @@
      minutes: 0,
      hours: 0,
      recordSegments: [], // 存储录音片段
      currentSegment: null, // 当前录音片段
      status: false,
      recorder: null
      recorder: null,
      currentSegment: null // 当前录音片段
    }
  },
  computed: {
@@ -79,32 +79,48 @@
      }
    },
    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)
      console.log('录音成功回调:', res)
      this.recorder = res
      // 保存当前录音片段
      if (res.localUrl) {
        this.recordSegments.push({
          url: res.localUrl,
          duration: this.seconds + this.minutes * 60 + this.hours * 3600
        })
        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) {
@@ -129,6 +145,7 @@
    closePopup() {
      // 清空录音片段
      this.recordSegments = []
      this.currentSegment = null
      // 重置计时器
      this.seconds = 0
      this.minutes = 0
@@ -161,7 +178,108 @@
    updateTimeDisplay() {
      this.time = `${String(this.hours).padStart(2, '0')}:${String(this.minutes).padStart(2, '0')}:${String(this.seconds).padStart(2, '0')}`;
    },
    onStop() {
    // 合并音频文件
    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()
@@ -173,12 +291,51 @@
      // 重置状态
      this.isRecording = false
      this.isPaused = false
      this.currentSegment = null
      
      // 处理录音片段
      if (this.recordSegments.length > 0) {
        // 发送第一个录音片段的URL
        this.$emit('submit', this.recordSegments[0].url)
        this.closePopup()
        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: '未检测到录音文件',
H5/pages/Appeal/Appeal.vue
@@ -154,9 +154,9 @@
import config from '@/config/index.js'
import voiceInputPopup from '@/components/voiceInputPopup.vue'
import {
        mapActions,
        mapState
    } from "vuex";
    mapActions,
    mapState
} from "vuex";
export default {
    components: {
@@ -183,6 +183,7 @@
            longitude: '',
            images: [],
            videos: [],
            voiceFile: '',//语音文件多个逗号拼接
            userInfo: uni.getStorageSync('userInfo'), //个人信息
            voiceInputShow: false,
        };
@@ -199,7 +200,7 @@
        this.getproblem()
        this.time = dayjs().format('YYYY-MM-DD')
    },
    methods: {
        ...mapActions(["playRecording", "pausePlaying"]),
        onPlayRecording(index) {
@@ -221,8 +222,8 @@
            this.voiceInputShow = false;
        },
        submitVoiceInput(e) {
            console.log('eeeeeeeeeeeeeeeeeee',e)
            this.videoContent.push({url: e, playing: false});
            console.log('eeeeeeeeeeeeeeeeeee', e)
            this.videoContent.push({ url: e.url, data: e.data, playing: false });
            this.voiceInputShow = false;
        },
        previewImage(index) {
@@ -260,7 +261,55 @@
                })]
            }))
        },
        submit(type) {
        async submit(type) {
            if (this.videoContent.length > 0) {
                uni.showLoading({
                    title: '正在上传语音文件...'
                });
                const uploadPromises = this.videoContent.map(item => {
                    return new Promise((resolve, reject) => {
                        console.log('item.data', item.data)
                        uni.uploadFile({
                            url: config.imageUrl,
                            file: item.data,
                            name: 'file',
                            // fileType: 'audio/mpeg',
                            // filePath: item.url,
                            // name: 'file',
                            header: {
                                'Content-Type': 'multipart/form-data',
                                'Authorization': uni.getStorageSync('token')
                            },
                            success: (uploadFileRes) => {
                                const response = JSON.parse(uploadFileRes.data);
                                if (response.code === 200) {
                                    resolve(response.data);
                                } else {
                                    reject(new Error('上传失败'));
                                }
                            },
                            fail: (error) => {
                                reject(error);
                            }
                        });
                    });
                });
                try {
                    const uploadedUrls = await Promise.all(uploadPromises);
                    this.voiceFile = uploadedUrls.join(',');
                    console.log('this.voiceFile', this.voiceFile)
                    uni.hideLoading();
                } catch (error) {
                    uni.hideLoading();
                    uni.showToast({
                        title: '语音文件上传失败',
                        icon: 'error'
                    });
                    return;
                }
            }
            const preciseRegex = /^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$/;
            if (!this.time) {
                uni.showToast({
@@ -306,6 +355,7 @@
                })
                return
            }
            const data = {
                time: this.time,
                problemType: this.problemType,
@@ -318,6 +368,7 @@
                descriptionContent: this.descriptionContent,
                images: this.images.join(','),
                videos: this.videos.join(','),
                voiceFile: this.voiceFile,
            }
            // 问题上报
            if (type == 1) {
@@ -380,6 +431,7 @@
                success: (res) => {
                    uni.showLoading()
                    console.log('res.tempFilePaths[0]', res.tempFilePaths[0])
                    uni.uploadFile({
                        url: config.imageUrl,
                        filePath: res.tempFilePaths[0],
management/src/pages/setting/user/components/addAndEdit.jsx
@@ -71,9 +71,7 @@
                    case 2:
                        getStreetList(adminInfo.districtsCode)
                        setLevelList(() => [{ name: '区县账号', value: 2 }, { name: '街道账号', value: 3 }, { name: '社区账号', value: 4 }, { name: '党员账号', value: 5 }])
                        this.$nextTick(() => {
                            form.setFieldsValue({ districtsCode: adminInfo.districtsCode })
                        })
                        form.setFieldsValue({ districtsCode: adminInfo.districtsCode })
                        break;
                    case 3:
@@ -128,9 +126,9 @@
    // 保存
    const okHandle = () => {
        form.validateFields().then((values) => {
            if(values.password){
            if (values.password) {
                values.password = CryptoJS.MD5(values.password).toString();
            }else{
            } else {
                delete values.password
            }
            if (values.DepartmentId) {