Vue音乐播放组件,组件外观简约,可歌词同步显示,拖拽播放进度。默认为vue3版本,推荐使用方式二
引入使用,可以更好的优化组件或修改成vue2
版本。
在线体验地址 音乐播放器
npm install apple-music-player
yarn add apple-music-player
import App from './App.vue'
import AppleMusicPlayer from 'apple-music-player'
createApp(App).use(AppleMusicPlayer).mount('#app')
<template>
<div style="padding-top:20%;display: flex;
justify-content: center;
align-items: center;">
<!-- 一般需要有一个div包着,设置div的宽高,播放器跟随父级宽高-->
<div style="height: 180px;width: 500px;border:1px solid #ccc;padding: 20px">
<AppleMusicPlayer progressColor="rgba(211, 16, 16, 0.1)" diskHW="110px" :musicList="musicList" :darkTheme="darkTheme" :offsetY="25" >
<!-- 这些可以换成图标按钮 -->
<template #next>
下一首
</template>
<template #last>
上一首
</template>
<template #pause>
暂停
</template>
<template #play>
播放
</template>
</AppleMusicPlayer>
</div>
</div>
</template>
<script lang="ts" setup>
import {ref} from 'vue'
//主题
const darkTheme=ref(false)
//歌曲列表
const musicList=[
{title:'好运来',img:'https://www.vae.zhangweicheng.xyz/VAE_Article_ShouTu/vae/6802ac85-9f22-4cc4-b812-87238f103c36.jpg',src:'https://www.vae.zhangweicheng.xyz/music/%E7%A5%96%E6%B5%B7%20-%20%E5%A5%BD%E8%BF%90%E6%9D%A5.mp3',lyric:'[00:00.00]好运来-祖海\n' +
'[00:02.60]词:车行\n' +
'[00:05.20]曲:戚建波\n' +
'[00:07.80]好运来祝你好运来\n' +
'[00:11.19]\n' +
'[00:11.79]好运带来了喜和爱\n' +
'[00:16.15]好运来我们好运来\n' +
'[00:20.29]迎着好运兴旺发达通四海\n' +
'[00:24.78]\n' +
'[00:29.17]叠个千纸鹤再系个红飘带\n' +
'[00:32.72]\n' +
'[00:33.23]愿善良的人们天天好运来\n' +
'[00:37.36]你勤劳生活美你健康春常在\n' +
'[00:41.71]你一生的忙碌为了笑逐颜开\n' +
'[00:46.71]\n' +
'[00:47.36]打个中国结请春风剪个彩\n' +
'[00:51.40]愿祖国的日月年年好运来\n' +
'[00:55.56]你凤舞太平年你龙腾新时代\n' +
'[01:00.04]你幸福的家园迎来百花盛开\n' +
'[01:05.36]好运来祝你好运来\n' +
'[01:09.68]好运带来了喜和爱\n' +
'[01:13.91]好运来我们好运来\n' +
'[01:18.17]迎着好运兴旺发达通四海\n' +
'[01:24.22]\n' +
'[01:33.43]叠个千纸鹤再系个红飘带\n' +
'[01:37.46]愿善良的人们天天好运来\n' +
'[01:41.64]你勤劳生活美你健康春常在\n' +
'[01:46.00]你一生的忙碌为了笑逐颜开\n' +
'[01:50.95]\n' +
'[01:51.46]打个中国结请春风剪个彩\n' +
'[01:55.74]愿祖国的日月年年好运来\n' +
'[01:59.87]你凤舞太平年你龙腾新时代\n' +
'[02:04.18]你幸福的家园迎来百花盛开\n' +
'[02:09.62]好运来祝你好运来\n' +
'[02:14.05]好运带来了喜和爱\n' +
'[02:18.24]好运来我们好运来\n' +
'[02:22.52]迎着好运兴旺发达通四海\n' +
'[02:26.74]好运来祝你好运来\n' +
'[02:31.10]好运带来了喜和爱\n' +
'[02:35.35]好运来我们好运来\n' +
'[02:39.63]迎着好运兴旺发达通四海\n' +
'[02:45.09]\n' +
'[02:59.12]好运来祝你好运来\n' +
'[03:03.26]好运带来了喜和爱\n' +
'[03:07.43]好运来我们好运来\n' +
'[03:11.68]迎着好运兴旺发达通四海\n' +
'[03:16.02]通四海好运来'}
];
</script>
<style scoped>
</style>
<script lang="ts">
export default {
name: "MusicPlayer"
}
</script>
<script setup lang="ts">
import {computed, onMounted, ref, toRefs, watch,withDefaults,defineProps} from "vue";
const audioRef = ref<HTMLAudioElement>();
const progressRef=ref('')
const progresscontainerRef=ref('')
interface MusicItem{
title:string;
img:string;
src:string;
lyric:string;
}
interface AudioPlayerProps {
musicList?: (MusicItem [] | undefined );
diskHW?:string;
progressColor?:string;
lyricColor?:string;
lyricSize?:string;
offsetY?:number;
darkTheme?:boolean;
}
const props= withDefaults(defineProps<AudioPlayerProps>(), {
musicList: () =>undefined,
diskHW: () =>'120px',
lyricSize: () =>'14px',
lyricColor: () =>'#1890ff',
offsetY: () =>25,
progressColor: () =>'#1890ff',
darkTheme: () =>false,
})
const {musicList, offsetY}=toRefs(props);
// 默认从第一首开始
let songIndex = ref(0);
let currentLyc = ref(0);
const lyricList = ref ([]);
const lycStyle = ref({});
//正在播放的音乐
const playSongSrc:any = computed(() => {
return musicList.value ? musicList.value[songIndex.value]:{}
});
const paused = ref(false);
//获取歌词
const getLyric=()=>{
let result = playSongSrc.value.lyric+' ';
if(result){
let lyricData =[];
result.split(/[\n]/).forEach((item,index) => {
let temp = item.split(/\[(.+?)\]/)
lyricData.push(
{
time: temp[1],
lyc: temp[2]
})
})
lyricList.value=lyricData.filter(v => v['lyc']);
}else{
audioRef.value ? audioRef.value.currentTime = 0 : null;
currentLyc.value=0;
lycStyle.value={
transform: `translateY(-0px)`,
}
lyricList.value=[]
lyricList.value.push(
{
time: '00:01:00',
lyc: playSongSrc.value.title
})
}
}
getLyric();
const timeUpdate=()=> {
if(playSongSrc.value.lyric){
const currentDate=getTime(audioRef.value.currentTime);
for (let i = 0; i < lyricList.value.length; i++) {
if (lyricList.value[i+1] && currentDate < lyricList.value[i + 1].time && currentDate > lyricList.value[i].time) {
currentLyc.value=i;
lycStyle.value={
transform: `translateY(-${offsetY.value * i+1}px)`,
}
}
}
}
}
// 秒转换-分:秒的格式
const getTime = time => {
if (time) {
const minutes = Math.floor(time / 60);
const remainingSeconds = time % 60;
const formattedMinutes = String(minutes).padStart(2, '0');
const formattedSeconds = remainingSeconds.toFixed(2).replace('.', ':').padStart(5, '0');
return `${formattedMinutes}:${formattedSeconds}`;
} else {
return "00:00";
}
};
//监听
watch(playSongSrc, function (value, oldvalue) {
try{
audioRef.value.src=value?.src;
getLyric();
audioRef.value?.play();
paused.value=true;
}catch (e) {
console.log(e)
}
},)
onMounted(()=>{
try{
audioRef.value.src=playSongSrc.value?.src;
// 播放中添加时间变化监听
audioRef.value.addEventListener("timeupdate", () => {
Timeupdate();
});
// 当前音乐播放完毕监听
audioRef.value.addEventListener("ended", () => {
playEnded();
})
audioRef.value.addEventListener('error', function() {
console.warn('音频文件加载失败');
return;
});
}catch (e) {
console.log(e)
}
})
const playSong=(isplay)=> {
paused.value=audioRef.value.paused;
paused.value? audioRef.value?.play():audioRef.value?.pause();
}
const switchSong=(isNext)=>{
paused.value=audioRef.value.paused;
let number=songIndex.value;
if (musicList.value) {
songIndex.value == musicList.value.length - 1 ? songIndex.value = 0 : isNext ? songIndex.value++ : number == 0 ? songIndex.value = musicList.value.length - 1 : songIndex.value--;
}
}
const Timeupdate=()=>{
updateProgress()
timeUpdate()
}
const playEnded=()=>{
switchSong(true)
}
const updateProgress=()=> {
const {
duration,
currentTime
} = audioRef.value;
const progressPercent = (currentTime / duration) * 100;
progressRef.value.style.width = `${progressPercent}%`
}
const setProgress=(clickX,width)=> {
const duration = audioRef.value.duration
audioRef.value.currentTime = (clickX / width) * duration
}
const setProgressBtn=(e)=> {
const width =progresscontainerRef.value.clientWidth;
const clickX = e.offsetX;
setProgress(clickX,width)
}
</script>
<template>
<div style="height: 100%;width: 100%; display: flex;
flex-direction: column;">
<audio ref='audioRef' ></audio>
<div class='player'>
<div class="player_disk" >
<div class="disk_img" :style="{backgroundImage:`url(${playSongSrc.img})`,opacity:darkTheme?0.8:1}">
</div>
<div class="control-style">
<div class="control_btn " @click="switchSong(false)">
<span class="play-btn">
<slot name="last"></slot>
</span>
</div>
<div class="control_btn" @click="playSong(!paused)">
<span class="play-btn" >
<div v-show="paused"><slot name="pause"></slot></div>
<div v-show="!paused"><slot name="play"></slot></div>
</span>
</div>
<div class="control_btn " @click="switchSong(true)">
<span class="play-btn">
<slot name="next"></slot>
</span>
</div>
</div>
</div>
<div className="player_control">
<div class="lyricBox" >
<div :style="{...lycStyle, color:darkTheme?'#bbb8b8':'#212020',fontSize:lyricSize}" class="lyricStyle">
<p v-for="(item,index) in lyricList" key={index} :style="{color:currentLyc==index?lyricColor:'',opacity:!darkTheme?1:0.6}">{{item.lyc}}</p>
</div>
<div className="progress-container" ref='progresscontainerRef' :style="{opacity:!darkTheme?1:0.6}" @click="(e)=>{ setProgressBtn(e)}">
<div ref='progressRef' class="progress" id="progress" :style="{'backgroundColor':progressColor}"></div>
</div>
</div>
</div>
</div>
</div>
</template>
<style >
.player{
display: flex;
height: 100%;
}
.player_disk{
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.control-style{
flex: 1;
display: flex;
text-align: center;
padding-top: 10px;
}
.control_btn{
flex: 1;
}
.player_control{
flex: 2.2;
margin-left: 10px;
}
.disk_img{
text-align: center;
width: 100%;
height: 100%;
background-size: 100% 100%;
border-radius: 5px;
}
.lyricStyle{
position: absolute;
left: 0;
right: 0;
transition: all 0.3s;
}
.lyricStyle p{
margin: 7px 0px;
}
.lyricBox{
height: 100%;
overflow: hidden;
position: relative;
text-align: center;
padding-bottom: 10px;
}
.control{
display: flex;
}
.progress {
height: 100%;
width: 0%;
transition: width 0.2s linear;
border-radius: 5px;
}
.progress-container{
width: 100%;
height: 3px;
position: absolute;
bottom: 0px;
cursor:pointer;
border-radius: 5px;
}
.lyricStyle p{
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
</style>
<template>
<div>
<br>
<!-- 这里的宽高必须设置,根据自身页面适当调整-->
<div style="height: 200px;width: 500px">
<MusicPlayer diskHW="80px" lyricSize="16px" progressColor="#36ad6a" lyricColor="#36ad6a" :musicList="musicList" :offsetY="32" >
<template #next>
下一首
</template>
<template #last>
上一首
</template>
<template #pause>
暂停
</template>
<template #play>
播放
</template>
</MusicPlayer>
</div>
</div>
</template>
<script lang="ts" setup>
import MusicPlayer from '../components/MusicPlayer.vue'
const musicList=[
{title:'三年二班',img:'https://www.vae.zhangweicheng.xyz/apple_Article_Head/vae/7e6361a6-58f7-4abb-997e-bdd88dd1dc06.jpg',src:'https://www.vae.zhangweicheng.xyz/music/%E5%91%A8%E6%9D%B0%E4%BC%A6%20-%20%E4%B8%89%E5%B9%B4%E4%BA%8C%E7%8F%AD.mp3',lyric:'[ti:三年二班]\n' +
'[ar:周杰伦]\n' +
'[al:叶惠美]\n' +
'\n' +
'[00:00.30]三年二班\n' +
'[00:01.30]作词:方文山 作曲:周杰伦\n' +
'[00:02.30]演唱:周杰伦\n' +
'[00:03.30]\n' +
'[00:06.45]训导处报告 训导处报告\n' +
'[00:09.27]三年二班 周杰伦\n' +
'[00:11.46]马上到训导处来\n' +
'[00:12.71]\n' +
'[00:40.05]眼睛你要擦亮 记住我的模样\n' +
'[00:42.71]表情不用太紧张\n' +
'[00:44.02]我是 三年二班\n' +
'[00:45.37]我专心打球的侧脸还满好看\n' +
'[00:47.52]黑板是吸收知识的地方\n' +
'[00:49.40]只是教室的阳光\n' +
'[00:50.66]那颜色我不太喜欢\n' +
'[00:51.97]没有操场的自然\n' +
'[00:53.32]为何比较漂亮的都是在隔壁班\n' +
'[00:56.04]还有考卷的答案\n' +
'[00:57.34]我刚好都不会算\n' +
'[00:58.64]没关系 再继续努力 没关系\n' +
'[01:01.28]为什么上课时举手很难\n' +
'[01:03.89]为什么拿线上宝物简单\n' +
'[01:06.69]为什么女生不喜欢太胖\n' +
'[01:09.22]为什么都别人手机在响\n' +
'[01:11.81]正手发长球的打法只是初级乒乓\n' +
'[01:14.71]反手短打再狠狠杀球是高级乒乓\n' +
'[01:17.27]回转技巧乒乓 前场速攻乒乓\n' +
'[01:19.95]对墙壁 在练习 乒乓 乒乓\n' +
'[01:22.64]这第一名到底要多强\n' +
'[01:25.93](不用问 一定有人向你挑战)\n' +
'[01:27.94]到底还要过多少关\n' +
'[01:31.14](不用怕 告诉他们谁是男子汉)\n' +
'[01:33.46]可不可以不要这个奖\n' +
'[01:36.51](不想问 我只想要流一点汗)\n' +
'[01:38.79]我当我自己的裁判\n' +
'[01:41.86](不想说 选择对手跟要打的仗)\n' +
'[01:44.11]这第一名到底要多强\n' +
'[01:46.95](不用问 一定有人向你挑战)\n' +
'[01:49.24]到底还要过多少关\n' +
'[01:52.57](不用怕 告诉他们谁是男子汉)\n' +
'[01:54.80]可不可以不要这个奖\n' +
'[01:57.84](不想问 我只想要流一点汗)\n' +
'[02:00.10]我当我自己的裁判\n' +
'[02:03.10](不想说 选择对手跟要打的仗)\n' +
'[02:05.28]\n' +
'[02:08.13]全体师生注意\n' +
'[02:09.46]今天我要表扬一位同学\n' +
'[02:11.97]他为校争光\n' +
'[02:13.44]我们要向他看齐\n' +
'[02:15.03]\n' +
'[02:28.06]我不想 就这样一直走\n' +
'[02:30.34]每天都遇上 充满敌意那种眼光\n' +
'[02:33.43]等机会 就是要打倒对方\n' +
'[02:36.10]这种结果我不要 这虚荣的骄傲\n' +
'[02:38.93]这目的很好笑\n' +
'[02:40.21]我其实都知道 你只是想炫耀\n' +
'[02:42.90]我永远做不到 你永远赢不了\n' +
'[02:45.50]我永远做不到\n' +
'[02:46.79]你永远赢不了 永远都赢不了\n' +
'[02:49.58]走下乡 寻找哪有花香\n' +
'[02:52.07](为什么 这么简单你做不到)\n' +
'[02:54.88]坐车厢 朝着南下方向\n' +
'[02:57.32](为什么 这种速度你追不到)\n' +
'[03:00.31]鸟飞翔 穿过这条小巷\n' +
'[03:02.80](为什么 这么简单你做不到)\n' +
'[03:05.60]仔细想 这种生活安详\n' +
'[03:08.07](为什么 这种速度你追不到)\n' +
'[03:10.93]不好笑 不好笑 不好笑\n' +
'[03:12.71]不好笑 不好笑 不好笑\n' +
'[03:14.57]\n' +
'[03:32.15]这第一名到底要多强\n' +
'[03:35.31](不用问 一定有人向你挑战)\n' +
'[03:37.48]到底还要过多少关\n' +
'[03:40.59](不用怕 告诉他们谁是男子汉)\n' +
'[03:42.92]可不可以不要这个奖\n' +
'[03:45.90](不想问 我只想要流一点汗)\n' +
'[03:48.14]我当我自己的裁判\n' +
'[03:51.25](不想说 选择对手跟要打的仗)\n' +
'[03:53.99]我不想 就这样一直走\n' +
'[03:56.04]每天都遇上 充满敌意那种眼光\n' +
'[03:58.79]等机会 就是要打倒对方\n' +
'[04:01.82]这种结果我不要 这虚荣的骄傲\n' +
'[04:04.17]我不想 就这样一直走\n' +
'[04:06.34]每天都遇上 充满敌意那种眼光\n' +
'[04:09.34]等机会 就是要打倒对方\n' +
'[04:12.11]这种结果我不要 这虚荣的骄傲\n' +
'[04:14.81]走下乡 寻找哪有花香\n' +
'[04:17.14]\n' +
'[04:20.24]坐车厢 朝着南下方向\n' +
'[04:22.45]\n' +
'[04:25.62]鸟飞翔 穿过这条小巷\n' +
'[04:27.96]\n' +
'[04:30.80]仔细想 这种生活安详\n' +
'[04:33.06]'}
];
</script>
<style scoped>
</style>
参数 | 说明 | 类型 | 默认值 |
---|---|---|---|
musicList | 歌词列表,一个数组对象,对象中需要包含:title(歌名),img(封面),src(音乐地址),lyric(歌词-怎么获取歌词?) | 数组 | 无 |
progressColor | 进度条背景颜色 | 颜色 | #1890ff |
offsetY | 歌词一行播放结束后,向上偏移多少(防止出现偏移过多出现的高亮问题) | 数字 | 25 |
lyricSize | 歌词字体大小 | 字符串 | 14px |
lyricColor | 歌词高亮背景色 | 字符串 | #1890ff |
diskHW | 磁盘图片的宽高 | 字符串 | 120px |
darkTheme | 黑夜主题 | 布尔 | false |
名称 | 说明 |
---|---|
next | 下一首 |
last | 上一首 |
pause | 暂停 |
play | 播放 |
注意:本组件暂未怎么测试,复制上方代码操作,简单的播放还是没问题的,后续还将继续优化。
该组件已经在旧版张苹果博客侧边栏使用,效果如下: