前言
最终实现效果gif:
1. 实现思路
- 解析歌词 拿到歌词数组 ->
lyricArr = [
{time: 0, lyric: '给我你的爱', uid: 111222},
{time: 1.2, lyric: '可以不可以', uid: 222333},
{time: 4.45, lyric: '啊WDNMD', uid: 333444},
]
- 渲染歌词dom ->
<span class='' data-index='0'>给我你的爱</span>
<span class='' data-index='1'>可以不可以</span>
<span class='' data-index='2'>啊WDNMD</span>
- 通过
audio
标签的timeupdate
钩子,可以拿到歌曲的实时播放时间currentTime
- 定义
Idx
,初始值为0
,全局的索引(当前高亮歌词的索引,也就是歌词数组的索引) - 在
timeupdate
钩子函数中,当currentTime
大于lyricArr[Idx].time
,获取到当前歌词dom绑定的data-index
属性的索引index
,又当index
和Idx
相等时,把Idx
的值赋值给lyricIndex
。PS:lyricIndex
是专门用来与标签里的索引(data-index)匹配,判断是否需要添加高亮样式。当Idx
的值大于lyricArr
的长度,就return false
, 代码不再执行了。 - 定义全局的
scrollHeight
,用来滚动的。利用lyricArr[Idx].offsetHeight
来获取当前匹配的歌词dom的高度,然后使用scrollHeight += lyricArr[Idx].offsetHeight
来进行累加。最后使用js改变transform:translateY()
属性即可实现自行滚动
2. 解析歌词
2.1 接口返回的歌词样式
lyric: "[00:00.000] 作词 : 马思唯 Masiwei/朴宰范 Jay Park/HARIKIRI↵[00:00.930] 作曲 : HARIKIRI/马思唯 Masiwei/朴宰范 Jay Park↵[00:01.860] 编曲 : HARIKIRI↵[00:02.790] 制作人 : HARIKIRI↵[00:03.720](Masiwei)↵[00:03.725]HARIKIRI on the beat, yeah↵[00:07.429]Prince Charming↵[00:09.033]Look, check this out↵[00:10.812]↵[00:13.975](Masiwei)↵[00:13.976]我不敢对你保证↵[00:15.442]玫瑰花她会⼀直盛开↵[00:17.398]今天晚上⽉亮会不会在↵[00:18.968]丘⽐特什么时候才会到来↵[00:21.117]我不敢对你保证↵[00:22.633]我能⼀直富有我能⼀直年轻↵[00:24.634]⼀直拥有魔⼒充满活⼒↵[00:26.171]不管遇到什么难题我都能够⼀直开⼼↵[00:28.775]↵[00:29.500](Masiwei)↵[00:29.501]there’s only one thing↵[00:31.257]that I can promise you↵[00:33.122]that I can promise you↵[00:34.888]yeah uh↵[00:36.660]there’s only one thing↵[00:38.476]that I can promise you↵[00:40.209](Masiwei & Jay Park)↵[00:40.210]I love you↵[00:42.556]↵[00:42.689](Jay Park)↵[00:42.690]I’ll give it all up for you↵[00:45.861]If you wanted to↵[00:47.515]It’d be nothing to me↵[00:49.797]널 가질수 있다면↵[00:53.037]다가와~~~↵[00:56.792]我期待baby 我期待↵[01:00.130]快点回到我身边↵[01:02.451]别等待 我依赖baby↵[01:05.892]我太依赖你↵[01:07.675]习惯有你在别离开↵[01:11.527]I love you, I promise↵[01:13.342]I’ll treat you, like a goddess↵[01:15.227]The sun don’t shine so bright↵[01:16.673]but I’ll still be by your side↵[01:18.749]I want to, I’m honest↵[01:20.525]Girl you so, damn flawless↵[01:22.423]你让我迷上了你的每⼀⾯↵[01:26.423]↵[01:26.742](Masiwei)↵[01:26.743]there’s only one thing↵[01:28.592]that I can promise you↵[01:30.392]that I can promise you↵[01:32.327]yeah uh↵[01:33.959]there’s only one thing↵[01:35.698]that I can promise you↵[01:37.583](Masiwei & Jay Park)↵[01:37.584]I love you↵[01:39.302]↵[01:39.385](Masiwei)↵[01:39.386]你不⽤去染头发↵[01:41.109]快点准备好⾛吧↵[01:42.883]我已经在你家楼下↵[01:44.682]让我做你的欧巴↵[01:46.516]对我来说你太神奇↵[01:48.166]别再犹豫 ride with me↵[01:49.974]虽然不懂 do rei mi↵[01:51.790]你让我想唱rnb↵[01:53.721]I be 在街⻆ 在夜店 追寻你的轨迹↵[01:57.593]不⽤为我做任何改变↵[01:59.219]在我眼⾥你最美丽↵[02:01.207]塞纳河边和我散步↵[02:02.824]baby让我做你的莎⼠⽐亚↵[02:04.607]对你说过的话 那都绝对算数 把你变成诗歌写在笔下↵[02:08.257]↵[02:10.009]서울 안에서↵[02:11.660]소주 한잔해↵[02:13.475]다시 출발해↵[02:15.326]저 꿈나라로↵[02:17.008]우리 함께 구름위로↵[02:18.687]我要带你去宇宙↵[02:20.428]baby we can fly↵[02:23.070]↵[02:23.078](Jay Park)↵[02:23.079]I love you, I promise↵[02:25.012]I’ll treat you, like a goddess↵[02:26.830]The sun don’t shine so bright↵[02:28.323]but I’ll still be by your side↵[02:30.300]I want to, I’m honest↵[02:32.218]Girl you so, damn flawless↵[02:33.999]你让我迷上了你的每⼀⾯↵[02:37.640]↵[02:38.381](Masiwei)↵[02:38.382]there’s only one thing↵[02:40.177]that I can promise you↵[02:42.060]that I can promise you↵[02:44.940]uh↵[02:45.639]there’s only one thing↵[02:47.440]that I can promise you↵[02:49.179](Masiwei & Jay Park)↵[02:49.180]I love you↵[02:51.982]↵[02:52.815]there’s only one thing↵[02:54.597]that I can promise you↵[02:56.348]that I can promise you↵[02:59.108]uh↵[02:59.868]there’s only one thing↵[03:01.707]that I can promise you↵[03:03.556](Masiwei & Jay Park)↵[03:03.557]I love you↵[03:06.307]↵[03:07.045]there’s only one thing↵[03:08.775]that I can promise you↵[03:10.711]that I can promise you↵[03:13.523]uh↵[03:14.057]there’s only one thing↵[03:16.091]that I can promise you↵[03:17.781](Masiwei & Jay Park)↵[03:17.782]I love you↵[03:20.784]↵[03:21.418] 配唱制作人 : HARIKIRI↵[03:22.052] 吉他 : Joey “Iki” Lee↵[03:22.686] 录音工程 : HARIKIRI↵[03:23.320] 录音室 : HARIKIRI Studio↵[03:23.954] 人声处理 : HARIKIRI↵[03:24.588] 混⾳⼯程师:HeadAche / HARIKIRI ↵[03:25.222] 混音助理:Jayda Love ↵[03:25.856] 混音室:Sidechain Studio / HARIKIRI Studio ↵[03:26.490] 母带处理:HeadAche↵[03:27.124] 母带处理室:Sidechain Studio ↵[03:27.758] 制作公司:HARIKIRI Music Ltd (UK)↵"
就是一行字符串 都没有给你换行的那种~
2.2 处理歌词,生成歌词数组
// src/typings/index.ts
export interface ILyric {
time: number,
lyric: string,
uid: number
}
// src/utils/formatAxiosRes.ts
interface IReturnLyric {
lyric: ILyric[],
tlyric?: ILyric[]
}
export const formatMusicLyrics = (lyric?: string, tlyric?: string):IReturnLyric => {
if (lyric === '') {
return { lyric: [{ time: 0, lyric: '这个地方没有歌词!', uid: 520520 }] }
}
const lyricObjArr: ILyric[] = [] // 最终生成的歌词数组
// 将歌曲字符串变成数组,数组每一项就是当前歌词信息
const lineLyric:any = lyric?.split(/\n/)
// 匹配中括号里正则的
const regTime = /\d{2}:\d{2}.\d{2,3}/
// 循环遍历歌曲数组
for (let i = 0; i < lineLyric?.length; i++) {
if (lineLyric[i] === '') continue
const time:number = formatLyricTime(lineLyric[i].match(regTime)[0])
if (lineLyric[i].split(']')[1] !== '') {
lyricObjArr.push({
time: time,
lyric: lineLyric[i].split(']')[1],
uid: parseInt(Math.random().toString().slice(-6))
})
}
}
console.log(lyricObjArr)
return {
lyric: lyricObjArr
}
}
const formatLyricTime = (time: string) => {
const regMin = /.*:/
const regSec = /:.*\./
const regMs = /\./
const min = parseInt((time.match(regMin) as any)[0].slice(0, 2))
let sec = parseInt((time.match(regSec) as any)[0].slice(1, 3))
const ms = time.slice((time.match(regMs) as any).index + 1, (time.match(regMs) as any).index + 3)
if (min !== 0) {
sec += min * 60
}
return Number(sec + '.' + ms)
}
2.3 最终生成的歌词数组
这里我只格式化了原歌词,翻译歌词是没有格式化的。如果有需要的伙伴可照着操作。
3. 实现歌词滚动
3.1 dom结构
<div ref="lyricDiv" class="lyrics">
<ul ref="lyric" style="transition: .3s">
<span
v-for='(item, index) in playingMusic.lyric'
:key="item.uid"
:class="{acting: lyricIndex === index}"
:data-index='index'
>
{{item.lyric.trim()}}
</span>
</ul>
</div>
<audio @timeupdate="handleTimeUpdate"></audio>
3.2 TS控制
setup() {
const lyric = ref<any>(null) // dom - 包含歌词标签的ul,高度很高,主要用于控制transform
const lyricDiv = ref<any>(null) // dom - 高度固定的外层div,主要用于html结构
const lyricIndex = ref<number>(0) // 当前高亮歌词的索引,与span标签里的index进行比较,判断是否高亮
let scrollHeight: number = 0 // 歌词区域要滚动的高度
let lyricHeight: number = 0 // 歌词区域最外层区域高度,也就是lyricDiv的高度
let flag: boolean = true // 判断当前高亮的索引是否已经超过了歌词数组的长度
// 处理歌曲播放进程
const handleTimeUpdate = (e:any): void => {
const { currentTime } = e.target
handleLyricTransform(currentTime)
}
// 歌词滚动
const handleLyricTransform = (currentTime: number): void => {
const item: ILyric = playingMusic.lyric[Idx]
if (flag && currentTime > item.time) {
// 实时获取dom,不然在上一首/下一首切换时,dom不会更新
lyricDomArr = lyric.value.querySelectorAll('span')
// 拿到当前正在播放的歌词span的索引
const index = parseInt(lyricDomArr[Idx].dataset.index)
if (Idx === index) {
lyricIndex.value = Idx
Idx += 1
if (Idx >= lyricDomArr.length) {
flag = false
return
}
scrollHeight += lyricDomArr[Idx].offsetHeight
if (lyric.value) {
lyric.value.style.transform = `translateY(${lyricHeight - scrollHeight}px)`
}
}
}
}
onMounted(() => {
// 获取span标签数组
lyricDomArr = lyric.value.querySelectorAll('span')
// 获取固定歌词区域高度的一半 用来让高亮歌词始终居中
lyricHeight = lyricDiv.value.offsetHeight / 2
})
return {
lyric,
lyricDiv,
lyricIndex,
handleTimeUpdate
}
}
4. 注意点
在切换歌曲后要记得重置lyric
的transform:translateY()
属性为0
。Idx
为0
,scrollHeight
为0
,flag
为true
。