vue3实现实现手机/PC端录音:recorder-core

通过 recorder-core 这个插件实现录音

recorder-core插件使用

下方的js文件是安装后封装的一个js文件,在需要使用的地方直接引入这个文件:import record from "./recorderCore.js";

js 复制代码
//  文件名称:recorderCore.js

// recorder-core插件使用方式:https://huaweicloud.csdn.net/6549fb3434bf9e25c799ca07.html?dp_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MTI5NDc5OSwiZXhwIjoxNzUzOTM3ODg5LCJpYXQiOjE3NTMzMzMwODksInVzZXJuYW1lIjoic2lzdWliYW4ifQ.Y2_R3XsABjzRvhML0rdYMuGJhYrIDM-rrPob4RDJtro&spm=1001.2101.3001.6650.6&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Eactivity-6-131902136-blog-147322532.235%5Ev43%5Econtrol&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Eactivity-6-131902136-blog-147322532.235%5Ev43%5Econtrol&utm_relevant_index=13
// 例子 https://blog.csdn.net/weixin_47137972/article/details/147322532?ops_request_misc=%257B%2522request%255Fid%2522%253A%25226a538d344bc89fef66029e7d7a2a0b06%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=6a538d344bc89fef66029e7d7a2a0b06&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~sobaiduend~default-1-147322532-null-null.nonecase&utm_term=vue%E5%AE%9E%E7%8E%B0%E5%BD%95%E9%9F%B3%E5%8A%9F%E8%83%BD&spm=1018.2226.3001.4450
//必须引入的核心
import Recorder from 'recorder-core';

//引入mp3格式支持文件;如果需要多个格式支持,把这些格式的编码引擎js文件放到后面统统引入进来即可
import 'recorder-core/src/engine/mp3';
import 'recorder-core/src/engine/mp3-engine';
//录制wav格式的用这一句就行
import 'recorder-core/src/engine/wav';



const record = {
    RecordApp: null,
    recBlob: null,
    /**麦克风授权 */
    getPermission: (fn) => {
        const newRec = Recorder({
            type: 'wav',
            bitRate: 16,
            sampleRate: 16000, //阿里采样率16000
            onProcess: function (buffers, powerLevel, duration, bufferSampleRate) {
                // console.log(buffers);
            },
        });

        //打开录音,获得权限
        newRec.open(
            () => {
                record.RecordApp = newRec;
                fn({ status: 'success', data: '开启成功' });
            },
            (msg, isUserNotAllow) => {
                //用户拒绝了录音权限,或者浏览器不支持录音
                fn({ status: 'fail', data: msg });
                // console.log((isUserNotAllow ? 'UserNotAllow,' : '') + '无法录音:' + msg);
            }
        );
    },
    /**开始录音 */
    startRecorder: () => {
        if (record.RecordApp && Recorder.IsOpen()) {
            record.RecordApp.start();
        }
    },
    /** 停止录音 */
    stopRecorder: (fn) => {
        try {
            if (!record) {
                // console.error('未打开录音');
                return;
            }
            record.RecordApp.stop((blob, duration) => {
                // console.log('录音成功', blob, '时长:' + duration + 'ms');
                if (blob) {
                    record.recBlob = blob;
                    const url = URL.createObjectURL(blob);
                    const formData = new FormData();
                    formData.append('audio', blob);
                    fn({ loading: true }, url, blob);
                }
                /* eslint-enable */
                record.RecordApp.close();
                record.RecordApp = null;
            });
        } catch (err) {
            fn({ err: err });
            // console.error('结束录音出错:' + err);
            record.RecordApp.close();
            record.RecordApp = null;
        }
    },

    /**关闭录音,释放麦克风资源 */
    destroyRecorder: () => {
        if (record.RecordApp) {
            record.RecordApp.close();
            record.RecordApp = null;
        }
    },

    /**暂停 */
    pauseRecorder: () => {
        if (record.RecordApp) {
            record.RecordApp.pause();
        }
    },
    /**恢复继续录音 */
    resumeRecorder: () => {
        if (record.RecordApp) {
            record.RecordApp.resume();
        }
    },
};

