vue3 简易的pc端音频播放器组件
实现的功能
- 播放/暂停
- 拖动进度条
- 显示当前播放时间
- 显示总时长
- 显示音频文件名
封装组件 AudioPlayer.vue
<template><div class="audio-wrapper"><audio size="#4.50MB" ref="audioRef":data-duration="totalDuration"@loadedmetadata="onMetadataLoaded"><source :src=audioSrc type="audio/mp3"></audio><div class="audio-left" @click="togglePlay"><imgid="audioPlayer":src="isPlaying ? pauseImg: playImg"alt="播放/暂停"/></div><div class="audio-right"><p style="max-width: 536px;">{{ audioFileName }}</p><div class="progress-bar-bg" id="progressBarBg" @click="handleProgressClick" ref="progressBarBg"><!-- 进度点 --><spanid="progressDot":style="{ left: progressWidth + '%' }"@mousedown="startDrag"@touchstart="startDrag"></span><!-- 进度条 --><div id="progressBar" class="progress-bar" :style="{ width: progressWidth + '%' }"></div></div><div class="audio-time"><span class="audio-length-current" id="audioCurTime">{{ currentTimeFormatted }}</span><span class="audio-length-total">{{ totalDurationFormatted }}</span></div></div></div></template><script>import { ref, onMounted, onUnmounted } from "vue";import pause from './image/pause.png' //暂停图标import play from './image/play.png' //播放图标export default {name: "AudioPlayer",props: {// 音频源地址audioSrc: {type: String,required: true,},// 音频文件名audioFileName: {type: String,default: "音频文件",},},setup(props) {const audioRef = ref(null); // 音频元素引用const isPlaying = ref(false); // 播放/暂停状态const progressWidth = ref(0); // 进度条宽度百分比const currentTimeFormatted = ref("00:00"); // 当前播放时间格式化显示const totalDurationFormatted = ref("00:00"); // 音频总时长格式化显示const dragFlag = ref(false); // 拖动状态标志const pauseImg = ref(pause)const playImg = ref(play)const dragPosition = ref({oriOffestLeft: 0, // 起始偏移量oriX: 0, // 起始X坐标maxLeft: 0, // 最大左偏移量maxRight: 0, // 最大右偏移量});const totalDuration = ref(0);const MOVE_EVENTS = {mouse: ['mousemove', 'mouseup'],touch: ['touchmove', 'touchend']};// 切换播放/暂停const togglePlay = () => {const audio = audioRef.value;if (audio.paused) {audio.play();isPlaying.value = true;} else {audio.pause();isPlaying.value = false;}};// 更新进度const updateProgress = () => {const audio = audioRef.value;if (!audio) return;// 计算进度百分比const value = audio.currentTime / audio.duration;progressWidth.value = value * 100;// 更新当前时间显示currentTimeFormatted.value = formatTime(audio.currentTime);};// 点击进度条跳转const handleProgressClick = (event) => {const audio = audioRef.value;if (!audio || (audio.paused && audio.currentTime === 0)) return;const progressBarBg = event.currentTarget;const pgsWidth = progressBarBg.getBoundingClientRect().width;const rate = event.offsetX / pgsWidth;// 设置音频当前播放时间audio.currentTime = audio.duration * rate;};// 开始拖动进度点const startDrag = (event) => {const audio = audioRef.value;if (!audio || (audio.paused && audio.currentTime === 0)) return;dragFlag.value = true;// 获取进度条背景元素const progressBarBg = document.getElementById("progressBarBg");// 记录起始位置信息dragPosition.value.oriOffestLeft = event.target.offsetLeft;dragPosition.value.oriX =event.touches ? event.touches[0].clientX : event.clientX;dragPosition.value.maxLeft = dragPosition.value.oriOffestLeft;dragPosition.value.maxRight = progressBarBg.offsetWidth -dragPosition.value.oriOffestLeft;// 禁止默认事件和冒泡event.preventDefault();event.stopPropagation();};// 拖动进度点时更新进度const handleMouseMove = (event) => {if (!dragFlag.value) return;const audio = audioRef.value;if (!audio) return;const clientX = event.touches ? event.touches[0].clientX : event.clientX;const length = clientX - dragPosition.value.oriX;const progressBarBg = document.getElementById("progressBarBg");// const pgsWidth = parseFloat(// window.getComputedStyle(progressBarBg).width.replace("px", "")// );const pgsWidth = progressBarBg.getBoundingClientRect().width;let offsetLeft = dragPosition.value.oriOffestLeft + length;offsetLeft = Math.max(0, Math.min(offsetLeft, pgsWidth));const rate = offsetLeft / pgsWidth;audio.currentTime = audio.duration * rate;updateProgress();};// 结束拖动const endDrag = () => {dragFlag.value = false;};// 监听音频元数据加载完成事件const onMetadataLoaded = () => {const audio = audioRef.value;if (!audio) return;totalDuration.value = audio.duration;totalDurationFormatted.value = formatTime(audio.duration);};// 格式化时间const formatTime = (value) => {const h = Math.floor(value / 3600);const m = Math.floor( (value %3600) / 60);const s = Math.floor(value % 60);if (h > 0) {return`${h.toString().padStart(2, "0")}:${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`;} else {return `${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`;}};// 监听音频事件onMounted(() => {const audio = audioRef.value;// 监听时间更新audio.addEventListener("timeupdate", updateProgress);// 监听播放完成audio.addEventListener("ended", () => {progressWidth.value = 0;currentTimeFormatted.value = "00:00";isPlaying.value = false;});audio.addEventListener('loadedmetadata', onMetadataLoaded); // 监听鼠标移动和抬起事件document.addEventListener("mousemove", handleMouseMove);document.addEventListener("touchmove", handleMouseMove);document.addEventListener("mouseup", endDrag);document.addEventListener("touchend", endDrag);});onUnmounted(() => {const audio = audioRef.value;// 移除事件监听器audio.removeEventListener("timeupdate", updateProgress);audio.removeEventListener("ended", () => {});audio.removeEventListener('loadedmetadata', onMetadataLoaded);Object.values(MOVE_EVENTS).flat().forEach(eventName => {document.removeEventListener(eventName, eventName.includes('move') ? handleMouseMove : endDrag);});});return {pauseImg,playImg,audioRef,isPlaying,progressWidth,currentTimeFormatted,togglePlay,handleProgressClick,startDrag,totalDurationFormatted,totalDuration,};},};
</script><style scoped>
.audio-wrapper {background-color: #fcfcfc;margin: 10px auto;max-width: 670px;height: 70px;border: 1px solid #e0e0e0;color: #3e3e3e;
}.audio-left {float: left;text-align: center;width: 18%;height: 100%;
}.audio-left img {width: 40px;position: relative;top: 15px;margin: 0;display: initial; /* 解除与app的样式冲突 */cursor: pointer;
}.audio-right {margin-right: 2%;float: right;width: 80%;height: 100%;
}.audio-right p {font-size: 15px;height: 35%;margin: 12px 0 2px 0;/* 歌曲名称只显示在一行,超出部分显示为省略号 */overflow: hidden;white-space: nowrap;text-overflow: ellipsis;max-width: 243px; /* 要适配小屏幕手机,所以最大宽度先设小一点,后面js根据屏幕大小重新设置 */
}.progress-bar-bg {background-color: #d9d9d9;position: relative;height: 2px;cursor: pointer;
}.progress-bar {background-color: #649fec;width: 0;height: 2px;
}.progress-bar-bg span {content: " ";width: 10px;height: 10px;border-radius: 50%;-moz-border-radius: 50%;-webkit-border-radius: 50%;background-color: #3e87e8;position: absolute;left: 0;top: 50%;margin-top: -5px;margin-left: -5px;cursor: pointer;
}.audio-time {overflow: hidden;margin-top: 1px;
}.audio-length-total {float: right;font-size: 12px;
}.audio-length-current {float: left;font-size: 12px;
}
</style>
使用组件
<script setup lang="ts">
import AudioPlayer from "./components/AudioPlayer.vue";
import audioUrl from "./components/image/测试.mp3";
</script><template><h1>hello vue1113</h1><AudioPlayer:audioSrc=audioUrl:audioFileName="'My Audio File'"/>
</template>