当前位置: 首页 > article >正文

vue2实现歌曲播放和歌词滚动效果

需求:需要实现歌词滚动效果。

思路:通过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>
        <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;
    },
    // 判断播放状态
    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>


http://www.kler.cn/a/301389.html

相关文章:

  • “fc-async”提供了基本的异步处理能力
  • 第六节、Docker 方式部署指南 github 上项目 mkdocs-material
  • MATLAB实现GARCH(广义自回归条件异方差)模型计算VaR(Value at Risk)
  • 9.C++面向对象6(实现一个较为完善的日期类)
  • SOP搭建:企业标准化操作程序构建与实施指南
  • 蓝桥杯每日真题 - 第15天
  • 【鸿蒙】HarmonyOS NEXT星河入门到实战5-基础语法
  • 【F的领地】项目拆解:小学教辅资料
  • 海外云手机——跨国业务的高效工具
  • Python中如何判断一个变量是否为None
  • Ubuntu快速安装Python3
  • 【C++】——vector
  • 数据库的介绍:关系型数据库和非关系型数据库究竟是什么?
  • 基于vue框架的城市体育运动交流平台15s43(程序+源码+数据库+调试部署+开发环境)系统界面在最后面。
  • springcloud-Zuul
  • SprinBoot+Vue健身俱乐部网站系统的设计与实现
  • 网页解析 lxml 库--实战
  • STM32 HAL freertos零基础(三) 队列
  • 时尚购物体验:Spring Boot技术在网页时装购物中的应用
  • UE中如何制作后处理设置面板
  • k8s的加密配置secret和应用配置configmap
  • 如何基于gpt模型抢先打造成功的产品
  • 【Leetcode152】乘积最大子数组(动态规划)
  • 音乐项目
  • JVM源码解析
  • 20道经典自动化测试面试题【建议收藏】