效果图
第一下载想听的歌曲和歌词(带时间轴的)
歌曲宝点击前往
歌词样例:
[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页面所在文件夹下面,方便写路径
下面是代码片段和完整代码
-
导入依赖:
import { ref, onMounted } from 'vue'; import axios from 'axios';
ref
和onMounted
是 Vue 3 的响应式 API,用于管理响应式变量和生命周期钩子。axios
用于进行 HTTP 请求,以获取歌词数据。
-
组件定义:
export default { setup() { // ... } };
- 该组件使用
setup
函数定义,并采用组合式 API。
- 该组件使用
响应式变量
- 定义响应式变量:
const lrcs = ref(''); // 用于存放歌词内容 const lrcData = ref([]); // 存放解析后的歌词数据 const audio = ref(null); // 用于引用 audio 元素 const ul = ref(null); // 用于引用歌词列表的 ul 元素 const container = ref(null); // 用于引用包含歌词的容器
歌词解析函数
-
时间解析函数:
const parseTime = (timeStr) => { const parts = timeStr.split(':'); return (+parts[0] * 60) + (+parts[1]) || 0; };
- 将歌词中时间格式(如
00:30
)转换为秒数,以便后续处理。
- 将歌词中时间格式(如
-
解析歌词函数:
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
。
歌词显示和同步
-
寻找当前歌词索引:
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; // 播放到最后一句 };
- 根据当前音频播放时间来定位应该高亮显示的歌词。
-
创建歌词 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
中,使用文档片段提高性能。
- 动态创建每一行歌词的
-
设置歌词偏移量:
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'); // 添加活跃类 } };
生命周期钩子
- 组件挂载时获取歌词:
onMounted(() => { fetchLyrics(); // 在组件挂载时获取歌词 audio.value.addEventListener('timeupdate', setOffset); // 监听音频时间更新 });
- 使用
onMounted
钩子在组件挂载后调用fetchLyrics
函数获取歌词。 - 同时,监听
timeupdate
事件,以便在播放器时间变化时更新歌词位置。
- 使用
获取歌词的函数
- 网络请求获取歌词:
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 请求获取歌词,并处理响应数据。如果请求失败,则在控制台输出错误信息。
模板和样式
-
模板部分:
<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
和一个音频播放组件。
- 定义了一个简单的 HTML 结构,其中包括一个绘制歌词的
-
样式部分:
<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>