在h5端实现录音发送功能(兼容内嵌微信小程序) recorder-core

本文将通过一个实际的 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环境才可以

相关推荐
^Rocky2 小时前
微信小程序(uniapp)实现腾讯云 IM 消息撤回
微信小程序·uni-app·腾讯云
疯狂的沙粒4 小时前
uniapp开发企业微信小程序时 wx.qy.login 在uniapp中使用的时候,需要导包吗?
前端·javascript·微信小程序·小程序·uni-app
1099054187 小时前
微信小程序进阶第2篇__事件类型_冒泡_非冒泡
微信小程序
疯狂的沙粒12 小时前
uniapp 开发企业微信小程序时,如何在当前页面真正销毁前或者关闭小程序前调用一个api接口
微信小程序·小程序·uni-app
山河故人16312 小时前
UniApp微信小程序自定义导航栏实现
微信小程序·uni-app·notepad++
魔术师ID13 小时前
微信小程序学习目录
学习·微信小程序·小程序
说私域1 天前
多级体验体系构建:基于开源AI智能客服与AI智能名片的S2B2C商城小程序体验升级路径研究
人工智能·小程序·开源·零售
^Rocky1 天前
微信小程序(uniapp)对接腾讯云IM
微信小程序·uni-app·腾讯云
栈狮1 天前
Uniapp+UView+Uni-star打包小程序极简方案
小程序·uni-app