| | |
| | | <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, |
| | |
| | | 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> |
| | |
| | | background: #fff; |
| | | border-radius: 24rpx 24rpx 0 0; |
| | | padding: 40rpx 0 30rpx 0; |
| | | position: relative; |
| | | z-index: 9999; |
| | | |
| | | .header { |
| | | display: flex; |
| | | justify-content: center; |
| | |
| | | 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; |
| | |
| | | } |
| | | } |
| | | } |
| | | |
| | | .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> |
| | |
| | | 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', |
| | | } |
| | |
| | | "enable" : false |
| | | } |
| | | }, |
| | | "devServer": { |
| | | "https": true |
| | | }, |
| | | "router" : { |
| | | "base" : "./" |
| | | } |
| | |
| | | { |
| | | "dependencies": { |
| | | "echarts": "^5.6.0" |
| | | }, |
| | | "permission": { |
| | | "scope.record": { |
| | | "desc": "录音功能需要使用您的录音权限" |
| | | } |
| | | } |
| | | } |
| | |
| | | <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> |
| | |
| | | detailedAddress: '', |
| | | descriptionTitle: '', |
| | | descriptionContent: '', |
| | | videoContent: [ |
| | | { url: 'xxx1', playing: false }, |
| | | { url: 'xxx2', playing: false } |
| | | ], |
| | | videoContent: [], |
| | | latitude: '', |
| | | longitude: '', |
| | | images: [], |
| | |
| | | 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) { |
New file |
| | |
| | | ## 1.0.1(2022-06-11) |
| | | 修复苹果手机在微信中无法获取音频长度问题 |
| | | ## 1.0.0(2022-06-10) |
| | | 版本上线 |
New file |
| | |
| | | <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> |
New file |
| | |
| | | { |
| | | "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" |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
New file |
| | |
| | | ## 插件简绍 |
| | | |
| | | ### 实现原理 |
| | | |
| | | > 通过 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 未知错误> | |
| | | |
| | | ### 案例演示 |
| | | |
| | |  |
| | | |
| | | ## 支持作者 |
| | | |
| | |  |
| | |
| | | |
| | | 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, |