export default record;

接下来就是使用上方的js文件

vue 复制代码
import record from "./config/recorderCore.js";

// 点击了开始录音按钮
const onclick_luyin = () => {
  record.getPermission(function (permiss) {
    if (permiss.status == "fail") {
      ElMessage.error(permiss.data); //这里直接写一个报错提示,我这用的是elementUI的
    } else {
      record.startRecorder(); //开始录音
    }
  });
};
// 录音停止按钮
const onclick_guanbi = () => {
  record.stopRecorder((res, url, blob) => {
    if (blob && blob.size) {
      console.log("文件大小:", blob.size);
      // 调用语音转文字的接口
      AudioToText(blob);
    }
  });
};
// 语音转文字
const AudioToText = async (file) => {
  const maxSize = 14 * 1024 * 1024; // 14MB
  if (file.size > maxSize) {
    ElMessage.error("语音文件的大小超过14MB,请重新录音");
    return;
  }

  const baseurl = `${DEEPSEEK_CONFIG.baseURL}/audio-to-text`;
  const apiKey = `${DEEPSEEK_CONFIG.apiKey}`;
  // 强制指定 MIME 为 audio/mp3
  const newFile = new File([file], "语音", { type: "audio/mp3" });
  const formData = new FormData();
  formData.append("file", newFile);

  try {
    const response = await fetch(baseurl, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
      },
      body: formData,
    });

    const text = await response.text();

    try {
      const data = JSON.parse(text);
      if (response.ok) {
        queryKeys.value = data.text;
      } else {
        queryKeys.value = "";
        ElMessage.error(`语音转文字接口错误:${JSON.stringify(data)}`);
      }
    } catch {
      queryKeys.value = "";
      ElMessage.error(`语音转文字接口响应不是 JSON:${text}`);
    }
  } catch (error) {
    queryKeys.value = "";
    ElMessage.error(`语音转文字接口请求异常:${error.message}`);
  }
};

提供一个vue录音动画组件

vue 复制代码
<template>
  <transition name="modal-fade">
    <div v-if="isOpen" class="modal-overlay" @click="handleOverlayClick">
      <!-- 科技感网格背景 -->
      <div class="tech-grid"></div>
      
      <!-- 扫描线效果 -->
      <div class="scan-line" :class="{ active: props.isOpen }"></div>
      
      <div class="modal-container">
        <!-- 装饰性光效 -->
        <div class="glow-effect top"></div>
        <div class="glow-effect bottom"></div>
        
        <div class="audio-animation-container">
          <!-- 高科技风格波形容器 -->
          <div class="wave-container">
            <div
              v-for="(bar, index) in bars"
              :key="index"
              class="wave-bar"
              :style="{
                height: bar.height,
                background: bar.gradient,
                boxShadow: bar.glow,
                transitionDelay: `${bar.delay}ms`,
                transform: `scaleX(${bar.scale})`
              }"
            ></div>
            
            <!-- 倒计时显示 - 现在位于波形中央 -->
            <div class="countdown-display">
              <span class="countdown-number">{{ countdown }} / 60</span>
            </div>
          </div>
        </div>
        
        <!-- 状态指示器 -->
        <div class="status-indicator">
          <div class="pulse-dot"></div>
          <span style="color:#fff">可以开始讲话了,总共能讲60秒</span>
        </div>
      </div>
    </div>
  </transition>
</template>

<script setup>
import { ref, onBeforeUnmount, watch, onUnmounted } from "vue";

const props = defineProps({
  isOpen: {
    type: Boolean,
    required: true,
  },
  // 科技感主色调
  primaryColor: {
    type: String,
    default: '#fff'
  },
  // 动画速度(毫秒)
  speed: {
    type: Number,
    default: 80
  },
  // 波形条数量
  barCount: {
    type: Number,
    default: 40, // 更多的波形条增强科技感
    validator: (value) => value >= 20 && value <= 40
  },
  // 最大高度比例
  maxHeightRatio: {
    type: Number,
    default: 85,
    validator: (value) => value >= 70 && value <= 100
  }
});

