在h5端实现录音发送功能(兼容内嵌微信小程序) recorder-core_vue2 h5端怎么实现发送语音的交互(按住、松开、取消),跟豆包app的效果一样
本文将通过一个实际的 Vue3 组件示例,带你一步步实现“按住录音,松开发送,上滑取消”的语音录制功能。
我们将使用强大且小巧的开源库 recorder-core,支持 MP3、WAV、AAC 等编码格式,兼容性较好。
🔧 项目依赖
pnpm add recorder-core dayjs# 或npm install recorder-core dayjs
我们实现的组件是一个 input
输入框,按下开始录音,松开结束录音,上滑取消录音。核心逻辑全部由 recorder-core
管理。
✅ 权限处理机制
第一次调用 rec.open()
时会触发麦克风授权窗口,用户点击「允许」后才能真正录音。所以我们用 isAuthorized
标记避免重复弹窗。
✅ 录音时间和状态展示
我们通过 onProcess()
回调实时拿到录音时间和音量等级,再结合 dayjs
把时间格式化展示在 UI 上(audioLoading.vue
可以自定义成动画弹窗或语音时长条等)。
✅ 录音取消(上滑手势)
录音时用户可能不想发送,我们监听 @touchmove
来模拟“上滑取消”操作,直接关闭并丢弃录音。
完整代码如下
import { ref } from \'vue\'; import dayjs from \'dayjs\'; import Recorder from \'recorder-core\'; import \'recorder-core/src/engine/mp3\'; // mp3 封装 import \'recorder-core/src/engine/mp3-engine\'; // mp3 编码核心模块 const audioLoading = ref(false); //语音弹框 const audioTime = ref(0); //语音时间 const isAuthorized = ref(false); // 是否授权 import AudioLoading from \'./audioLoading.vue\';function formatDateToss(inputStr) { return dayjs(inputStr).format(\'mm:ss\'); } let rec = null; /*长按开始录制语音*/ const handleTouchStart = e => { audioTime.value = 0; rec = Recorder({ type: \'mp3\', sampleRate: 16000, bitRate: 16, onProcess(buffers, powerLevel, duration, sampleRate) { audioLoading.value = true; audioTime.value = formatDateToss(duration); }, }); rec.open( () => { if (isAuthorized.value) { rec.start(); } isAuthorized.value = true; }, (msg, isUserNotAllow) => { audioLoading.value = false; console.log(\'停止录音失败: \' + msg); }, ); }; /*语音录制结束*/ const handleTouchEnd = () => { audioLoading.value = false; rec.stop( (blob, duration) => { rec.close(); const url = URL.createObjectURL(blob); console.log(url); }, msg => { rec.close(); console.log(\'停止录音失败: \' + msg); }, ); }; //上滑取消 const handleTouchMove = () => { rec.close(); rec.stop(); audioLoading.value = false; };
AudioLoading加载组件
{{ audioTime }} 松开发送,上滑取消 import { watch, ref } from \'vue\'; import { onLoad } from \'@dcloudio/uni-app\'; const props = defineProps({ audioTime: { type: Number, }, audioLoading: { type: Boolean, default: false, }, }); const radomHeight = ref([ 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, ]); onLoad(() => {}); let timer; watch( () => props.audioLoading, val => { if (val) { timer = setInterval(() => { myradom(); }, 500); } else { clearInterval(timer); } }, ); const myradom = () => { let _radomheight = radomHeight.value; for (var i = 0; i < radomHeight.value.length; i++) { //+1是为了避免为0 _radomheight[i] = 100 * Math.random().toFixed(2) + 10; } radomHeight.value = _radomheight; }; .modal-body { position: fixed; top: 500rpx; left: 235rpx; width: 280rpx; height: 280rpx; background: rgba(0, 0, 0, 0.75); border-radius: 16rpx; backdrop-filter: blur(20rpx); box-sizing: border-box; padding-top: 40rpx; } .time { width: 100%; text-align: center; font-size: 28rpx; font-family: PingFangSC-Regular, PingFang SC; font-weight: 400; color: #ffffff; } .sound-waves { width: 100%; box-sizing: border-box; padding-left: 10%; margin-top: 70rpx; height: 50rpx; text-align: center; } .sound-waves view { transition: all 0.5s; width: 1%; margin-left: 1.5%; margin-right: 1.5%; height: 100rpx; background-color: #ffffff; float: left; } .desc { width: 100%; font-size: 30rpx; font-family: PingFangSC-Regular, PingFang SC; font-weight: 400; color: #ffffff; line-height: 42rpx; text-align: center; margin-top: 20rpx; } .record-btn { width: 584rpx; height: 74rpx; line-height: 74rpx; text-align: center; background: #ffffff; border-radius: 16rpx; font-size: 32rpx; font-family: PingFangSC-Semibold, PingFang SC; font-weight: 600; color: #000000; } .record-btn::after { border: none; }
注意如果内嵌到微信小程序中开发环境 会直接拒绝权限
必须部署到http环境才可以