hejianhao
3 天以前 04e2948d80d575bb273363264de7d52219538331
Merge branch 'master' of http://120.76.84.145:10101/gitblit/r/H5/threeSide
6个文件已修改
4个文件已添加
560 ■■■■■ 已修改文件
H5/components/voiceInputPopup.vue 211 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
H5/config/index.js 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
H5/manifest.json 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
H5/package.json 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
H5/pages/Appeal/Appeal.vue 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
H5/uni_modules/mumu-recorder/changelog.md 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
H5/uni_modules/mumu-recorder/components/mumu-recorder/mumu-recorder.vue 113 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
H5/uni_modules/mumu-recorder/package.json 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
H5/uni_modules/mumu-recorder/readme.md 117 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
H5/utils/request.js 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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,160 @@
  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: [], // 存储录音片段
      currentSegment: null, // 当前录音片段
      status: false,
      recorder: null
    }
  },
  methods: {
    closePopup() {
      this.$emit('update:show', false)
  computed: {
    getRecordButtonSrc() {
      if (this.isPaused) {
        return '/static/Appeal/start.png'
      }
      return this.isRecording ? '/static/Appeal/stop.png' : '/static/Appeal/start.png'
    }
    },
    onPlay() {},
    onPause() {},
    onStop() {},
  mounted() {
  },
  methods: {
    handlerSave() {
      let tag = document.createElement('a')
      tag.href = this.recorder.localUrl
      tag.download = '录音'
      tag.click()
    },
    handlerOnCahnger() {
      if (!this.isRecording && !this.isPaused) {
        // 开始新的录音
        this.startNewRecording()
      } else if (this.isRecording) {
        // 暂停当前录音
        this.pauseRecording()
      } else if (this.isPaused) {
        // 继续录音
        this.resumeRecording()
      }
    },
    startNewRecording() {
      this.isRecording = true
      this.isPaused = false
      this.startTimer()
      this.$refs.mumuRecorder.start()
    },
    pauseRecording() {
      this.isRecording = false
      this.isPaused = true
      this.stopTimer()
      this.$refs.mumuRecorder.stop()
    },
    resumeRecording() {
      this.isRecording = true
      this.isPaused = false
      this.startTimer()
      this.$refs.mumuRecorder.start()
    },
    handlerSuccess(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
        })
      }
    },
    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.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')}`;
    },
    onStop() {
      // 如果正在录音,先停止当前录音
      if (this.isRecording) {
        this.$refs.mumuRecorder.stop()
      }
      // 停止计时
      this.stopTimer()
      // 重置状态
      this.isRecording = false
      this.isPaused = false
      // 处理录音片段
      if (this.recordSegments.length > 0) {
        // 发送第一个录音片段的URL
        this.$emit('submit', this.recordSegments[0].url)
        this.closePopup()
      } else {
        uni.showToast({
          title: '未检测到录音文件',
          icon: 'none',
          duration: 2000
        })
      }
    }
  },
  beforeDestroy() {
    this.stopTimer();
  }
}
</script>
@@ -48,6 +199,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 +210,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 +243,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;
  }
}
::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> 
H5/config/index.js
@@ -1,5 +1,5 @@
export default {
    BASE_URL: 'https://huacheng.psciio.com',
    // BASE_URL: 'http://192.168.110.188:6194',
    // BASE_URL: 'https://huacheng.psciio.com',
    BASE_URL: 'http://192.168.110.188:6194',
    imageUrl: 'https://huacheng.psciio.com/api/huacheng-applets/common/uploadimages',
}
H5/manifest.json
@@ -78,6 +78,9 @@
                "enable" : false
            }
        },
        "devServer": {
            "https": true
        },
        "router" : {
            "base" : "./"
        }
H5/package.json
@@ -1,5 +1,10 @@
{
  "dependencies": {
    "echarts": "^5.6.0"
  },
  "permission": {
    "scope.record": {
      "desc": "录音功能需要使用您的录音权限"
    }
  }
}
H5/pages/Appeal/Appeal.vue
@@ -62,7 +62,7 @@
                <view class="flex a-center j-between">
                    <text class="w-108 fs-27 font-bold mr-85">问题描述</text>
                    <view class="flex a-center" @click="voiceInput">
                        <image src="/static/Appeal/yuyin.png" class="w-30 h-30 mr-11" mode=""></image>
                        <text class="fs-23 red">语音输入</text>
                    </view>
                </view>
@@ -178,10 +178,7 @@
            detailedAddress: '',
            descriptionTitle: '',
            descriptionContent: '',
            videoContent: [
                { url: 'xxx1', playing: false },
                { url: 'xxx2', playing: false }
            ],
            videoContent: [],
            latitude: '',
            longitude: '',
            images: [],