const emit = defineEmits(["close"]);

// 倒计时相关
const countdown = ref(0);
let countdownTimer = null;

// 关闭模态框
const closeModal = () => {
  stopAnimation();
  stopCountdown();
  emit("close");
};

// 点击遮罩关闭
const handleOverlayClick = () => {
  closeModal();
};

// 生成科技感渐变色
const generateGradient = (index) => {
  // 基于位置生成微妙的色调变化
  const hueOffset = (index % 10) * 3;
  const baseColor = props.primaryColor;
  const lightColor = shadeColor(baseColor, 30);
  return `linear-gradient(180deg, ${lightColor} 0%, ${baseColor} 70%)`;
};

// 调整颜色明暗度
const shadeColor = (color, percent) => {
  let R = parseInt(color.substring(1, 3), 16);
  let G = parseInt(color.substring(3, 5), 16);
  let B = parseInt(color.substring(5, 7), 16);

  R = parseInt(R * (100 + percent) / 100);
  G = parseInt(G * (100 + percent) / 100);
  B = parseInt(B * (100 + percent) / 100);

  R = (R < 255) ? R : 255;
  G = (G < 255) ? G : 255;
  B = (B < 255) ? B : 255;

  R = Math.round(R);
  G = Math.round(G);
  B = Math.round(B);

  const RR = ((R.toString(16).length === 1) ? "0" + R.toString(16) : R.toString(16));
  const GG = ((G.toString(16).length === 1) ? "0" + G.toString(16) : G.toString(16));
  const BB = ((B.toString(16).length === 1) ? "0" + B.toString(16) : B.toString(16));

  return `#${RR}${GG}${BB}`;
};

// 波形数据数组
const bars = ref([]);
// 初始化波形数据
for (let i = 0; i < props.barCount; i++) {
  const color = generateGradient(i);
  const glowColor = shadeColor(props.primaryColor, 50);
  bars.value.push({
    height: '5%',
    gradient: color,
    glow: `0 0 8px ${glowColor}, 0 0 12px ${glowColor}33`,
    delay: calculateDelay(i, props.barCount),
    scale: 1
  });
}

// 计算每个波形条的动画延迟,创建同步波动效果
function calculateDelay(index, total) {
  // 创造波浪式延迟模式,增强科技感
  return (index % 5) * 40;
}

// 动画定时器
let animationTimer = null;
let pulseTimer = null;

// 生成更有规律的波形高度,符合高科技感
const generateHeights = () => {
  const newBars = [...bars.value];
  const maxHeight = props.isOpen ? props.maxHeightRatio : 10;
  const minHeight = props.isOpen ? 5 : 3;
  
  // 创建更有规律的波形模式,类似音频频谱
  const time = Date.now() / 500;
  newBars.forEach((bar, index) => {
    // 使用正弦函数创建更流畅的波形
    const frequency = 0.5 + (index / newBars.length) * 2;
    const amplitude = props.isOpen ? 0.5 + Math.random() * 0.5 : 0.2;
    const baseHeight = (maxHeight - minHeight) * 0.5 + minHeight;
    const wave = Math.sin(time * frequency + (index * 0.3)) * amplitude;
    const height = Math.floor(baseHeight * (1 + wave));
    
    // 添加微妙的缩放效果
    const scale = props.isOpen ? 1 + (wave * 0.1) : 1;
    
    newBars[index] = {
      ...bar,
      height: `${height}%`,
      scale: scale,
      gradient: generateGradient(index)
    };
  });
  
  bars.value = newBars;
};

