本文將通過一個實際的 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
來模擬“上滑取消”操作,直接關閉并丟棄錄音。
完整代碼如下
<template><inputdisabled="true"placeholder="按住 說話"@touchstart="handleTouchStart"@touchmove="handleTouchMove"@touchend="handleTouchEnd"/><AudioLoading :audioLoading="audioLoading" :audioTime="audioTime" />
</template><script setup>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;};
</script>
AudioLoading加載組件
<template><view class="modal-body" v-if="audioLoading"><view class="time">{{ audioTime }}</view><view class="sound-waves"><viewv-for="(item, index) in radomHeight":key="index":style="`height: ${item}rpx; margin-top: -${item / 2}rpx;`"></view><view style="clear: both; width: 0; height: 0"></view></view><view class="desc">松開發送,上滑取消</view></view>
</template><script setup>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;};
</script><style scoped lang="scss">.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;}
</style>
注意如果內嵌到微信小程序中開發環境 會直接拒絕權限
必須部署到http環境才可以