@@ -224,7 +221,8 @@
            this.voiceInputShow = false;
        },
        submitVoiceInput(e) {
            this.videoContent.push(e);
            console.log('eeeeeeeeeeeeeeeeeee',e)
            this.videoContent.push({url: e, playing: false});
            this.voiceInputShow = false;
        },
        previewImage(index) {
H5/uni_modules/mumu-recorder/changelog.md
New file
@@ -0,0 +1,4 @@
## 1.0.1(2022-06-11)
修复苹果手机在微信中无法获取音频长度问题
## 1.0.0(2022-06-10)
版本上线
H5/uni_modules/mumu-recorder/components/mumu-recorder/mumu-recorder.vue
New file
@@ -0,0 +1,113 @@
<template>
    <view class="recorder">
    </view>
</template>
<script>
    export default {
        data() {
            return {
                isUserMedia: false,
                stream: null,
                audio: null,
                recorder: null,
                chunks: [],
                startTime: 0
            }
        },
        mounted() {
            /**
             *     error 事件的返回状态
             *     100: 请在HTTPS环境中使用
             *     101: 浏览器不支持
             *  201: 用户拒绝授权
             *  500: 未知错误
             * */
            if (origin.indexOf('https') === -1) {
                this.$emit('error', '100')
                throw '请在 https 环境中使用本插件。'
            }
            if (!navigator.mediaDevices || !window.MediaRecorder) {
                this.$emit('error', '101')
                throw '当前浏览器不支持'
            }
            this.getRecorderManager()
        },
        methods: {
            getRecorderManager() {
                this.audio = document.createElement('audio')
                navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => {
                    this.isUserMedia = true
                    stream.getTracks().forEach((track) => {
                        track.stop()
                    })
                }).catch(err => {
                    this.onErrorHandler(err)
                })
            },
            start() {
                if (!this.isUserMedia) return console.log('设备不支持')
                navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => {
                    this.startTime = new Date().getTime()
                    this.stream = stream
                    this.recorder = new MediaRecorder(stream)
                    this.recorder.ondataavailable = this.getRecordingData
                    this.recorder.onstop = this.saveRecordingData
                    this.recorder.start()
                }).catch(err => {
                    this.onErrorHandler(err)
                })
            },
            stop() {
                this.recorder.stop()
                this.stream.getTracks().forEach((track) => {
                    track.stop()
                })
            },
            getRecordingData(e) {
                this.chunks.push(e.data)
            },
            saveRecordingData() {
                const blob = new Blob(this.chunks, { 'type': 'audio/mpeg' }),
                    localUrl = URL.createObjectURL(blob)
                const endTime = new Date().getTime()
                let duration = (endTime - this.startTime).toString().split('')
                duration.splice(duration.length - 2)
                duration.splice(duration.length - 1, 0, '.')
                duration = parseFloat(duration.join(''))
                const recorder = {
                    data: blob,
                    duration: duration,
                    localUrl: localUrl
                }
                this.$emit('success', recorder)
            },
            onErrorHandler(err) {
                console.log(err)
                if (err.name === 'NotAllowedError') {
                    this.$emit('error', '201')
                    throw '用户拒绝了当前的浏览器实例的访问请求'
                }
                if (err.name === 'NotReadableError') {
                    this.$emit('error', '101')
                    throw '当前浏览器不支持'
                }
                this.$emit('error', '500')
                throw '调用失败,原因不详'
            }
        },
        destroyed() {
            this.stop()
        }
    }
