基于Vue2實(shí)現(xiàn)歌曲播放和歌詞滾動(dòng)效果
需求:需要實(shí)現(xiàn)歌詞滾動(dòng)效果。
思路:通過(guò)js+css的transform屬性完成。
難點(diǎn):主要就是需要知道正在播放的歌詞是那句,然后對(duì)正在播放的歌詞進(jìn)行變色和放大,最難的就是讓高亮歌詞隨著歌曲播放滾動(dòng)。
1.先看效果圖


2.處理歌詞格式(項(xiàng)目中有后端兄弟實(shí)現(xiàn)轉(zhuǎn)換的可以省略)
// 處理歌詞格式
parseLrc(musicLrc) {
const lines = musicLrc?.split(`\n`);
let lrcList = lines.map((line) => {
let [time, words] = line?.split("]") ?? [null, null];
return {
time: this.parseTime(time.substring(1)) || null,
words: words || null,
};
});
let lrcListRes = lrcList.filter((v) => {
return v.name !== null && v.words !== null;
});
// this.computingTime();
return lrcListRes;
},
parseTime(t) {
const part = t?.split(":");
return Number(part[0] * 60) + Number(part[1]);
},歌詞格式一般都是數(shù)組對(duì)象,對(duì)象的key各位可以自己根據(jù)需要命名,主要是思路。思路最重要!思路最重要!思路最重要!
我這里的數(shù)據(jù)格式如下圖


