基于Vue2實(shí)現(xiàn)歌曲播放和歌詞滾動(dòng)效果
需求:需要實(shí)現(xiàn)歌詞滾動(dòng)效果。
思路:通過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的事件來進(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è)事件是核心代碼!?。?/strong>
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)先選擇本地音樂!?。?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"
>
暫無歌詞
</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: {
// 添加本地音樂
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)文章
Vue3實(shí)現(xiàn)表單輸入的雙向綁定的示例代碼
Vue.js 是一個(gè)漸進(jìn)式的JavaScript框架,廣泛用于構(gòu)建交互式用戶界面,在 Vue 中,雙向綁定是一項(xiàng)非常核心的概念,尤其是在處理表單輸入時(shí),它使得數(shù)據(jù)和用戶界面之間的互動(dòng)變得簡(jiǎn)單又高效,在本篇博客中,我們將深入探討如何在 Vue 3 中實(shí)現(xiàn)表單輸入的雙向綁定2025-01-01
Vue3使用Vue Router實(shí)現(xiàn)前端路由控制
在現(xiàn)代Web應(yīng)用中,前端路由控制是非常重要的一部分,它可以幫助我們將不同的頁面內(nèi)容展示給用戶,同時(shí)保持用戶在瀏覽不同頁面時(shí)的連貫性,本文將介紹如何使用Vue Router來實(shí)現(xiàn)前端路由控制,需要的朋友可以參考下2024-10-10
Vue3中虛擬dom轉(zhuǎn)成真實(shí)dom的過程詳解
Vue.js?在其運(yùn)行過程中會(huì)將模板編譯成虛擬?DOM?(VNode),然后再將?VNode?渲染成實(shí)際的?DOM?節(jié)點(diǎn),這個(gè)過程是由?Vue?內(nèi)部的編譯器和渲染系統(tǒng)完成的,本文給大家介紹了Vue3中虛擬dom轉(zhuǎn)成真實(shí)dom的過程,需要的朋友可以參考下2024-09-09
Electron進(jìn)程間通信的實(shí)現(xiàn)
本文主要介紹了Electron進(jìn)程間通信的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-05-05
解決vue-cli + webpack 新建項(xiàng)目出錯(cuò)的問題
下面小編就為大家分享一篇解決vue-cli + webpack 新建項(xiàng)目出錯(cuò)的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2018-03-03
Vue3?登錄頁懸浮動(dòng)畫和細(xì)膩交互的hover效果實(shí)現(xiàn)
這篇文章主要介紹了Vue3登錄頁懸浮動(dòng)畫和細(xì)膩交互的hover效果實(shí)現(xiàn)的相關(guān)資料,這樣的效果可以讓表單在視覺上更具層次感、反饋更及時(shí),需要的朋友可以參考下2026-05-05

