010_Vue音乐播放器(player.vue 播放器组件)
不满意之前的页面结构,所以我重构了一下,以home.vue作为父级组件,recommend组件、singer组件、rank组件和search组件作为子路由自建都归类到tab组件中。player.vue 组件,放置在home.vue组件下,这个组件一直存在,不过由于v-show="isShow"的关系,vuex中的playList没有数据,所以隐藏掉了,然后根据fullScree...
不满意之前的页面结构,所以我重构了一下,以home.vue作为父级组件,recommend组件、singer组件、rank组件和search组件
作为子路由自建都归类到tab组件中。



player.vue 组件,放置在home.vue组件下,这个组件一直存在,不过由于v-show="isShow"的关系,vuex中的playList没有数据,所以隐藏掉了,然后根据fullScreen数据,来判断是展示正常播放器还是迷你播放器。
<template>
<div class="player" v-show="isShow" :style="fullScreenStyle">
<!-- 正常播放器 -->
<transition name="normal" appear mode="in-out">
<div class="normalPlayer" v-show="fullScreen">
<!-- 整背景图 -->
<div class="bgImage">
<img width="100%" height="100%" :src="currentSongInfo.img" />
</div>
<!-- 顶部 -->
<div class="top">
<div class="back">
<i class="el-icon-arrow-down icon-back" @click="goBack"></i>
</div>
<h1 class="title">{{currentSongInfo.song_name}}</h1>
<h2 class="subtitle">{{currentSongInfo.author_name}}</h2>
</div>
<!-- 中部 唱片 -->
<div
class="middle"
@touchstart="middleTouchStart"
@touchmove.prevent="middleTouchMove"
@touchend="middleTouchEnd"
>
<!-- 旋转图片部分 -->
<div class="middle-l" ref="middleL">
<div class="cd-wrapper">
<div class="cd">
<img :style="imageRotateStop" class="image" v-lazy="currentSongInfo.img" />
</div>
</div>
</div>
<!-- 歌词滚动部分 -->
<Scroll class="middle-r" ref="lyricList" :data="currentLyrics && currentLyrics.lines">
<div class="lyric-wrapper">
<div v-if="currentLyrics">
<p
class="text"
:class="{'currentLine':currentLineNum == index}"
ref="lyricLine"
v-for="(line,index) in currentLyrics.lines"
:key="index"
>{{line.txt}}</p>
</div>
</div>
</Scroll>
</div>
<!-- 底部 -->
<div class="bottom">
<!-- 图像/歌词切换 -->
<div class="dot-wrapper">
<span class="dot" :class="{'active':currentShow=='img'}"></span>
<span class="dot" :class="{'active':currentShow=='lyric'}"></span>
</div>
<!-- 播放时间及进度条 -->
<div class="progress-wrapper">
<span class="item item-l">{{TimeFormat(currentTime)}}</span>
<div class="progress-bar-wrapper">
<progress-bar :percent="percent" @percentChange="onProgressBarChange"></progress-bar>
</div>
<span class="item item-r">{{TimeFormat(currentSong.duration)}}</span>
</div>
<!-- 按钮 -->
<ul class="operators">
<!-- 播放模式 -->
<li class="icon i-left">
<!-- <i class="icon-sequence el-icon-refresh-left"></i> -->
<i @click="modeChange" :class="iconMode"></i>
</li>
<!-- 上一首 -->
<li class="icon i-left">
<i @click="lastSong" class="icon-prev el-icon-d-arrow-left"></i>
</li>
<!-- 播放/暂停 -->
<li class="icon i-center">
<i ref="togglePlaying" @click="togglePlaying" class="el-icon-video-pause"></i>
</li>
<!-- 下一首 -->
<li class="icon i-right">
<i @click="nextSong" class="icon-next el-icon-d-arrow-right"></i>
</li>
<!-- 喜欢/取消喜欢 -->
<li class="icon i-right">
<i class="icon icon-not-facorite el-icon-star-off"></i>
</li>
</ul>
</div>
</div>
</transition>
<!-- 迷你播放器 -->
<div class="miniPlayer" v-show="!fullScreen" @click="setFullScreen">
<!-- 图片 -->
<div class="icon">
<img :style="imageRotateStop" v-lazy="currentSongInfo.img" />
</div>
<!-- 文本 -->
<div class="text">
<h2 class="name">{{currentSongInfo.song_name}}</h2>
<p class="desc">{{currentSongInfo.author_name}}</p>
</div>
<!-- 按钮 -->
<div class="mini-icon">
<i ref="miniTogglePlaying" @click="togglePlaying" class="el-icon-video-pause"></i>
</div>
<!-- 播放列表 -->
<div class="mini-icon">
<i class="el-icon-tickets"></i>
</div>
</div>
<audio
ref="audio"
:src="currentSongInfo.play_url"
@canplay="ready"
@timeupdate="updateTime"
@error="error"
@ended="end"
></audio>
</div>
</template>
<script>
import { mapMutations, mapGetters, mapActions } from "vuex";
import { getKugouMusicPlay } from "../../api/kugouMusicPlay";
import progressBar from "../base/progress-bar"; //进度条组件
import Lyric from "lyric-parser"; //歌词滚动播放插件
import Scroll from "../base/scroll";
export default {
created() {
this.touch = {}; //因为这个touch数据对象不需要get和set,所以放在created里面定义即可实现操作同一份数据
},
data() {
return {
emptyList: {
list: [],
index: -1
},
currentSongInfo: "", //根据currentSong的hash数据,发送请求,得到的歌曲详细信息
fullScreenStyle: "position:fixed;",
imageRotateStop: "animation-play-state: paused;", //停止旋转的style
songReady: false, //连续切换曲目时会出错,使用此变量标识歌曲资源是否请求回来
currentTime: 0, //播放当前时间
currentLyrics: null, //当前歌曲歌词信息
currentLineNum: 0, //当前歌曲歌词所在行
currentShow: "img" //当前是 图像还是歌词
};
},
methods: {
...mapActions(["selectPlay"]),
...mapMutations({
set_fullScreen: "set_fullScreen",
set_playing: "set_playing",
set_currentIndex: "set_currentIndex",
set_mode: "set_mode",
set_playList: "set_playList"
}),
noPlayer() {
this.selectPlay({
list: this.emptyList,
index: this.emptyList.index
});
//以下设置无效
// this.set_playing(false);
// this.set_currentIndex(-1);
},
goBack() {
//将vuex的fullScreen数据设为false
this.set_fullScreen(false);
// this.noPlayer();
},
setFullScreen(event) {
//非点击迷你播放器的播放按钮或歌曲列表按钮
if (event.target.nodeName !== "I") {
//将vuex的fullScreen数据设为true
this.set_fullScreen(true);
}
},
//请求歌曲播放信息
async _getKugouMusicPlay() {
let info = await getKugouMusicPlay(this.currentSong.hash);
if (info["SSA-CODE"]) {
if (info["SSA-CODE"] != "") {
//表示请求过于频繁,今天无法再播放歌曲
//使用element组件
this.$message({
type: "error",
message: "请求过于频繁,今天无法再播放歌曲了哦",
offset: -4, //自顶部栏向下偏移量
duration: 2000
});
this.noPlayer(); //设置空数据,变现为没有获取到歌单歌曲列表
setTimeout(() => {
this.$router.go(-1);
return false;
}, 2000);
} else {
this.currentSongInfo = info;
this.currentLyrics = new Lyric(info.lyrics, this.handleLyric); //当前歌词信息
if (this.playing) {
this.currentLyrics.play();
}
}
} else {
this.currentSongInfo = info;
this.currentLyrics = new Lyric(info.lyrics, this.handleLyric); //当前歌词信息
if (this.playing) {
this.currentLyrics.play();
}
}
},
//切换播放playing数据状态
togglePlaying() {
//歌曲资源未准备完成,return
if(!this.songReady){
return;
}
this.set_playing(!this.playing);
if(this.currentLyrics){
this.currentLyrics.togglePlay();
}
},
//上一首
lastSong() {
//歌曲资源尚未请求成功,不切换歌曲
if (!this.songReady) {
return;
}
//改变下标index,获取歌曲currentSong,
//重新设置 当前播放歌曲信息 currentSongInfo
let index = this.currentIndex - 1;
if (index <= 0) {
//歌曲列表结尾
index = this.currentIndex - 1; //重头开始
}
this.set_currentIndex(index);
if (!this.playing) {
//暂停时切换歌曲需要改变state的playing状态
this.togglePlaying();
}
this.songReady = false; //请求完当前歌曲,重新将songReady置为false
},
//下一首
nextSong() {
//歌曲资源尚未请求成功,不切换歌曲
if (!this.songReady) {
return;
}
let index = this.currentIndex + 1;
if (index == this.playList.length) {
//歌曲列表结尾
index = 0; //重头开始
}
this.set_currentIndex(index);
if (!this.playing) {
//暂停时切换歌曲需要改变state的playing状态
this.togglePlaying();
}
this.songReady = false; //请求完当前歌曲,重新将songReady置为false
},
//歌曲资源请求成功可以播放
ready() {
this.songReady = true;
},
//歌曲请求失败,也置为true,避免切换歌曲不执行
error() {
this.songReady = true;
},
updateTime(event) {
this.currentTime = event.target.currentTime; //audio当前播放的时间,可读写
},
//用于将时间戳转换成所需时间格式
TimeFormat(timeStamp) {
timeStamp = timeStamp | 0; // |0 向下取整
let minute = (timeStamp / 60) | 0; //取余得到分钟
let second = timeStamp % 60; //取余得秒
if (minute < 10) {
minute = "0" + minute;
}
if (second < 10) {
second = "0" + second;
}
return minute + ":" + second;
},
//根据 拖放 进度条 传回的percent百分比值,修改歌曲的播放位置,通过audio的currentTIme可读写属性
onProgressBarChange(percent) {
this.$refs.audio.currentTime = this.currentSong.duration * percent;
if (!this.playing) {
//非播放状态,拖拽后播放,改变播放状态
this.togglePlaying();
}
if(this.currentLyrics){
this.currentLyrics.seek(this.currentSong.duration * percent*1000);//拖动进度,改变歌词位置
}
},
//切换播放模式
modeChange() {
let mode = this.mode;
mode++;
mode > 2 ? (mode = 0) : mode;
this.set_mode(mode);
//切换播放列表
let list = null;
//顺序列表作为参数,使用洗牌函数得到随机播放列表
if ((mode = 2)) {
//随机播放
list = this.shuffle(this.sequenceList);
} else {
//顺序播放、单曲循环
list = this.playList;
}
this.resetCurrentIndex(list); //设置当前播放曲目索引,保证歌曲不会因为歌单的改变而切换
this.set_playList(list); //将新的播放列表提交到state中
},
// 避免切换播放模式时候重新设置了currentSong,导致当前播放歌曲被切换
resetCurrentIndex(list) {
let index = list.findIndex(item => {
return item.audio_id == this.currentSong.audio_id; //ES6函数,在新播放列表中根据歌曲audio_id找到当前曲目的新索引
});
this.set_currentIndex(index); //重新设置一遍index,这样保证查找到的歌曲是同一首,即currentSong不变
},
//洗牌函数的封装
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;
},
// 歌曲播放结束的end事件
end() {
//循环播放
if (this.mode == 1) {
this.loop();
} else {
//切换到下一首
this.nextSong();
}
},
//循环播放函数loop
loop() {
this.$refs.audio.currentTime = 0;
this.$refs.audio.play();
if(this.currentLyrics){
this.currentLyrics.seek(0);//循环播放时,最后将歌词偏移到初始位置
}
},
//lyric-parser 监听歌词改变的回调函数
handleLyric(newLyricLine) {
//每次歌词行改变,更新data的当前歌词行,在页面结构中,根据index等于当前行,显示高亮
this.currentLineNum = newLyricLine.lineNum;
//如果当前歌词行数超过5条,通过scroll滑动,实现歌词居中
if (newLyricLine.lineNum > 5) {
//大于5行,滚动到当前行数-5行的节点
let lineEl = this.$refs.lyricLine[newLyricLine.lineNum - 5];
this.$refs.lyricList.scrollToElement(lineEl, 1000);
} else {
//五行之内,滚动到顶部即可
this.$refs.lyricList.scrollToElement(0, 0, 1000);
}
},
//图像/歌词滑动切换的效果
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
}
},
computed: {
...mapGetters([
"playing", //播放状态
"fullScreen", //是否全屏
"playList", //播放列表
"currentSong", //当前歌曲
"currentIndex", //当前索引
"mode", //播放模式
"sequenceList" //顺序列表
]),
isShow() {
if (this.playList) {
if (this.playList.length) {
//播放列表有歌曲,展开播放器,发起请求,请求点击歌曲的详细信息
this._getKugouMusicPlay();
return true;
}
} else {
return false;
}
},
percent() {
return this.currentTime / this.currentSong.duration; //根据当前播放时间来计算进度条百分比
},
//播放状态样式
iconMode() {
//mode : 0 顺序播放 1 单曲循环 2 随机播放
return this.mode === 0
? "el-icon-refresh-left"
: this.mode === 1
? "el-icon-refresh"
: "el-icon-connection";
}
},
watch: {
fullScreen: function(newVal) {
newVal == true
? (this.fullScreenStyle = "top:0;")
: (this.fullScreenStyle = "bottom:0;height:0;"); //播放器全屏时的样式
},
currentSong(newSong, oldSong) {
//当playingList或者index改变导致调用此监听,若audio_id不变,即currentSong不变,则不执行播放操作
if (newSong.audio_id == oldSong.audio_id) {
return;
}
if(this.currentLyrics){
this.currentLyrics.stop();//歌曲改变,将之前的歌词播放定时器停止
}
//监听到currentSong数据变化,异步自动播放歌曲
this.$nextTick(() => {
this.set_playing(true); //改变vuex中的播放状态
this.$refs.audio.play(); //播放音乐
});
},
currentSongInfo(newSong, oldSong) {
if (newSong.audio_id == oldSong.audio_id) {
return;
}
//监听到currentSongInfo数据变化,异步自动播放歌曲
this.$nextTick(() => {
this.set_playing(true); //改变vuex中的播放状态
this.$refs.audio.play(); //播放音乐
});
},
playing(newPlaying) {
const audio = this.$refs.audio; //缓存audio对象
if (newPlaying) {
//播放
//播放时显示暂停图标 el-icon-video-pause
this.$refs.togglePlaying.classList = "el-icon-video-pause";
this.$refs.miniTogglePlaying.classList = "el-icon-video-pause";
this.imageRotateStop = ""; //图片旋转
this.$refs.audio.play();
} else {
//暂停
//暂停时显示播放图标 el-icon-video-play
this.$refs.togglePlaying.classList = "el-icon-video-play";
this.$refs.miniTogglePlaying.classList = "el-icon-video-play";
this.imageRotateStop = "animation-play-state: paused;"; //图片暂停旋转
this.$refs.audio.pause();
}
}
},
components: {
progressBar,
Scroll
}
};
</script>
<style lang="less" scoped>
其中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 返回当前媒体的URLcurrentTime 当前播放的时间,单位秒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 当前音视频源的URLstartOffsetTime 返回当前的时间偏移(Date对象)textTracks 返回可用的文本轨迹(TextTrackList对象)videoTracks 返回可用的视频轨迹(VideoTrackList对象)volume 音量值
歌曲播放时间及自定义进度条
audio标签带有timeUpdate 事件,会自动抛出当前播放时间的时间戳,在此使用updateTime函数来接收当前播放时间。




