用户首先访问的是Home.vue组件,该组件在加载完成的时候调用created方法获取全部的歌单
(getSongsMenu)和歌曲(getAllSinger), 并将全部的歌单和歌手存放到了
songsMenuAndSinger中,这是个数组,里面存放的是两个对象,一个是歌单(每个歌单对象由多个songsMenuSong对象构成,每个songsMenuSong对象中包含一个song对象)对象另一个是歌曲对象。歌单和歌曲对象中又分别含有一个数组,用来分别存放歌单和歌曲。如下:songsMenuAndSinger: [
{name: '推荐歌单>', list: []},
{name: '热门歌手>', list: []}
]
此时,在Home.vue页面遍历songsMenuAndSinger数组。
<div class="section" v-for="(item, index) in songsMenuAndSinger" :key="index"> <div class="section-title"><b>{{item.name}}</b></div> <content-list :contentList=item.list></content-list> </div>
其中item就指的是songsMenuAndSinger数组中的两个对象{name: '推荐歌单>', list: [ ]}和{name: '热门歌手>', list: []},然后将list传递给了ContentList.vue组件,由该组件进一步遍历。
props: [
'contentList'],
也就是说Home.vue组件中item.list传递到了contentList中。
<li class="content-item" v-for="(item, index) in contentList" :key="index"> <div class="kuo" @click="goAblum(item, item.introduce)"> <img class="item-img" :src="(item.picture)" alt=""> <div class="mask" @click="goAblum(item, item.name)"> </div> </div> <p class="item-name"><b>{{item.name || item.title}}</b></p> </li>
这里的item代表的就是歌单和歌手对象了,第一次遍历的时候是歌单,第二次遍历的时候是歌手。因为歌单个歌手的是通过introduce字段区分的,因此如果当前是歌单的话会传递introduce参数,如果是歌手的话,会传递歌手的名字name参数。(下面以歌手为例,假设是第二次遍历,contentList中全是纯歌手对象)。
在点击歌手的头像后触发goAblum方法。
goAblum (item, type) { //将单个歌手或者是歌单放到缓存中,便于在页面跳转的时候取出。 this.$store.commit('setSingerOrMenuTemp', item) if (type) { //转向歌手页面 this.$router.push({path: `/singer-album/${item.id}`}) } else { //转向歌单页面 this.$router.push({path: `/song-list-album/${item.id}`}) } }
先将获取的该歌手对象保存到缓存singerOrMenuTemp中,当前是歌手触发的方法的话就保存的是一个歌手对象,反之,是一个歌单对象。然后携带个当前歌手(歌单)对象的参数id跳转到singerAlbum.vue页面中。
mounted () { this.singerId = this.$route.params.id // 从路由中获取歌手id this.singerOrMenu = this.singerOrMenuTemp //从缓存获取歌手的信息 this.getSongList() }
singerAlbum.vue页面一加载就获取路径参数id和缓存中存储的歌手(歌单)对象,其中获取id的目的是为了根据id查歌单或者是歌手的全部歌曲,在滴哦用this.getSongList()方法时用到了,如下。
getSongList () { getSongOfSingerId(this.singerId) .then(res => { //这是要在播放列表显示的歌曲 this.songs = res this.$store.commit("setListOfSongs",res) }) .catch(err => { console.log(err) }) }
然后就将获取的某个歌手或者是歌单的全部歌曲放到缓存中去了。此外将查询结果展示在当前页面上遍历获取的全部歌曲,如下.
<album-content :songList=this.songs> </album-content>
然后将获取的全部歌曲传递到了AlbumContent.vue页面,在该页面中遍历获取的歌曲,如下。
<li class="list-content" v-for="(item, index) in songList" :key="index"> <div class="song-item" :class="{'is-play': id === item.id}" @click="toplay(item.id, item.url, item.picture, index, item.name, item.lyrics)"> <span class="item-index"> <span v-if="id !== item.id">{{index + 1}}</span> <svg v-if="id === item.id" class="icon" aria-hidden="true"> <use xlink:href="#icon-yinliang"></use> </svg> </span> <span class="item-title">{{replaceFName(item.name)}}</span> <span class="item-name">{{replaceLName(item.name)}}</span> <span class="item-intro">{{item.introduction}}</span> <span class="item-intro">{{item.introduction}}</span> </div> </li>
如果是歌单的话稍微有些不同,因为每个歌单中的歌曲(也就是songsMenuSong中包含一个song对象)如下图。
<li class="list-content" v-for="(item, index) in songList" :key="index"> <div class="song-item" :class="{'is-play': id === item.id}" @click="toplay(item.song.id, item.song.url, item.song.picture, index, item.song.name, item.song.lyrics)"> <span class="item-index"> <span v-if="id !== item.id">{{index + 1}}</span> <svg v-if="id === item.id" class="icon" aria-hidden="true"> <use xlink:href="#icon-yinliang"></use> </svg> </span> <span class="item-title">{{item.song.name}}</span> <span class="item-name">{{item.song.name}}</span> <span class="item-intro">{{item.song.introduction}}</span> <span class="item-intro">时长</span> </div> </li>
此时,songList中存放的就是一个个的song对象了,:class="{'is-play': id === item.id}"表示如果缓存中的id和歌曲id一样的话激活该calss样式。
@click="toplay(item.id, item.url, item.picture, index, item.name, item.lyrics)调用的是minix中的播放方法,参数为当前歌曲的id,当前歌曲的播放地址,当前歌曲的图片地址,当前歌曲在当前歌曲列表中的索引位置,当前歌曲的名字和歌词。
当用户点击播放时,会触发toPlay方法,如下。
toplay: function (id, url, picture, index, name, lyrics) { this.$store.commit('setId', id) this.$store.commit('setListIndex', index) this.$store.commit('setUrl', url) this.$store.commit('setpicUrl', picture) this.$store.commit('setTitle', this.replaceFName(name)) this.$store.commit('setArtist', this.replaceLName(name)) this.$store.commit('setLyric', this.parseLyric(lyrics)) if (this.loginIn) { this.$store.commit('setIsActive', false) getCollectionOfUser(this.userId) .then(res => { for (let item of res) { if (item.songId === id) { this.$store.commit('setIsActive', true) break } } }) .catch(err => { console.log(err) }) } },
调用播放方法其实就是将这些数据存储到缓存中,this.loginIn是在用户登录后执行的,SongAudio.vue组件是根据url播放歌曲的,一旦缓存中的url改变,SongAudio.vue组件中播放的歌曲也会随着改变。如下。
<audio :src='url' controls="controls" ref="player" preload="true" @canplay="startPlay" @timeupdate="timeupdate" @ended="ended"> </audio>
至此,点击播放列表中的歌曲可以切换播放。
接下来就是画一个好看的播放器了。就是playBar.vue组件。
<div ref="progress" class="progress" @mousemove="mousemove"> <!--进度条--> <div ref="bg" class="bg" @click="updatemove"> <div ref="curProgress" class="cur-progress" :style="{width: curLength+'%'}"></div> </div> <!--进度条 end --> <!--拖动的点点--> <div ref="idot" class="idot" :style="{left: curLength+'%'}" @mousedown="mousedown" @mouseup="mouseup"></div> <!--拖动的点点 end --> </div>
progress指的是当前整个的进度条,curProgress指的是当前歌曲播放经过的区域,idot和curProgress的值是相同的都表示的是经过的区域。
点击播放按钮会触发事件togglePlay,判断取出缓存中的值isPlay,缓存中的isPlay默认是false,也就是不播放状态,执行else语句更新缓存中的isPlay为true,还要接着更新播放按钮的状态。如下。
// 控制音乐播放 / 暂停 togglePlay () { if (this.isPlay) { this.$store.commit('setIsPlay', false) } else { this.$store.commit('setIsPlay', true) } },
//监控isPlay改变播放器的状态 isPlay(){ if(this.isPlay){ this.$store.commit('setPlayButtonUrl', '#icon-zanting') }else{ this.$store.commit('setPlayButtonUrl', '#icon-bofang') } },
进度条的拖拽原理:
首先需要在播放器页面也就是playbar.vue先计算好进度条的总长度如下:
this.progressLength = this.$refs.progress.getBoundingClientRect().width //获取进度条的长度
当前播放歌曲的时间占歌曲总时长的百分比=当前歌曲的进度条占总进度条的百分比。如下,是当前播放时间占歌曲总时间的百分比。
this.curLength = (this.curTime / this.duration) * 100
如下,是歌曲的进度条占总进度条的百分比。点击事件 @click="updatemove"是拖动点点播放歌曲的时候触发该事件。
<!--进度条--> <div ref="bg" class="bg" @click="updatemove"> <div ref="curProgress" class="cur-progress" :style="{width: curLength+'%'}"></div> </div>
如下,是点点在进度条上的偏移量。
<!--拖动的点点--> <div ref="idot" class="idot" :style="{left: curLength+'%'}" @mousedown="mousedown" @mouseup="mouseup"></div> <!--拖动的点点 end -->
鼠标点击点点的时候会使变量tag变为true ,并且获得开始移动的点点的X坐标,如下。
mousedown (e) { this.mouseStartX = e.clientX this.tag = true },
当tag变为true的时候就说明当前是拖动点点的状态,进度条的位置也要随着改变,进度条从点点被点击开始算,为
let movementX = e.clientX - this.mouseStartX
其中 e.clientX表示的是当前的点点的位置,mouseStartX表示的是刚开始点点移动的位置,两者的差就是点点的位移。还要加上点点还没移动之前的进度条的长度,
this.progressLength = this.$refs.progress.getBoundingClientRect().width
两者相加就是当前的进度条的长度和点点的位置。
let newPercent = ((curLength + movementX) / this.progressLength) * 100
this.curLength = newPercent
this.mouseStartX = e.clientX //重新为当前点点的位置赋值。
mousedown (e) { this.mouseStartX = e.clientX this.tag = true },
// 拖拽中 mousemove (e) { if (!this.duration) { return false } if (this.tag) { let movementX = e.clientX - this.mouseStartX let curLength = this.$refs.curProgress.getBoundingClientRect().width // 计算出百分比 this.progressLength = this.$refs.progress.getBoundingClientRect().width let newPercent = ((curLength + movementX) / this.progressLength) * 100 if (newPercent > 100) { newPercent = 100 } this.curLength = newPercent this.mouseStartX = e.clientX //重新为当前点点的位置赋值。 // 根据百分比推出对应的播放时间 this.changeTime(newPercent) } },
然后还要改变播放时间, 就是根据当前的newPercent (也就是当前的进度占总进度条的长度的百分比)来更新时间,如下。
// 更改歌曲进度 changeTime (percent) { let newCurTime = this.duration * (percent * 0.01) this.$store.commit('setChangeTime', newCurTime) },
另外时间的格式需要转化成时分秒的形式,如下。
// 播放时间的开始和结束 curTime () { this.nowTime = this.formatSeconds(this.curTime) this.songTime = this.formatSeconds(this.duration) // 移动进度条 this.curLength = (this.curTime / this.duration) * 100 },
// 解析播放时间 formatSeconds (value) { let theTime = parseInt(value) let theTime1 = 0 let theTime2 = 0 if (theTime > 60) { theTime1 = parseInt(theTime / 60) // 分 theTime = parseInt(theTime % 60) // 秒 // 是否超过一个小时 if (theTime1 > 60) { theTime2 = parseInt(theTime1 / 60) // 小时 theTime1 = 60 // 分 } } // 多少秒 if (parseInt(theTime) < 10) { var result = '0:0' + parseInt(theTime) } else { result = '0:' + parseInt(theTime) } // 多少分钟时 if (theTime1 > 0) { if (parseInt(theTime) < 10) { result = '0' + parseInt(theTime) } else { result = parseInt(theTime) } result = parseInt(theTime1) + ':' + result } // 多少小时时 if (theTime2 > 0) { if (parseInt(theTime) < 10) { result = '0' + parseInt(theTime) } else { result = parseInt(theTime) } result = parseInt(theTime2) + ':' + parseInt(theTime1) + ':' + result } return result },
音量控件就是个图标加滑块,就是在mounted中设置监听图标点击事件,监听到了之后弹出滑块,在watch中监听滑块的值,并更新到缓存中,如下。
document.querySelector('.icon-volume').addEventListener('click', function (e) { document.querySelector('.volume').classList.add('show-volume') e.stopPropagation() }, false) document.querySelector('.volume').addEventListener('click', function (e) { e.stopPropagation() }, false) document.addEventListener('click', function () { document.querySelector('.volume').classList.remove('show-volume') },
volume () { this.$store.commit('setVolume', this.volume / 100) },
歌曲列表的组件是个公共组件,因此写到了compoents里面。 歌曲列表就是当前的歌曲列表只不过就是能在其他页面显示罢了(这里出现bug),,如下:
<template> <transition name="slide-fade"> <div class="the-aside" v-if="this.showAside"> <h2 class="title">播放列表</h2> <ul class="menus"> <li v-for="(item, index) in listOfSongs" :class="{'is-play': id=== item.id}" :key="index" @click="toplay(item.id,item.url,item.picture,index,item.name,item.lyrics)"> {{replaceFName(item.name)}} </li> </ul> </div> </transition> </template>
上一曲和下一曲就是在获取当前歌曲在歌单中的位置的基础上索引的加减 ,以上一首为例,如下。
prev () { if (this.listIndex !== -1 && this.listOfSongs.length > 1) { //当前歌单列表的数量是大于1的才会切换 if (this.listIndex > 0) { //不是第一首音乐 this.$store.commit('setListIndex', this.listIndex - 1) //将上一首歌曲在播放列表中的位置存放到缓存中。 this.toPlay(this.listOfSongs[this.listIndex].url) //播放当前的歌曲 } else { this.$store.commit('setListIndex', this.listOfSongs.length - 1) this.toPlay(this.listOfSongs[this.listIndex].url)//切换到倒数第一首音乐,放到缓存中。 } } },
当前歌曲播放完成后自动播放下一首歌曲,触发事件是在SongAudio.vue 中的end方法,该方法中改变autoNext的值,在playBar组件中监控这个值的变化,只要改变就调用播放下一首的方法,如下。
// 音乐播放结束时触发 ended () { this.$store.commit('setIsPlay', false) this.$store.commit('setCurTime', 0) this.$store.commit('setAutoNext', !this.autoNext) }
// 自动播放下一首 autoNext () { this.next() }
// 下一首 next () { if (this.listIndex !== -1 && this.listOfSongs.length > 1) { if (this.listIndex < this.listOfSongs.length - 1) { this.$store.commit('setListIndex', this.listIndex + 1) this.toPlay(this.listOfSongs[this.listIndex].url) } else { //否则就是最后一首(也就是第一首) this.$store.commit('setListIndex', 0) this.toPlay(this.listOfSongs[0].url) } }
至此整个播放器完毕。
对于歌词的解析,因为后台存入的歌词是带有时间 的如下,
[01:22.290] 海上清辉与圆月 盛进杯光将所有歌词看作是一个数组,每行歌词是数组的一个数据。然后用正则表达式将数组中每个元素的时间和歌词分离。也就是如下格式:
【【时间,歌词】,【时间,歌词】】