// 启动动画
const startAnimation = () => {
  if (animationTimer) clearInterval(animationTimer);
  if (pulseTimer) clearInterval(pulseTimer);
  
  generateHeights();
  animationTimer = setInterval(generateHeights, props.speed);
  
  // 启动脉冲效果
  pulseTimer = setInterval(() => {
    const glowElements = document.querySelectorAll('.glow-effect');
    glowElements.forEach(el => {
      el.classList.add('pulse');
      setTimeout(() => el.classList.remove('pulse'), 500);
    });
  }, 2000);
};

// 停止动画并重置
const stopAnimation = () => {
  if (animationTimer) {
    clearInterval(animationTimer);
    animationTimer = null;
  }
  if (pulseTimer) {
    clearInterval(pulseTimer);
    pulseTimer = null;
  }
  
  // 平滑重置为低波形
  const newBars = [...bars.value].map(bar => ({
    ...bar,
    height: '5%',
    scale: 1
  }));
  bars.value = newBars;
};

// 启动倒计时
const startCountdown = () => {
  // 重置倒计时
  countdown.value = 0;
  
  // 清除现有定时器
  if (countdownTimer) {
    clearInterval(countdownTimer);
  }
  
  // 设置新定时器
  countdownTimer = setInterval(() => {
    countdown.value++;
    
    // 当倒计时达到60时关闭模态框
    if (countdown.value >= 60) {
      handleOverlayClick();
    }
  }, 1000);
};

// 停止倒计时
const stopCountdown = () => {
  if (countdownTimer) {
    clearInterval(countdownTimer);
    countdownTimer = null;
  }
};

// 监听isOpen状态变化,控制动画和倒计时
watch(
  () => props.isOpen,
  (newVal) => {
    if (newVal) {
      startAnimation();
      startCountdown();
    } else {
      stopCountdown();
    }
  },
  { immediate: true }
);

// 监听颜色变化
watch(
  () => props.primaryColor,
  (newVal) => {
    const updatedBars = [...bars.value].map((bar, index) => {
      const color = generateGradient(index);
      const glowColor = shadeColor(newVal, 50);
      return {
        ...bar,
        gradient: color,
        glow: `0 0 8px ${glowColor}, 0 0 12px ${glowColor}33`
      };
    });
    bars.value = updatedBars;
  }
);

// 组件卸载时清理
onBeforeUnmount(() => {
  stopAnimation();
  stopCountdown();
});

onUnmounted(() => {
  if (animationTimer) clearInterval(animationTimer);
  if (pulseTimer) clearInterval(pulseTimer);
  if (countdownTimer) clearInterval(countdownTimer);
});
</script>

