前言
在业务开发的时候,以为网上有很多现成的播放器可以引入第三方的来使用,异想天开的觉得引入一下就可以改一改就可以实现了,没想到网上五花八门,根本使用不了美梦破碎,最后只能自己动手,手撸一个vue3.x ➕ ts版本的音乐播放器。
技术栈
vue3.x + ts 为了贴近前端的技术前沿,肯定要用最新的技术栈啦,使用vue3.x 的组合式api开发完成 代码聚合在一个useHooks中,别提有多爽了,清晰脱俗
传送门
github.com/vintonHuang… 目前没有办法做成npm包让大家使用,所以只能让大家如果有需要的话,可以直接在里面搬逻辑,修改音乐资源,修改样式。因为这种东西是在是太自定义了
话不多说,直接上代码,懂代码的自然懂,可以直接去下载仓库去看,会使用到一些工具类的函数。ts代码有注释,函数都有什么作用。相信你一看就能会,有什么建议可以提issues,一起讨论
vue文件模版代码
<!--
* @Author: Vinton
* @Date: 2022-04-29 14:35:24
* @Description: 模版代码
-->
<template>
<div class="audio-container">
<div class="btn-change" @click="changeNextBatch"></div>
<div class="music-image">
<img
class="animate-img"
:src="importResource(currentMusicImageUrl)"
alt=""
/>
</div>
<div class="process">
<span class="process-startTime">{{ currentProcessTime }}</span>
<span id="process" class="process-content" @click="changeProgress">
<div
class="process-content-line"
:style="{ width: progressNum + '%' }"
></div>
<div class="process-content-points"></div>
</span>
<span class="process-entTime">{{ currentTotalTime }}</span>
</div>
<div class="operation-btn">
<div class="operation-btn-pre" @click="changePreMusic"></div>
<div
:class="[audioIsPlaying ? 'pause-btn' : 'play-btn']"
@click="audioPlayOrPause"
></div>
<div class="operation-btn-next" @click="changeNextMusic"></div>
</div>
<div class="music-content">
<ul>
<li
v-for="(item, index) in currentAudioMenus"
:key="index"
:class="{ acitve: isActiveIndex === index }"
@click="playMusicOnMusicList(index, item)"
>
<span v-if="isActiveIndex === index" class="music-wave"> </span>
<span v-else>{{ index + 1 }}</span>
<span>{{ item.name }}</span>
<span>{{ item.author }}</span>
<span>{{ item.duration }}</span>
</li>
</ul>
</div>
</div>
<audio
class="music-feast"
:src="importResource(currentMusicSrc)"
preload="auto"
:autoPlay="false"
style="display: none"
@timeupdate="timeupdate"
@ended="playEnded"
@loadedmetadata="loadedMetaData"
></audio>
</template>
<script lang="ts">
export default {
name: "AudioModel",
};
</script>
<script setup lang="ts">
import { useAudioAction } from "./composables/useAudioAction";
import { useImportResForOss } from "@/hooks/useImportResForOss";
const { importResource } = useImportResForOss();
const {
currentAudioMenus,
isActiveIndex,
audioIsPlaying,
audioPlayOrPause,
playMusicOnMusicList,
currentMusicImageUrl,
currentMusicSrc,
changePreMusic,
changeNextMusic,
currentProcessTime,
timeupdate,
playEnded,
loadedMetaData,
currentTotalTime,
progressNum,
seeked,
changeProgress,
changeNextBatch,
} = useAudioAction();
</script>
<style scoped lang="less">
@import url("../../styles/mixin.less");
.audio-container {
.main-bg(750px,1086px,"@/assets/bg_03.jpg");
.btn-change {
position: relative;
left: 600px;
top: 10px;
.main-bg(142px, 49px, "@/assets/btn-change.png");
}
.music-image {
margin: 0 auto;
.main-bg(367px, 367px, "@/assets/cover_bg.png");
position: relative;
img {
position: absolute;
left: 42px;
top: 40px;
width: 280px;
height: 280px;
}
}
.process {
font-size: 22px;
color: #a64c3c;
line-height: 56px;
cursor: pointer;
display: flex;
align-items: center;
position: relative;
left: 42px;
&-content {
width: 520px;
height: 2px;
border: 2px solid #dbb0a6;
margin: 0 18px;
display: flex;
align-items: center;
&-line {
height: 2px;
background: #b7594c;
}
&-points {
width: 15px;
height: 15px;
background: #b7594c;
border-radius: 50%;
}
}
}
.operation-btn {
display: flex;
align-items: center;
position: relative;
left: 245px;
&-pre {
.main-bg(37px, 33px, "@/assets/icon_pre.png");
}
.play-btn {
margin: 0 51px;
.main-bg(85px, 85px, "@/assets/icon_start.png");
}
.pause-btn {
margin: 0 51px;
.main-bg(85px, 85px, "@/assets/icon_pause.png");
}
&-next {
.main-bg(36px, 33px, "@/assets/icon_next.png");
}
}
.music-content {
width: 735px;
height: 235px;
opacity: 0.72;
background: #fffdf1;
border-radius: 3px;
margin: 22px auto;
padding-top: 15px;
ul {
display: flex;
flex-direction: column;
justify-content: space-around;
font-size: 22px;
text-align: left;
line-height: 52px;
color: #b6594b;
li {
cursor: pointer;
.music-wave {
.main-bg(25px, 25px, "@/assets/voice.png");
}
display: flex;
justify-content: space-around;
text-align: left;
align-items: center;
span:nth-child(2) {
width: 200px;
}
span:nth-child(3) {
width: 200px;
}
span:nth-child(4) {
width: 50px;
}
&.acitve {
color: #dfb155;
}
}
}
}
}
</style>
组合式音乐播放器代码
/*
* @Author: Vinton
* @Date: 2022-03-31 22:02:35
* @Description: 音乐播放器逻辑代码
*/
import FeastService from "@/service/audio";
import { audioInfo } from "@/interfaces/audio";
import { onMounted, ref, watch, computed, reactive, nextTick } from "vue";
import { formatSeconds } from "@/utils/index";
export const useAudioAction = () => {
const audioMenus = ref<Array<audioInfo>[]>([]);
const audioIndex = ref<number>(0); // 批次
let audio: HTMLAudioElement | undefined;
let image: HTMLImageElement | undefined;
onMounted(async () => {
const { state, data } = await FeastService.getAudioInfo();
if (state === 200) {
audioMenus.value = data.data;
}
audio = Array.from(document.getElementsByTagName("audio"))?.find(
(item) => item.className === "music-feast"
);
image = Array.from(document.getElementsByTagName("img"))?.find(
(item) => item.className === "animate-img"
);
nextTick(() => {
totalProcess = document.getElementById("process");
});
});
const currentAudioMenus = computed(() => {
return audioMenus.value[audioIndex.value];
});
// 切换下一首歌曲
const changeNextBatch = () => {
handlePlayEnd();
audioIndex.value += 1;
};
// 控制下一批次可以循环到第一批次
watch(
() => audioIndex.value,
(newVal) => {
if (newVal > audioMenus.value.length - 1) {
audioIndex.value = 0;
}
}
);
const isActiveIndex = ref<number>(-1);
const audioIsPlaying = ref<boolean>(false);
const currentMusicItem = reactive<audioInfo>({} as audioInfo);
let flag: number;
let timer: NodeJS.Timeout;
// 旋转图片
const rotate = () => {
let deg = 0;
flag = 1;
timer = setInterval(function () {
if (image != undefined) {
image.style.transform = "rotate(" + deg + "deg)";
}
deg += 1;
if (deg > 360) {
deg = 0;
}
}, 30);
};
const imagePause = () => {
clearInterval(timer);
flag = 0;
};
// 点击底下的按钮播放音乐和暂停音乐
const audioPlayOrPause = () => {
audioIsPlaying.value = !audioIsPlaying.value;
if (!currentMusicItem.audioSrc) {
// 一进入页面,进行点击了暂停播放按钮,默认播放第一首歌
setCurrentAudioRes(currentAudioMenus.value[0]);
isActiveIndex.value = 0;
rotate();
return;
}
if (flag) {
imagePause();
audio?.pause();
} else {
rotate();
audio?.play();
}
};
// 点击列表播放音乐
const playMusicOnMusicList = (index: number, item: audioInfo) => {
if (isActiveIndex.value === index) {
return;
}
imagePause();
setCurrentAudioRes(item);
audioIsPlaying.value = true;
isActiveIndex.value = index;
rotate();
};
// 点击上一首音乐
const changePreMusic = (): void => {
if (!currentMusicItem.audioSrc) {
// 如果当前没有音乐选择播放就直接操作无效
return;
}
if (isActiveIndex.value == 0) {
playMusicOnMusicList(3, currentAudioMenus.value[3]);
} else {
playMusicOnMusicList(
isActiveIndex.value - 1,
currentAudioMenus.value[isActiveIndex.value - 1]
);
}
};
// 点击下一首音乐
const changeNextMusic = (): void => {
if (!currentMusicItem.audioSrc) {
return;
}
if (isActiveIndex.value == 3) {
playMusicOnMusicList(0, currentAudioMenus.value[0]);
} else {
playMusicOnMusicList(
isActiveIndex.value + 1,
currentAudioMenus.value[isActiveIndex.value + 1]
);
}
};
// 设置当前播放音乐的资源
const setCurrentAudioRes = (item: audioInfo): void => {
currentMusicItem.url = item.url;
currentMusicItem.name = item.name;
currentMusicItem.duration = item.duration;
currentMusicItem.author = item.author;
currentMusicItem.audioSrc = item.audioSrc;
};
const currentMusicImageUrl = computed(() => {
return currentMusicItem.url
? currentMusicItem.url
: "/resource/assets/audio/01/3/Juvenile.png";
});
const currentMusicSrc = computed(() => {
return currentMusicItem.audioSrc ? currentMusicItem.audioSrc : "";
});
const handleTimeFormat = (value: number) => {
if (!value) {
return "00:00";
}
let timeInfo = formatSeconds(value);
if (timeInfo.substring(0, 2) === "00") {
timeInfo = timeInfo.substring(3);
}
return timeInfo;
};
const currTime = ref<number>(0);
const currentProcessTime = computed(() => {
return handleTimeFormat(currTime.value);
});
const totalTime = ref<number>(0);
const currentTotalTime = computed(() => {
return handleTimeFormat(totalTime.value);
});
// 当前歌曲资源的总时长
const loadedMetaData = (e: any) => {
totalTime.value = parseInt(e.currentTarget.duration);
};
// 目前歌曲播放到哪里的时间
const timeupdate = (e: any) => {
currTime.value = parseInt(e?.currentTarget?.currentTime);
};
// 音乐播放完毕需要清空播放资源数据
const playEnded = () => {
handlePlayEnd();
};
const handlePlayEnd = () => {
audioIsPlaying.value = false;
isActiveIndex.value = -1;
imagePause();
// 清空当前数据
setCurrentAudioRes({
url: "",
audioSrc: "",
duration: "",
name: "",
author: "",
});
};
// 播放进度
const progressNum = computed(() => {
if (!currTime.value || !totalTime.value) {
return 0;
}
return (currTime.value / totalTime.value) * 100;
});
// 跳转歌曲进度
const seeked = (progressNum: number) => {
const seekTime = totalTime.value * (progressNum / 100);
if (audio !== undefined) {
audio.currentTime = seekTime;
}
};
let totalProcess: HTMLElement | null;
const changeProgress = (e: any) => {
if (!audioIsPlaying.value) {
return;
}
let progressPercent: number;
if (totalProcess !== null) {
progressPercent =
((e.pageX - totalProcess?.getBoundingClientRect().left) /
totalProcess?.getBoundingClientRect().width) *
100;
seeked(progressPercent > 0 ? progressPercent : 0);
}
};
return {
currentAudioMenus,
isActiveIndex,
audioIsPlaying,
audioPlayOrPause,
playMusicOnMusicList,
currentMusicImageUrl,
currentMusicSrc,
changePreMusic,
changeNextMusic,
currentProcessTime,
timeupdate,
playEnded,
loadedMetaData,
currentTotalTime,
progressNum,
seeked,
changeProgress,
changeNextBatch,
};
};
结束语
我是黄老师要变胖,一位卑微前端打工仔,分享一个在开发中,手撸的音乐小组件,希望可以帮到你,最好是只要cv就可以完事,早早下班,早早摸鱼🐟