3.利用audio的timeupdate的事件來(lái)進(jìn)行計(jì)算歌詞是否高亮以及偏移量
// 添加audio事件
autoDown() {
const that = this;
var audio = document.getElementById("myAudio");
audio.addEventListener("ended", function () {
that.switchingBtn("down");
});
audio.addEventListener("timeupdate", function () {
that.computingTime();
});
},
// 計(jì)算播放時(shí)間對(duì)應(yīng)的下標(biāo)
computingTime() {
let arr = this.selectedFiles[this.playIndex].lrcList || [];
let currentTime = document.getElementById("myAudio")?.currentTime || 0;
let index = arr.findIndex((e) => currentTime < e.time) - 1;
this.currentIndex = index >= 0 ? index : arr.length - 1;
},
// 計(jì)算偏移量(保證高亮的歌詞在中間)
computingOffset(index) {
// 外部大盒子高度
let musicLrcBoxHeight = this.$refs.musicLrc?.clientHeight;
// 歌詞總高度
let musicLrcHeight = this.$refs.musicLrc_bady?.clientHeight;
// 每個(gè)li高度
let musicLrcLiHeight = this.$refs.musicLrc_item?.clientHeight || 22;
// 歌詞偏移高度
let offsetHenght =
index * musicLrcLiHeight + musicLrcLiHeight / 2 - musicLrcBoxHeight / 2;
// 最大偏移高度
let offsetMax = musicLrcHeight - musicLrcBoxHeight + 10;
if (offsetHenght < 0) {
offsetHenght = 0;
}
// if (offsetHenght > offsetMax) {
// offsetHenght = offsetMax;
// }
this.$refs.musicLrc_bady.style.transform = `translateY(-${offsetHenght}px)`;
},注意:這里的computingTime和computingOffset兩個(gè)事件是核心代碼!!!
4.整個(gè)demo源碼
<template>
<div class="h100 dis_sb bs">
<div class="music bg-fff bs">
<div class="p10 bs mb10" style="height: 40px">
<el-button type="text" size="small" @click="triggerFileInput">
選擇歌曲
</el-button>
<span class="f12 ml10">請(qǐng)先選擇本地音樂(lè)?。。?lt;/span>
<input
ref="audioInput"
style="display: none; height: 10px"
type="file"
@change="handleFileSelect"
multiple
accept="audio/*"
/>
</div>
<div class="bs p10" v-if="selectedFiles?.length > 0" style="height: 60px">
<audio
id="myAudio"
class="audio"
controls
:src="fileUrl || selectedFiles[0]?.url"
autoplay
/>
<div class="tac bs" style="line-height: 20px">
<el-button type="text" size="small" @click="switchingBtn('up')">
上一曲
</el-button>
<el-button type="text" size="small" @click="togglePlay">
{{ playing ? "暫停" : "播放" }}
</el-button>
<el-button type="text" size="small" @click="switchingBtn('down')">
下一曲
</el-button>
</div>
</div>
<div v-if="selectedFiles?.length > 0" class="p10 music_body">
<ul class="main bs mt20">
<li
:class="[index == playIndex ? 'main_item_action li' : 'li']"
v-for="(file, index) in selectedFiles"
:key="index"
>
<p @click="choose(file, index)">{{ file.name }}</p>
</li>
</ul>
</div>
</div>
<div class="bs h100 p10 musicLrc" ref="musicLrc">
<ul class="musicLrc_bady tac f14" ref="musicLrc_bady">
<li
v-for="(v, i) in selectedFiles[playIndex]?.lrcList"
:key="i"
ref="musicLrc_item"
>
<p :class="[i == currentIndex ? 'musicLrc_action ' : '']">
{{ v.words }}
</p>
</li>
<div
v-if="selectedFiles[playIndex]?.lrcList?.length == 0"
class="tac bs"
style="margin: auto; padding-top: 30px; color: #ccc"
>
暫無(wú)歌詞
</div>
</ul>
</div>
</div>
</template>
<script>
import musicList from "./musicList.js";
export default {
data() {
return {
selectedFiles: [],
lrcList: [],
fileUrl: null,
playing: false,
isPlay: false,
playIndex: 0,
currentTime: 0,
currentIndex: null,
};
},
created() {
musicList.forEach((e) => {
if (e.lrc) {
e.lrcList = this.parseLrc(e.lrc);
} else {
e.lrcList = [];
}
});
this.selectedFiles = JSON.parse(JSON.stringify(musicList));
},
watch: {
selectedFiles: {
handler(newVal, oldVal) {
if (newVal?.length > 4) {
this.$nextTick(() => {
this.fileUrl = newVal[0]?.url;
this.playing = true;
this.playIndex = 0;
});
}
},
deep: true,
},
$route: {
handler(newVal, oldVal) {
if (newVal?.path !== "/music") {
this.$nextTick(() => {
this.playing = false;
});
}
},
deep: true,
},
currentIndex: {
handler(newVal, oldVal) {
if (newVal > 0) {
setTimeout(() => {
this.computingOffset(newVal);
}, 150);
} else {
this.computingOffset(0);
}
},
deep: true,
},
},
mounted() {
this.$nextTick(() => {
this.autoDown();
});
},
methods: {
// 添加本地音樂(lè)
handleFileSelect(event) {
const files = event.target.files;
this.selectedFiles = JSON.parse(JSON.stringify(musicList));
this.fileUrl = null;
for (let i = 0; i < files?.length; i++) {
const file = files[i];
const reader = new FileReader();
reader.onload = (e) => {
const fileObj = {
name: file.name,
url: e.target.result,
// lrc: null,
lrcList: [],
};
this.selectedFiles.push(fileObj);
};
reader.readAsDataURL(file);
}
},
triggerFileInput() {
this.$refs.audioInput.click();
},
// 選擇播放歌曲
choose(v, i) {
const that = this;
that.playing = true;
that.fileUrl = that.selectedFiles[i]?.url;
that.playIndex = i;
},
// 判斷播放狀態(tài)
togglePlay() {
const that = this;
var audio = document.getElementById("myAudio");
if (audio.paused) {
audio.play();
that.playing = true;
} else {
audio.pause();
that.playing = false;
}
},
// 添加audio事件
autoDown() {
const that = this;
var audio = document.getElementById("myAudio");
audio.addEventListener("ended", function () {
that.switchingBtn("down");
});
audio.addEventListener("timeupdate", function () {
that.computingTime();
});
},
// 播放歌曲
playAudio() {
var audio = document.getElementById("myAudio");
audio.play();
this.currentIndex = 0;
},
// 切換歌曲
switchingBtn(v) {
const that = this;
that.playing = true;
const length = this.selectedFiles?.length || 0;
if (v === "down") {
this.playIndex = (this.playIndex + 1) % length;
} else {
this.playIndex = (this.playIndex - 1 + length) % length;
}
that.fileUrl = that.selectedFiles[that.playIndex]?.url;
setTimeout(() => {
that.playAudio();
}, 150);
},
// 處理歌詞格式
parseLrc(musicLrc) {
const lines = musicLrc?.split(`\n`);
let lrcList = lines.map((line) => {
let [time, words] = line?.split("]") ?? [null, null];
return {
time: this.parseTime(time.substring(1)) || null,
words: words || null,
};
});
let lrcListRes = lrcList.filter((v) => {
return v.name !== null && v.words !== null;
});
// this.computingTime();
return lrcListRes;
},
parseTime(t) {
const part = t?.split(":");
return Number(part[0] * 60) + Number(part[1]);
},
// 計(jì)算播放時(shí)間對(duì)應(yīng)的下標(biāo)
computingTime() {
let arr = this.selectedFiles[this.playIndex].lrcList || [];
let currentTime = document.getElementById("myAudio")?.currentTime || 0;
let index = arr.findIndex((e) => currentTime < e.time) - 1;
this.currentIndex = index >= 0 ? index : arr.length - 1;
},
// 計(jì)算偏移量(保證高亮的歌詞在中間)
computingOffset(index) {
// 外部大盒子高度
let musicLrcBoxHeight = this.$refs.musicLrc?.clientHeight;
// 歌詞總高度
let musicLrcHeight = this.$refs.musicLrc_bady?.clientHeight;
// 每個(gè)li高度
let musicLrcLiHeight = this.$refs.musicLrc_item?.clientHeight || 22;
// 歌詞偏移高度
let offsetHenght =
index * musicLrcLiHeight + musicLrcLiHeight / 2 - musicLrcBoxHeight / 2;
// 最大偏移高度
let offsetMax = musicLrcHeight - musicLrcBoxHeight + 10;
if (offsetHenght < 0) {
offsetHenght = 0;
}
// if (offsetHenght > offsetMax) {
// offsetHenght = offsetMax;
// }
this.$refs.musicLrc_bady.style.transform = `translateY(-${offsetHenght}px)`;
},
},
};
</script>
<style scoped>
.music {
width: calc(50% - 5px);
height: 100%;
border-radius: 10px;
}
#myAudio {
width: 100%;
height: 30px;
}
.music_body {
height: calc(100% - 126px);
overflow-x: hidden;
overflow-y: auto;
}
.main {
.li {
border: 1px solid #eee;
border-radius: 5px;
padding: 0 10px;
margin-bottom: 10px;
box-sizing: border-box;
line-height: 30px;
font-size: 12px;
cursor: grab;
color: #666;
}
.main_item_action {
border: 1px solid #409eff;
color: #409eff;
}
}
.musicLrc {
width: calc(50% - 5px);
height: 100%;
background-color: rgb(0, 0, 0);
border-radius: 10px;
color: #ccc;
line-height: 22px;
/* overflow: hidden; */
transform: translateY();
overflow-x: hidden;
overflow-y: auto;
}
.musicLrc_bady {
li {
transition: 0.8s;
}
}
.musicLrc_action {
transform: scale(1.5);
color: #409eff;
}
</style>以上就是基于Vue2實(shí)現(xiàn)歌曲播放和歌詞滾動(dòng)效果的詳細(xì)內(nèi)容,更多關(guān)于Vue2歌曲播放和歌詞滾動(dòng)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue 搭建后臺(tái)系統(tǒng)模塊化開發(fā)詳解
這篇文章主要介紹了vue 搭建后臺(tái)系統(tǒng)模塊化開發(fā)詳解,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-05-05
解決vue.js在編寫過(guò)程中出現(xiàn)空格不規(guī)范報(bào)錯(cuò)的問(wèn)題
下面小編就為大家?guī)?lái)一篇解決vue.js在編寫過(guò)程中出現(xiàn)空格不規(guī)范報(bào)錯(cuò)的問(wèn)題。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-09-09
Vue2.0 實(shí)現(xiàn)移動(dòng)端圖片上傳功能
本文主要介紹VUE2.0圖片上傳功能的實(shí)現(xiàn)。原理是通過(guò)js控制和input標(biāo)簽的方式完成這一效果,無(wú)需加載其他組件。具體實(shí)例大家大家參考下本文2018-05-05
vue實(shí)現(xiàn)html轉(zhuǎn)化pdf并復(fù)制文字
這篇文章主要為大家詳細(xì)介紹了vue實(shí)現(xiàn)html轉(zhuǎn)化pdf的兩種方式,分別為能復(fù)制文字和不能復(fù)制文字的方法,有需要的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-10-10
VUE中對(duì)object.object和object[object]的使用解讀
這篇文章主要介紹了VUE中對(duì)object.object和object[object]的使用,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-06-06

