本文将通过一个实际的 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>
<input
disabled="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">
<view
v-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环境才可以