</script>
<style>
</style>
H5/uni_modules/mumu-recorder/package.json
New file
@@ -0,0 +1,87 @@
{
  "id": "mumu-recorder",
  "displayName": "h5录音组件,调用H5原生功能使用麦克风进行录音",
  "version": "1.0.1",
  "description": "演示案例中模仿了微信的长按发送语音,与普通录音demo。",
  "keywords": [
    "录音",
    "麦克风",
    "模仿微信"
],
  "repository": "",
  "engines": {
    "HBuilderX": "^3.1.0"
  },
  "dcloudext": {
    "category": [
        "前端组件",
        "通用组件"
    ],
    "sale": {
      "regular": {
        "price": "0.00"
      },
      "sourcecode": {
        "price": "0.00"
      }
    },
    "contact": {
      "qq": ""
    },
    "declaration": {
      "ads": "无",
      "data": "无",
      "permissions": "麦克风"
    },
    "npmurl": ""
  },
  "uni_modules": {
    "dependencies": [],
    "encrypt": [],
    "platforms": {
      "cloud": {
        "tcb": "y",
        "aliyun": "y"
      },
      "client": {
        "Vue": {
          "vue2": "y",
          "vue3": "y"
        },
        "App": {
          "app-vue": "n",
          "app-nvue": "n"
        },
        "H5-mobile": {
          "Safari": "y",
          "Android Browser": "y",
          "微信浏览器(Android)": "y",
          "QQ浏览器(Android)": "y"
        },
        "H5-pc": {
          "Chrome": "y",
          "IE": "n",
          "Edge": "y",
          "Firefox": "y",
          "Safari": "y"
        },
        "小程序": {
          "微信": "u",
          "阿里": "u",
          "百度": "u",
          "字节跳动": "u",
          "QQ": "u",
          "钉钉": "u",
          "快手": "u",
          "飞书": "u",
          "京东": "u",
          "小红书": "u"
        },
        "快应用": {
          "华为": "u",
          "联盟": "u"
        }
      }
    }
  }
}
H5/uni_modules/mumu-recorder/readme.md
New file
@@ -0,0 +1,117 @@
## 插件简绍
### 实现原理
> 通过 navigator.mediaDevices.getUserMedia(需要https环境) 这个api调用麦克风,获取到到音频流数据。
>
> 通过 MediaRecorder 这个构造函数对音频流进行接收,完成录制后会返回一个存储`Blob`内容的录制数据。
### 使用环境
需要https环境才能使用,本地测试可以在 manifest.json  中点击源码展示,找到h5 ,添加:"devServer" : { "https" : true}
**请勿使用 UC浏览器 与 夸克等阿里旗下的浏览器,发现他们使用的内核都较低,无法正常获取音频流,并且都有对接音频流截取的插件,导致无法正常获取音频流的数据。在微信中可以正常使用,推荐在微信内打开演示案例 **
需要https环境才能使用!!!
需要https环境才能使用!!!
需要https环境才能使用!!!
### 插件使用
**插件已支持 uni_modules 支持组件easycom,以下代码演示的是普通使用**
``` html
<!-- HTML -->
    <view>
    <audio :src='recorder.localUrl' v-if='recorder' name='本地录音' controls="true"></audio>
    <view @click='handlerOnCahnger'>
            {{!status?'开始录音':'结束录音'}}
        </view>
        <mumu-recorder ref='recorder' @success='handlerSuccess' @error='handlerError'></mumu-recorder>
    </view>
```
``` javascript
// js
    import MumuRecorder from '@/uni_modules/mumu-recorder/components/mumu-recorder/mumu-recorder.vue'
    export default {
        components: { MumuRecorder },
        data() {
            return {
                status: false,
                recorder: null
            }
        },
        onLoad() {
        },
        methods: {
            handlerSave() {
                let tag = document.createElement('a')
                tag.href = this.recorder.localUrl
                tag.download = '录音'
                tag.click()
            },
            handlerOnCahnger() {
                if (this.status) {
                    this.$refs.recorder.stop()
                } else {
                    this.$refs.recorder.start()
                }
                this.status = !this.status
            },
            handlerSuccess(res) {
                console.log(res)
                this.recorder = res
            },
            handlerError(code) {
                switch (code) {
                    case '101':
                        uni.showModal({
                            content: '当前浏览器版本较低,请更换浏览器使用,推荐在微信中打开。'
                        })
                        break;
                    case '201':
                        uni.showModal({
                            content: '麦克风权限被拒绝,请刷新页面后授权麦克风权限。'
                        })
                        break
                    default:
                        uni.showModal({
                            content: '未知错误,请刷新页面重试'
                        })
                        break
                }
            }
        }
    }
```
### 相关API
##### 组件内部方法($refs 调用)
| 方法名 | 说明     | 参数 |
| ------ | -------- | ---- |
| start  | 开始录音 | 无   |
| stop   | 结束录音 | 无   |
##### 事件(Events)
| 事件名  | 说明                 | 回调参数                                                     |
| ------- | -------------------- | ------------------------------------------------------------ |
| success | 停止录音后调用此事件 | 返回录音数据,是一个对象<br />{ data: 音频的 blob 数据,上传请使用这个            <br />duration: 当前音频长度<br/>localUrl: 当前音频的本地链接,可直接通过 audio 标签进行播放 } |
| error   | 组件内部发生错误     | 错误码:<100 当前不是https环境> <101 浏览器不支持> <201 麦克风权限被拒绝> <500  未知错误> |
### 案例演示
![enter description here](https://h5plugin.mumudev.top/public/recorder/qrcode.png)
## 支持作者
![支持作者](https://student.mumudev.top/wxMP.jpg)
H5/utils/request.js
@@ -19,9 +19,9 @@
    return new Promise(function(resolve, reject) {
        let token = uni.getStorageSync('token')
        // token =
        //     'Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOjE2MjA0LCJ0eXBlIjoxLCJleHAiOjE3NDM1NTY3NzYsImNyZWF0ZWQiOjE3NDIyNjA3NzY2NTR9.SAmdlprtz_Z1cNSNB4ANYsKJFC7E7Jfxo6XbT9vpbO6zHBpTRfq_oyp9UlDQtHNiYgv-MuyqCBPw1-x88C_-8A'
        // uni.setStorageSync('token', token)
        token =
            'Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOjE4OTU1MjI1Nzk1MDcwODEyMTgsInR5cGUiOjEsImV4cCI6MTc0Nzk4MzIyMiwiY3JlYXRlZCI6MTc0NjY4NzIyMjAxMX0.pmUfTkxkbBirDMbnMR1IaLsbSiiwHc366_yyAetCzTOWYxTNgmQlmvw26_W62NHLOebB_ZAEgZsPvkJcaLOoPg'
        uni.setStorageSync('token', token)
        let header = {
            'content-type': type ? 'application/x-www-form-urlencoded;charset=UTF-8' : 'application/json',
            'Authorization': token,