由于currentTime是时间戳,所以需要将其格式化 TimeFormat函数

然后歌曲的总时长为currentSong的duration属性,也是一个时间戳,调用TimeFormat函数即可
效果页

进度条组件 progress-bar.vue
// 播放器进度条组件
<template>
<!-- 最外层总的长条 -->
<div class="progress-bar" ref="progressBar" @click="progressClick">
<!-- 里层进度条 -->
<div class="bar-inner">
<div class="progress" ref="progress"></div>
<div
class="progress-btn-wrapper"
@touchstart="progressTouchStart"
@touchmove="progressTouchMove"
@touchend="progressTouchEnd"
>
<!-- 按钮-拖动 -->
<div class="progress-btn" ref="progressBtn"></div>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
//进度条百分比,由播放歌曲的时间得出百分比
percent: {
type: Number,
default: 0
}
},
created() {
//维护同一个touch对象,不同回调函数中共享数据
this.touch = {};
},
watch: {
percent(newPercent) {
if (newPercent >= 0 && this.touch.initiated == false) {
//非拖动时进度条随歌曲播放而进行
const barWidth = this.$refs.progressBar.clientWidth - 16; //减去按钮的宽度
const offsetWidth = newPercent * barWidth; //得到偏移的宽度
this._offset(offsetWidth);
}
}
},
methods: {
//拖拽进度条
progressTouchStart(e) {
this.touch.initiated = true; //表示已初始化
this.touch.startX = e.touches[0].pageX; //横向坐标
this.touch.left = this.$refs.progress.clientWidth; //记录按钮偏移位置
},
progressTouchMove(e) {
if (!this.touch.initiated) {
return; //未初始化
}
const deltaX = e.touches[0].pageX - this.touch.startX; //偏移量
const offsetWidth = Math.min(
this.$refs.progressBar.clientWidth - 16,
Math.max(0, this.touch.left + deltaX)
); // 0~this.touch.left+deltaX
this._offset(offsetWidth);
},
progressTouchEnd() {
this.touch.initiated = false;
this.triggerPercent();
},
//点击进度条
progressClick(e) {
this._offset(e.offsetX);
this.triggerPercent();
},
//设置偏移量
_offset(offsetWidth) {
this.$refs.progress.style.width = offsetWidth + "px"; //进度条进行偏移
this.$refs.progressBtn.style.left = offsetWidth + "px"; //按钮进行偏移
},
//联动到歌曲播放进度改变
triggerPercent() {
const barWidth = this.$refs.progressBar.clientWidth - 16; //减去按钮的宽度
const percent = this.$refs.progress.clientWidth / barWidth;
this.$emit("percentChange", percent); //拖动完成时抛出 百分比改变的时间,联动到歌曲播放进度改变
}
}
};
</script>
<style lang="less" scoped>
.progress-bar {
position: relative;
height: 30px;
top: 0.5rem;
.bar-inner {
position: absolute;
width: 100%;
top: 13px;
height: 4px;
background-color: rgba(0, 0, 0, 0.8);
.progress {
position: absolute;
height: 100%;
background-color: red;
}
.progress-btn-wrapper {
position: absolute;
// left: -8px;
left: 0;
top: -13px;
width: 30px;
height: 30px;
.progress-btn {
position: relative;
touch-action: 7px;
top: 0.4rem;
left: 7px;
box-sizing: border-box;
width: 16px;
height: 16px;
border: 3px solid red;
border-radius: 50%;
background-color: red;
}
}
}
}
</style>
根据传入的 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;
}
至此,可以通过点击播放模式来切换歌单,且保持当前歌曲的播放状态。
根据audio派发的ended事件,对歌曲播放完毕后进行处理


点击进度条按钮的bug
当点击到进度条按钮本身时,由于获取的节点对象错误,导致进度条进度移动的错误

歌词的显示
将歌词部分(由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歌曲数据请求有次数限制,当天次数限制时,弹出提示框后跳回歌单详情页。

效果页:

更多推荐





所有评论(0)