{{currentSongInfo.song_name}}
{{currentSongInfo.author_name}}
不满意之前的页面结构,所以我重构了一下,以home.vue作为父级组件,recommend组件、singer组件、rank组件和search组件
作为子路由自建都归类到tab组件中。
player.vue 组件,放置在home.vue组件下,这个组件一直存在,不过由于v-show="isShow"的关系,vuex中的playList没有数据,所以隐藏掉了,然后根据fullScreen数据,来判断是展示正常播放器还是迷你播放器。
{{currentSongInfo.song_name}}
{{currentSongInfo.author_name}}
{{line.txt}}
{{TimeFormat(currentTime)}}
{{TimeFormat(currentSong.duration)}}
-
-
-
-
-
{{currentSongInfo.song_name}}
{{currentSongInfo.author_name}}
其中currentSongInfo——当前歌曲详细信息(包括play_url数据等),使用devServer发起后台请求(因为需要伪造cookie)
其中传入的hash值作为get请求的参数
在devServer的before函数内劫持对应请求,其中Cookie值为可在使用中自己手动抓取然后写死在请求中即可
Cookie:"kg_mid=bbbd01eb4d89517f78a82335ab0aec58; kg_dfid=2B054t0bTXp10XkJEz1QDSOQ; kg_dfid_collect=d41d8cd98f00b204e9800998ecf8427e; Hm_lvt_aedee6983d4cfc62f509129360d6bb3d=1572510353,1572868917,1572868957,1572869876"
其中用于标识歌曲的currentSong由vuex数据中的playList和index计算得出,方便切换上一首下一首歌曲(在点击歌单歌曲的时候已将playList和index数据提交到state)
用css3动画 @keyframes里设置transform:rotate(); 控制动画暂停和运动可以用属性:animation-play-state:paused(暂停)|running(运动);
且只能写在同一个class或style内,所以将animation-play-state:paused 封入一个data属性中,以style对象的形式动态引用
当audio抛出的canplay事件时表示歌曲请求成功,使用ready函数接收,改变能否播放的标识,切换歌曲时,以此标识来表示能否播放。
audio的属性
audioTracks 返回可用的音轨列表(MultipleTrackList对象)
autoplay 媒体加载后自动播放
buffered 返回缓冲部件的时间范围(TimeRanges对象)
controller 返回当前的媒体控制器(MediaController对象)
controls 显示播控控件
crossOrigin CORS设置
currentSrc 返回当前媒体的URL
currentTime 当前播放的时间,单位秒
defaultMuted 缺省是否静音
defaultPlaybackRate 播控的缺省倍速
duration 返回媒体的播放总时长,单位秒
ended 返回当前播放是否结束标志
error 返回当前播放的错误状态
initialTime 返回初始播放的位置
loop 是否循环播放
mediaGroup 当前音视频所属媒体组 (用来链接多个音视频标签)
muted 是否静音
networkState 返回当前网络状态
paused 是否暂停
playbackRate 播放的倍速
played 当前播放部件已经播放的时间范围(TimeRanges对象)
preload 页面加载时是否同时加载音视频
readyState 返回当前的准备状态 {
0: HAVE_NOTHING 没有准备就绪的状态
1: HAVE_METADATA 关于音频就绪的元数据
2: HAVE_CURRENT_DATA 当前可用,但下一帧不确定
3: HAVE_FUTURE_DATA 当前和下一帧可用
4: HAVE_ENOUGH_DATA 有足够的数据支持播放
}
seekable 返回当前可跳转部件的时间范围(TimeRanges对象)
seeking 返回用户是否做了跳转操作
src 当前音视频源的URL
startOffsetTime 返回当前的时间偏移(Date对象)
textTracks 返回可用的文本轨迹(TextTrackList对象)
videoTracks 返回可用的视频轨迹(VideoTrackList对象)
volume 音量值
audio标签带有timeUpdate 事件,会自动抛出当前播放时间的时间戳,在此使用updateTime函数来接收当前播放时间。
由于currentTime是时间戳,所以需要将其格式化 TimeFormat函数
然后歌曲的总时长为currentSong的duration属性,也是一个时间戳,调用TimeFormat函数即可
效果页
进度条组件 progress-bar.vue
// 播放器进度条组件
根据传入的 percent(百分比,由currentTime和duration计算而来)播放时间百分比
效果页
接下来通过一系列touch事件实现拖拽按钮调整播放位置的操作
首先 touchstart 事件 ,将 拖拽 标识变量initiated变量置为true,根据event对象的touches[0].pageX得到touchstart的起始位置 startX,存入this.touch对象,然后由已播放进度条的宽度得到按钮的偏移量 left。
在touchmove事件中,首先判断是否 拖拽初始化,未初始化则return,然后通过移动后的touches[0].pageX起始坐标减去记录的起始坐标startX得出移动偏移量deltaX,this.touch.left + deltaX 得出 拖动后的位置,然后跟 this.$refs.progressBar.clientWidth - 16 (进度条最大值)比较,小于它则取得出的拖动width,大于则取this.$refs.progressBar.clientWidth - 16(进度条末尾),然后将拖动的距离作为参数调用_offset函数,在函数中对 进度条 和 按钮的位置进行设置,实现偏移效果。
上面只是实现拖动,歌曲实际播放位置并没有改变,在touchend事件中,修改初始化值为false
调用triggerPercent函数,将进度条偏移的百分比作为参数抛出percent函数给父组件处理。根据进度百分比,设置audio的currentTime的值来进行歌曲播放位置的改变。
效果页
mapGetters引入mode和sequenceList数据
给播放模式绑定样式和事件
洗牌函数
/*洗牌函数的封装*/
getRandom(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
},
shuffle(arr) {
//不修改原数组
let _arr = arr.slice();
for (let i = 0; i < _arr.length; i++) {
let j = this.getRandom(0, i);
let t = _arr[i];
_arr[i] = _arr[j];
_arr[j] = t;
}
return _arr;
}
至此,可以通过点击播放模式来切换歌单,且保持当前歌曲的播放状态。
当点击到进度条按钮本身时,由于获取的节点对象错误,导致进度条进度移动的错误
将歌词部分(由currentSongInfo中取得)插入到Scroll组件中,以进行滚动,并根据currentShow来表示当前应当显示图片还是歌词部分。
其中,歌词部分使用Lyric函数进行处理(由cnpm install lyric-parser --save安装)
此时歌词即能随着歌曲滚动。
//图像/歌词滑动切换的效果
middleTouchStart(e) {
this.touch.initiated = true; //初始化
const touch = e.touches[0];
this.touch.startX = touch.pageX;
this.touch.startY = touch.pageY;
},
middleTouchMove(e) {
if (!this.touch.initiated) {
return;
}
const touch = e.touches[0];
const deltaX = touch.pageX - this.touch.startX;
const deltaY = touch.pageY - this.touch.startY;
if (Math.abs(deltaY) > Math.abs(deltaX)) {
//纵轴上的偏移大于横轴上的位移,认为是纵向滚动,不切换
return;
}
const left = this.currentShow === "img" ? 0 : -window.innerWidth; //为图像则不偏移,为歌词则向左移。
const offsetWidth = Math.min(
0,
Math.max(-window.innerWidth, left + deltaX)
);
this.touch.percent = Math.abs(offsetWidth / window.innerWidth); //得到滑动的比例
this.$refs.lyricList.$el.style.transform = `translate3d(${offsetWidth}px,0,0)`; //vue组件无法直接访问节点,使用$el
this.$refs.lyricList.$el.style.transform = `transformDuration:300ms`;
this.$refs.middleL.style.opacity = 1 - this.touch.percent;
this.$refs.middleL.style.transform = `transformDuration:300ms`;
},
middleTouchEnd() {
let offsetWidth;
let opacity; //从右向左滑
if (this.currentShow == "img") {
//滑动距离大于10%
if (this.touch.percent > 0.1) {
offsetWidth = -window.innerWidth;
opacity= 0;
this.$refs.middleL.style.opacity = opacity;
this.$refs.middleL.style.transform = `transformDuration:300ms`;
this.currentShow = "lyric"; //同时改变当前显示状态
} else {
offsetWidth = 0;
opacity= 1;
this.$refs.middleL.style.opacity = opacity;
this.$refs.middleL.style.transform = `transformDuration:300ms`;
}
}
//从左向右滑
else {
if (this.percent < 0.9) {
offsetWidth = 0;
opacity= 1;
this.$refs.middleL.style.opacity = opacity;
this.$refs.middleL.style.transform = `transformDuration:300ms`;
this.currentShow = "img";
} else {
offsetWidth = -window.innerWidth;
opacity= 0;
this.$refs.middleL.style.opacity = opacity;
this.$refs.middleL.style.transform = `transformDuration:300ms`;
}
}
this.$refs.lyricList.$el.style.transform = `translate3d(${offsetWidth}px,0,0)`; //vue组件无法直接访问节点,使用$el
this.touch.initiated = false
}
样式
.middle {
width: 202vw;
.middle-l {
width: 100vw;
vertical-align: top;
display: inline-block;
transition: all 1s;
.cd-wrapper {
.cd {
.image {
width: 17rem;
border: 2px solid gray;
border-radius: 50%;
animation: imgRotate 20s linear infinite;
}
}
}
}
.middle-r {
width: 100vw;
display: inline-block;
transition: all 1s;
.lyric-wrapper {
.text {
line-height: 2;
color: rgba(255, 255, 255, 0.5);
font-size: 1rem;
}
// 当前行歌词高亮
.currentLine {
font-size: 1.2rem;
color: rgba(255, 255, 255, 1);
}
}
}
.wrapper {
height: 63vh !important;
}
}
此时图像和歌词能够左右切换。
效果图
酷狗API歌曲数据请求有次数限制,当天次数限制时,弹出提示框后跳回歌单详情页。
效果页: