需求:需要实现歌词滚动效果。
思路:通过js+css的transform属性完成。
难点:主要就是需要知道正在播放的歌词是那句,然后对正在播放的歌词进行变色和放大,最难的就是让高亮歌词随着歌曲播放滚动。
1.先看效果图
2.处理歌词格式(项目中有后端兄弟实现转换的可以省略)
// 处理歌词格式
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]);
},
歌词格式一般都是数组对象,对象的key各位可以自己根据需要命名,主要是思路。思路最重要!思路最重要!思路最重要!
我这里的数据格式如下图
3.利用audio的timeupdate的事件来进行计算歌词是否高亮以及偏移量
// 添加audio事件
autoDown() {const that = this;var audio = document.getElementById("myAudio");audio.addEventListener("ended", function () {that.switchingBtn("down");});audio.addEventListener("timeupdate", function () {that.computingTime();});
},// 计算播放时间对应的下标
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;
},
// 计算偏移量(保证高亮的歌词在中间)
computingOffset(index) {// 外部大盒子高度let musicLrcBoxHeight = this.$refs.musicLrc?.clientHeight;// 歌词总高度let musicLrcHeight = this.$refs.musicLrc_bady?.clientHeight;// 每个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两个事件是核心代码!!!
4.整个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">请先选择本地音乐!!!</span><inputref="audioInput"style="display: none; height: 10px"type="file"@change="handleFileSelect"multipleaccept="audio/*"/></div><div class="bs p10" v-if="selectedFiles?.length > 0" style="height: 60px"><audioid="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"><liv-for="(v, i) in selectedFiles[playIndex]?.lrcList":key="i"ref="musicLrc_item"><p :class="[i == currentIndex ? 'musicLrc_action ' : '']">{{ v.words }}</p></li><divv-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;},// 判断播放状态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]);},// 计算播放时间对应的下标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;},// 计算偏移量(保证高亮的歌词在中间)computingOffset(index) {// 外部大盒子高度let musicLrcBoxHeight = this.$refs.musicLrc?.clientHeight;// 歌词总高度let musicLrcHeight = this.$refs.musicLrc_bady?.clientHeight;// 每个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>