
通过 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})`;
}