1 实现功能
随着时间滚动播放歌词
高亮放大当前播放的歌词文字
可以拖动歌词
双击歌词切换到歌词对应的播放时间
2 实现思路
定义了一些数据,包括歌词列表
、每行歌词的高度
、歌词滚动的距离
、当前歌词索引
、当前播放时间
、总时长
。
主要方法:getLyric
、updateTime
、setCurrentTime
、getDuration
、mouseDown
、move
、mouseUp
。
2.1 getLyric
获取歌词
2.2 updateTime
更新当前播放时间
并判断是否需要滚动
2.3 setCurrentTime
设置当前audio元素播放时间
双击歌词切换到歌词对应的播放时间
2.4 getDuration
获取audio元素总时长
2.5 mouseDown
鼠标按下事件 主要为了添加鼠标移动事件
2.6 move
鼠标移动事件 设置滚动距离
2.7 mouseUp
鼠标抬起事件 主要是为了移除鼠标移动事件
3 代码
3.1 HTML
<template>
<div>
<audio @timeupdate="updateTime" ref="audioRef" :src="audio" controls
@canplay="getDuration"></audio>
<div class="lyric" :style="{height:`${lineHeight*10}px`}" @mousedown="mouseDown" @mouseup="mouseUp"
@mouseleave="mouseUp">
<div class="lyric-item" v-for="item in lyric" :key="item.index"
:style="{transform:`translateY(${moveY}px)`,height:`${lineHeight}px`,lineHeight:`${lineHeight}px`}"
:title="item.formatTime" :class="{active:currentIndex===item.index,noAnimate}"
@dblclick="setCurrentTime(item.time)">
{{ item.lyric }}
</div>
</div>
</div>
</template>
3.2 JavaScript
<script setup lang="ts">
import audio from "@/assets/audios/漠河舞厅.mp3";
import { nextTick, onMounted, onUnmounted, ref } from "vue";
import axios from "axios";
const audioRef = ref() // audio元素
const lyric = ref<Lyric[]>([]) // 歌词列表
const lineHeight = ref(40) // 每行歌词的高度
const moveY = ref(lineHeight.value * 4) // 歌词滚动的距离 默认为居中
const currentIndex = ref(0) // 当前歌词索引
let timer: any; // 定时器
const currentTime = ref(0) // 当前播放时间
const duration = ref(0) // 总时长
const noAnimate = ref(false) // 是否不需要动画
/**
* 歌词类型
*/
interface Lyric {
time: number;
lyric: string;
index: number;
formatTime: string;
}
/**
* 格式化时间 000.000
* @param time 时间
*/
const formatTime = (time: string) => {
let min = time.split(":")[0] ? time.split(":")[0] : 0;
let sec = time.split(":")[1] ? time.split(":")[1] : 0;
return Number(min) * 60 + Number(sec);
};
/**
* 格式化时间 00:00
* @param time 时间
*/
const formatTime2 = (time: number) => {
time = Number(time.toFixed(0))
if (time < 10) {
return "0:" + "0" + time;
}
if (time < 60 && time >= 10) {
return "0:" + time;
}
if (time >= 60) {
let n = time / 60;
n = Math.floor(n);
let y = time % 60;
if (y < 10) {
return n + ":" + "0" + y;
} else {
return n + ":" + y;
}
}
return "0:00";
}
/**
* 获取歌词
*/
const getLyric = async () => {
const res = await axios({
url: "/json/data.json",
})
lyric.value = []
const r = res.data.lyric;
const re = /\[([^\]]+)\]([^\[]+)/g;
let i = 0;
r.replace(re, (_: any, $1: any, $2: any) => {
if (!$2.replace(/\n/g, '').trim()) return "";
lyric.value.push({
time: formatTime($1),
lyric: $2.replace(/\n/g, '').trim(),
index: i++,
formatTime: formatTime2(formatTime($1))
});
return "";
});
};
/**
* 更新当前播放时间 并判断是否需要滚动
* @param e 事件
*/
const updateTime = (e: any) => {
currentTime.value = e.currentTarget!!.currentTime
// 判断是否播放完毕
if (currentTime.value >= duration.value) {
audioRef.value!.pause()
}
// 判断是否需要滚动
lyric.value.forEach(item => {
if (item.time <= currentTime.value + 0.2) {
currentIndex.value = item.index
if (!noAnimate.value) {
moveY.value = lineHeight.value * 4 - lineHeight.value * item.index
}
}
})
}
/**
* 设置当前audio元素播放时间
* @param time 时间
*/
const setCurrentTime = (time: number) => {
audioRef.value.currentTime = time;
}
/**
* 获取audio元素总时长
*/
const getDuration = () => {
duration.value = audioRef.value!.duration
}
/**
* 鼠标按下事件
* @param e 事件
*/
const mouseDown = (e: MouseEvent) => {
e.currentTarget!.addEventListener('mousemove', <EventListenerOrEventListenerObject>move) // 添加鼠标移动事件
}
/**
* 鼠标移动事件
* @param e 事件
*/
const move = (e: MouseEvent) => {
clearTimeout(timer);
// 设置不需要动画 如果有动画会导致滚动距离不准确
noAnimate.value = true
// 获取鼠标移动的距离
const disY = e.movementY
// 判断是否超出范围
if (moveY.value + disY > lineHeight.value * 4) {
moveY.value = 4 * lineHeight.value
return
} else if (moveY.value + disY < -lineHeight.value * (lyric.value.length - 5)) {
moveY.value = -lineHeight.value * (lyric.value.length - 5)
return
}
// 设置滚动距离
moveY.value += disY
}
/**
* 鼠标抬起事件
* @param e 事件
*/
const mouseUp = (e: MouseEvent) => {
e.currentTarget!.removeEventListener('mousemove', <EventListenerOrEventListenerObject>move) // 移除鼠标移动事件
timer = setTimeout(async () => { // 2秒后恢复动画
noAnimate.value = false;
await nextTick()
}, 2000)
}
onMounted(() => {
// 获取歌词
getLyric()
});
onUnmounted(() => {
clearTimeout(timer)
})
</script>
3.3 CSS
<style lang="less" scoped>
.lyric {
height: 300px;
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
user-select: none;
cursor: grab;
.lyric-item {
height: 30px;
line-height: 30px;
text-align: center;
color: #333;
font-size: 16px;
transition: all .3s;
font-weight: 600;
&.noAnimate {
transition: font .3s;
}
&.active {
color: var(--el-color-primary);
font-size: 22px;
}
}
}
</style>