<style scoped>
/* 高科技风格配色方案 */
:root {
  --tech-blue: #00e5ff;
  --tech-dark: #0a1929;
  --tech-darker: #050f1a;
  --tech-light: #64ffda;
  --glow: 0 0 10px var(--tech-blue), 0 0 20px rgba(0, 229, 255, 0.3);
  --glow-strong: 0 0 15px var(--tech-blue), 0 0 30px rgba(0, 229, 255, 0.5);
  --transition-fast: all 0.1s ease-out;
  --transition-slow: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

.modal-overlay {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 200px;
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 1000;
  height: 200px;
  overflow: hidden;
}

/* 科技感网格背景 */
.tech-grid {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-image: 
    linear-gradient(rgba(0, 229, 255, 0.1) 1px, transparent 1px),
    linear-gradient(90deg, rgba(0, 229, 255, 0.1) 1px, transparent 1px);
  background-size: 20px 20px;
  z-index: -1;
  animation: gridMove 8s linear infinite;
}

/* 扫描线效果 */
.scan-line {
  position: absolute;
  top: -5%;
  left: 0;
  right: 0;
  height: 2px;
  background: linear-gradient(90deg, transparent, var(--tech-blue), transparent);
  opacity: 0.3;
  z-index: 1;
  transition: opacity 0.5s ease;
}

.scan-line.active {
  opacity: 0.6;
  animation: scan 3s linear infinite;
}

@keyframes scan {
  0% { top: -5%; }
  100% { top: 105%; }
}

@keyframes gridMove {
  0% { background-position: 0 0; }
  100% { background-position: 20px 20px; }
}

.modal-container {
  position: relative;
  background-color: var(--tech-dark);
  border: 1px solid rgba(0, 229, 255, 0.3);
  border-radius: 8px;
  backdrop-filter: blur(10px);
  box-shadow: var(--glow);
  width: 90%;
  max-width: 600px;
  padding: 20px;
  display: flex;
  flex-direction: column;
  align-items: center;
  overflow: hidden;
}

/* 装饰性光效 */
.glow-effect {
  position: absolute;
  left: 0;
  right: 0;
  height: 2px;
  background: linear-gradient(90deg, transparent, var(--tech-blue), transparent);
  opacity: 0.6;
  transition: var(--transition-slow);
}

.glow-effect.top { top: 0; }
.glow-effect.bottom { bottom: 0; }

.glow-effect.pulse {
  opacity: 1;
  box-shadow: var(--glow-strong);
}

.audio-animation-container {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 140px;
}

.wave-container {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 2px; /* 更紧密的波形条 */
  width: 100%;
  height: 100%;
  position: relative; /* 新增:为了让倒计时能绝对定位在波形内部 */
}

.wave-bar {
  width: 3px; /* 更细的波形条 */
  border-radius: 1px;
  transition: var(--transition-fast);
  transform-origin: center bottom;
}

/* 倒计时显示样式 - 现在位于波形中央 */
.countdown-display {
  position: absolute;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  color: var(--tech-blue);
  font-family: 'Courier New', monospace;
  z-index: 2; /* 确保在波形条上方显示 */
  pointer-events: none; /* 允许点击穿透到下方的波形 */
}

.countdown-number {
  font-size: 24px;
  font-weight: 600;
  font-weight: bold;
  text-shadow: 0 0 8px var(--tech-blue), 0 0 12px rgba(0, 229, 255, 0.5);
  line-height: 1;
}

.countdown-label {
  font-size: 10px;
  opacity: 0.8;
  margin-top: 2px;
}

/* 状态指示器 */
.status-indicator {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 10px;
}

.status-indicator span {
  color: var(--tech-blue);
  font-family: 'Courier New', monospace;
  font-size: 11px;
  letter-spacing: 1px;
  opacity: 0.8;
}

.pulse-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background-color: #ff3e3e;
  animation: pulse 1.5s infinite;
}

@keyframes pulse {
  0% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(255, 62, 62, 0.7); }
  70% { transform: scale(1); box-shadow: 0 0 0 6px rgba(255, 62, 62, 0); }
  100% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(255, 62, 62, 0); }
}

/* 过渡动画 */
.modal-fade-enter-active,
.modal-fade-leave-active {
  transition: opacity 0.4s ease, transform 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
}

.modal-fade-enter-from {
  opacity: 0;
  transform: translateY(20px) scale(0.98);
}

.modal-fade-leave-to {
  opacity: 0;
  transform: translateY(10px) scale(0.99);
}
</style>

组件使用:

vue 复制代码
<AudioWaveAnimation
       :isOpen="isRecording"
       :primaryColor="getRandomShadow()"
       @close="onclick_guanbi"
     />

// js
import AudioWaveAnimation from "./component/AudioWaveAnimation.vue";

// 组件的显示与隐藏开关
const isRecording = ref(false);
// 生成随机颜色
function getRandomShadow() {
 const hue = Math.floor(Math.random() * 360); // 色相(0-360)
 const saturation = Math.floor(Math.random() * 30) + 70; // 饱和度(70%-100%)
 const lightness = Math.floor(Math.random() * 20) + 60; // 明度(60%-80%)
 const alpha = 0.3 + Math.random() * 0.3; // 透明度(0.3-0.6)
 // 直接返回hsla颜色值
 return `hsla(${hue}, ${saturation}%, ${lightness}%, ${alpha})`;
}