vue实现歌词实时滚动 播放器页面

效果图

第一下载想听的歌曲和歌词(带时间轴的)

歌曲宝点击前往

歌词样例:

[00:00.0]一个人挺好 - 孟颖
[00:05.32]词:杨小壮

第二创建vue项目(由于简单跳过,不会的点击前往

第三 由于歌词可以通过网络请求获得,也可以创建js文件存储歌词

网络请求需要安装axios

npm install axios

创建js文件

在任意位置创建应该js文件将歌词复制进去

export let lirc = `[00:00.0]一个人挺好 - 孟颖
[00:05.32]词:杨小壮
[00:10.65]曲:杨小壮
[00:15.98]OP:乐巢文化(Solo Music)
........
[03:00.82]陷得越深越困扰
[03:03.08]就这样吧一个人挺好`;

这里歌词我省略了

第四创建一个vue界面(MusicView.vue),在router里面写出他的路由,以便浏览器能访问页面

将下载的歌曲移动到vue页面所在文件夹下面,方便写路径

下面是代码片段和完整代码

  1. 导入依赖

    import { ref, onMounted } from 'vue';
    import axios from 'axios';
    
    • ref 和 onMounted 是 Vue 3 的响应式 API,用于管理响应式变量和生命周期钩子。
    • axios 用于进行 HTTP 请求,以获取歌词数据。
  2. 组件定义

    export default {
      setup() {
        // ...
      }
    };
    
    • 该组件使用 setup 函数定义,并采用组合式 API。

响应式变量

  1. 定义响应式变量
    const lrcs = ref(''); // 用于存放歌词内容
    const lrcData = ref([]); // 存放解析后的歌词数据
    const audio = ref(null); // 用于引用 audio 元素
    const ul = ref(null); // 用于引用歌词列表的 ul 元素
    const container = ref(null); // 用于引用包含歌词的容器
    

歌词解析函数

  1. 时间解析函数

    const parseTime = (timeStr) => {
      const parts = timeStr.split(':');
      return (+parts[0] * 60) + (+parts[1]) || 0;
    };
    
    • 将歌词中时间格式(如 00:30)转换为秒数,以便后续处理。
  2. 解析歌词函数

    const parseLrc = () => {
      const lines = lrcs.value.split('\n');
      return lines.map(line => {
        const parts = line.split(']');
        const timeStr = parts[0].substring(1);
        return {
          time: parseTime(timeStr),
          words: parts[1] || ''
        };
      });
    };
    
    • 将歌词字符串按照换行符分割,并解析每一行的时间和歌词内容。
    • 返回一个对象数组,每个对象包含 time 和 words

歌词显示和同步

  1. 寻找当前歌词索引

    const findIndex = () => {
      const curTime = audio.value.currentTime;
      for (let i = 0; i < lrcData.value.length; i++) {
        if (curTime < lrcData.value[i].time) {
          return i > 0 ? i - 1 : -1; // 确保索引不会小于 0
        }
      }
      return lrcData.value.length - 1; // 播放到最后一句
    };
    
    • 根据当前音频播放时间来定位应该高亮显示的歌词。
  2. 创建歌词 DOM 元素

    const createLrcElements = () => {
      const frag = document.createDocumentFragment();
      lrcData.value.forEach(item => {
        const li = document.createElement('li');
        li.textContent = item.words;
        frag.appendChild(li);
      });
      ul.value.appendChild(frag);
    };
    
    • 动态创建每一行歌词的 li 元素,并添加到 ul 中,使用文档片段提高性能。
  3. 设置歌词偏移量

    const setOffset = () => {
      const index = findIndex(); // 查找当前歌词索引
      const containerHeight = container.value.clientHeight;
      const liHeight = ul.value.children[0]?.clientHeight || 0;
      const maxOffset = Math.max(ul.value.clientHeight - containerHeight, 0);
      let offset = liHeight * index + liHeight / 2 - containerHeight / 2;
      offset = Math.max(0, Math.min(offset, maxOffset)); // 确保 offset 在合法范围内
      ul.value.style.transform = `translateY(-${offset}px)`; // 更新 ul 的位移
      // 高亮当前歌词
      const activeLi = ul.value.querySelector('.active');
      if (activeLi) {
        activeLi.classList.remove('active');
      }
      const li = ul.value.children[index];
      if (li) {
        li.classList.add('active'); // 添加活跃类
      }
    };
    

生命周期钩子

  1. 组件挂载时获取歌词
    onMounted(() => {
      fetchLyrics(); // 在组件挂载时获取歌词
      audio.value.addEventListener('timeupdate', setOffset); // 监听音频时间更新
    });
    
    • 使用 onMounted 钩子在组件挂载后调用 fetchLyrics 函数获取歌词。
    • 同时,监听 timeupdate 事件,以便在播放器时间变化时更新歌词位置。

获取歌词的函数

  1. 网络请求获取歌词
    const fetchLyrics = async () => {
      try {
        const response = await axios.post("api/user/music", "hhhh"); // 发送请求
        lrcs.value = response.data.data; // 更新歌词
        lrcData.value = parseLrc(); // 解析歌词数据
        createLrcElements(); // 创建歌词 DOM 元素
      } catch (error) {
        console.error("获取歌词失败:", error); // 错误处理
      }
    };
    
    • 发送 POST 请求获取歌词,并处理响应数据。如果请求失败,则在控制台输出错误信息。

模板和样式

  1. 模板部分

    <template>
      <div class="body">
        <br>
        <div class="container" ref="container">
          <ul class="lrc-list" ref="ul"></ul>
        </div>
        <audio ref="audio" controls>
          <source src="../music/一个人挺好-孟颖.mp3" type="audio/mpeg">
        </audio>
      </div>
    </template>
    
    • 定义了一个简单的 HTML 结构,其中包括一个绘制歌词的 ul 和一个音频播放组件。
  2. 样式部分

    <style>
    * {
      margin: 0;
      padding: 0;
    }
    
    .body {
      background: url("../miscu/181750LAK1m.jpg");
      color: #666;
      height: 100vh;
      text-align: center;
    }
    
    audio {
      width: 450px;
      margin: 30px 0;
    }
    
    .container {
      margin-top: 15%;
      height: 420px;
      overflow: hidden;
    }
    
    .container ul {
      transition: 0.6s;
      list-style: none;
    }
    
    .container li {
      height: 30px;
      line-height: 30px;
      transition: 0.2s;
    }
    
    .container li.active {
      color: sandybrown;
      transform: scale(1.2);
    }
    </style>
    
    • 定义了一些基础样式,包括背景、音频元素的样式、歌词容器的大小以及歌词列表的样式等。

完整代码

代码中的歌曲路径和歌词路径可能不同,修改即可,

这里我用的是js文件存储歌词,下一次改为网络请求得到歌词

<script>
import {ref, onMounted} from 'vue';
import {lirc} from '../music/data'


export default {
  setup() {
    const lrc = lirc; // 您可以在这里设置歌词
    const lrcData = ref([]);
    const audio = ref(null);
    const ul = ref(null);
    const container = ref(null);
    const parseTime = (timeStr) => {
      const parts = timeStr.split(':');
      return (+parts[0] * 60) + (+parts[1]) || 0; // 确保返回值为数字
    };
    const parseLrc = () => {
      const lines = lrc.split('\n');
      return lines.map(line => {
        const parts = line.split(']');
        const timeStr = parts[0].substring(1);
        return {
          time: parseTime(timeStr),
          words: parts[1] || ''
        };
      });
    };

    const findIndex = () => {
      const curTime = audio.value.currentTime;
      for (let i = 0; i < lrcData.value.length; i++) {
        if (curTime < lrcData.value[i].time) {
          return i > 0 ? i - 1 : -1; // 确保索引不会小于0
        }
      }
      return lrcData.value.length - 1; // 播放到最后一句
    };

    const createLrcElements = () => {
      const frag = document.createDocumentFragment();
      lrcData.value.forEach(item => {
        const li = document.createElement('li');
        li.textContent = item.words;
        frag.appendChild(li);
      });
      ul.value.appendChild(frag);
    };
    const setOffset = () => {
      const index = findIndex();
      const containerHeight = container.value.clientHeight;
      const liHeight = ul.value.children[0]?.clientHeight || 0; // 使用可选链防止报错
      const maxOffset = Math.max(ul.value.clientHeight - containerHeight, 0);
      let offset = liHeight * index + liHeight / 2 - containerHeight / 2;
      offset = Math.max(0, Math.min(offset, maxOffset)); // 确保 offset 在合法范围内
      ul.value.style.transform = `translateY(-${offset}px)`;

      const activeLi = ul.value.querySelector('.active');
      if (activeLi) {
        activeLi.classList.remove('active');
      }

      const li = ul.value.children[index];
      if (li) {
        li.classList.add('active');
      }
    };
    onMounted(() => {
      lrcData.value = parseLrc();
      createLrcElements();
      audio.value.addEventListener('timeupdate', setOffset);
    });
    return {
      audio,
      ul,
      container,
      lrc
    };
  }
}
</script>

<template>
  <div class="body">
    <br>
    <div class="container" ref="container">
      <ul class="lrc-list" ref="ul"></ul>
    </div>
    <audio ref="audio" controls>
      <source src="../music/一个人挺好-孟颖.mp3" type="audio/mpeg">
    </audio>
  </div>
</template>

<style>
* {
  margin: 0;
  padding: 0;
}

.body {
  background: url("../miscu/181750LAK1m.jpg");
  color: #666;
  height: 100vh;
  text-align: center;
}

audio {
  width: 450px;
  margin: 30px 0;
}

.container {
  margin-top: 15%;
  height: 420px;
  overflow: hidden;
  /* border: 2px solid #fff; */
}

.container ul {
  /* border: 2px solid #fff; */
  transition: 0.6s;
  list-style: none;
}

.container li {
  height: 30px;
  /* border: 1px solid #fff; */
  line-height: 30px;
  transition: 0.2s;
}

.container li.active {
  color: sandybrown;
  /* font-size: ; */
  transform: scale(1.2);
}
</style>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值