feat(歌词): 添加滚动歌词组件和接口定义
新增 ScrollingLyrics.vue 组件实现歌词滚动显示功能,包括: 1. 解析 LRC 格式歌词文本 2. 根据当前播放时间高亮对应歌词行 3. 支持歌词和间隔时间的处理 同时在 vite-env.d.ts 中新增 LyricsLine 和 GapLine 接口定义,并在 Playroom.vue 中集成歌词组件
This commit is contained in:
		
							parent
							
								
									3a5377595b
								
							
						
					
					
						commit
						7ecc38dda8
					
				
							
								
								
									
										143
									
								
								src/components/ScrollingLyrics.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								src/components/ScrollingLyrics.vue
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,143 @@
 | 
			
		|||
<script setup lang="ts">
 | 
			
		||||
import { onMounted, ref } from 'vue'
 | 
			
		||||
import axios from 'axios'
 | 
			
		||||
import { watch } from 'vue'
 | 
			
		||||
import { usePlayQueueStore } from '../stores/usePlayQueueStore'
 | 
			
		||||
 | 
			
		||||
const playQueueStore = usePlayQueueStore()
 | 
			
		||||
 | 
			
		||||
const parsedLyrics = ref<(LyricsLine | GapLine)[]>([])
 | 
			
		||||
const currentLineIndex = ref(0)
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	lrcSrc?: string
 | 
			
		||||
}>()
 | 
			
		||||
 | 
			
		||||
onMounted(async () => {
 | 
			
		||||
	if (props.lrcSrc) {
 | 
			
		||||
		const lrcContent = await axios.get(props.lrcSrc) 
 | 
			
		||||
		parsedLyrics.value = parseLyrics(lrcContent.data)
 | 
			
		||||
		console.log(parsedLyrics.value)
 | 
			
		||||
	}
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
function parseLyrics(lrcText: string, minGapDuration: number = 5): (LyricsLine | GapLine)[] {
 | 
			
		||||
  if (!lrcText) return []
 | 
			
		||||
  
 | 
			
		||||
  const lines = lrcText.split('\n')
 | 
			
		||||
  const tempParsedLines: (LyricsLine | GapLine)[] = []
 | 
			
		||||
  
 | 
			
		||||
  // LRC时间格式正则: [mm:ss.xx] 或 [mm:ss]
 | 
			
		||||
  const timeRegex = /\[(\d{1,2}):(\d{2})(?:\.(\d{1,3}))?\]/g
 | 
			
		||||
  
 | 
			
		||||
  // 第一步:解析所有时间标签和歌词
 | 
			
		||||
  for (const line of lines) {
 | 
			
		||||
    const matches = [...line.matchAll(timeRegex)]
 | 
			
		||||
    if (matches.length === 0) continue
 | 
			
		||||
    
 | 
			
		||||
    // 提取歌词文本(去掉所有时间标签)
 | 
			
		||||
    const text = line.replace(/\[\d{1,2}:\d{2}(?:\.\d{1,3})?\]/g, '').trim()
 | 
			
		||||
    
 | 
			
		||||
    // 解析每个时间标签
 | 
			
		||||
    for (const match of matches) {
 | 
			
		||||
      const minutes = parseInt(match[1])
 | 
			
		||||
      const seconds = parseInt(match[2])
 | 
			
		||||
      const milliseconds = match[3] ? parseInt(match[3].padEnd(3, '0')) : 0
 | 
			
		||||
      
 | 
			
		||||
      const totalSeconds = minutes * 60 + seconds + milliseconds / 1000
 | 
			
		||||
      
 | 
			
		||||
      if (text) {
 | 
			
		||||
        tempParsedLines.push({
 | 
			
		||||
          type: 'lyric',
 | 
			
		||||
          time: totalSeconds,
 | 
			
		||||
          text: text,
 | 
			
		||||
          originalTime: match[0]
 | 
			
		||||
        })
 | 
			
		||||
      } else {
 | 
			
		||||
        tempParsedLines.push({
 | 
			
		||||
          type: 'gap',
 | 
			
		||||
          time: totalSeconds,
 | 
			
		||||
          originalTime: match[0]
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // 按时间排序
 | 
			
		||||
  tempParsedLines.sort((a, b) => a.time - b.time)
 | 
			
		||||
  
 | 
			
		||||
  // 第二步:处理间隔,只保留超过指定时长的间隔
 | 
			
		||||
  const finalLines: (LyricsLine | GapLine)[] = []
 | 
			
		||||
  const lyricLines = tempParsedLines.filter(line => line.type === 'lyric') as LyricsLine[]
 | 
			
		||||
  const gapLines = tempParsedLines.filter(line => line.type === 'gap') as GapLine[]
 | 
			
		||||
  
 | 
			
		||||
  // 如果没有歌词行,直接返回
 | 
			
		||||
  if (lyricLines.length === 0) {
 | 
			
		||||
    return tempParsedLines
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // 为每个间隔计算持续时间
 | 
			
		||||
  for (let i = 0; i < gapLines.length; i++) {
 | 
			
		||||
    const gapLine = gapLines[i]
 | 
			
		||||
    
 | 
			
		||||
    // 找到这个间隔之后的第一个歌词行
 | 
			
		||||
    const nextLyricLine = lyricLines.find(lyric => lyric.time > gapLine.time)
 | 
			
		||||
    
 | 
			
		||||
    if (nextLyricLine) {
 | 
			
		||||
      const duration = nextLyricLine.time - gapLine.time
 | 
			
		||||
      gapLine.duration = duration
 | 
			
		||||
      
 | 
			
		||||
      // 只有当间隔超过最小持续时间时才保留
 | 
			
		||||
      if (duration >= minGapDuration) {
 | 
			
		||||
        finalLines.push(gapLine)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // 添加所有歌词行
 | 
			
		||||
  finalLines.push(...lyricLines)
 | 
			
		||||
  
 | 
			
		||||
  // 最终排序
 | 
			
		||||
  return finalLines.sort((a, b) => a.time - b.time)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function findCurrentLineIndex(time: number): number {
 | 
			
		||||
  if (parsedLyrics.value.length === 0) return -1
 | 
			
		||||
  
 | 
			
		||||
  let index = -1
 | 
			
		||||
  for (let i = 0; i < parsedLyrics.value.length; i++) {
 | 
			
		||||
    if (time >= parsedLyrics.value[i].time) {
 | 
			
		||||
      index = i
 | 
			
		||||
    } else {
 | 
			
		||||
      break
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return index
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
watch(() => playQueueStore.currentTime, (value) => {
 | 
			
		||||
	currentLineIndex.value = findCurrentLineIndex(value)
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
function calculateTopRange() {
 | 
			
		||||
	return window.innerHeight / 3
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
	<div class="w-[40rem] overflow-x-visible flex flex-col gap-4 pl-16" :style="{
 | 
			
		||||
		paddingTop: `${calculateTopRange()}px`,
 | 
			
		||||
		paddingBottom: `${calculateTopRange() * 2}px`
 | 
			
		||||
	}">
 | 
			
		||||
		<div class="" v-for="(line, index) in parsedLyrics" :key="index">
 | 
			
		||||
			<div v-if="line.type === 'lyric'" class="text-3xl font-bold relative">
 | 
			
		||||
				<div :class="currentLineIndex === index ? 'text-black/80 blur-3xl' : 'text-black/20 blur-3xl'">{{ line.text }}</div>
 | 
			
		||||
				<div class="absolute top-0" :class="currentLineIndex === index ? 'text-white' : 'text-white/50'">{{ line.text }}</div>
 | 
			
		||||
			</div>
 | 
			
		||||
			<!-- <div v-else class="text-white/50 text-sm">
 | 
			
		||||
				{{ line.originalTime }}
 | 
			
		||||
			</div> -->
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			@ -8,6 +8,8 @@ import { useTemplateRef } from 'vue'
 | 
			
		|||
import { ref, watch } from 'vue'
 | 
			
		||||
import { usePreferences } from '../stores/usePreferences'
 | 
			
		||||
 | 
			
		||||
import ScrollingLyrics from '../components/ScrollingLyrics.vue'
 | 
			
		||||
 | 
			
		||||
import RewindIcon from '../assets/icons/rewind.vue'
 | 
			
		||||
import ForwardIcon from '../assets/icons/forward.vue'
 | 
			
		||||
import PlayIcon from '../assets/icons/play.vue'
 | 
			
		||||
| 
						 | 
				
			
			@ -157,182 +159,187 @@ function calculateStickyTop() {
 | 
			
		|||
		<div class="bg-transparent w-full h-full absolute top-0 left-0" />
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<div class="absolute top-0 left-0 flex justify-center items-center h-screen w-screen overflow-y-auto gap-16 z-10 select-none">
 | 
			
		||||
		<div class="flex flex-col w-96 gap-4 sticky" :style="{
 | 
			
		||||
			top: `${calculateStickyTop()}px`
 | 
			
		||||
		}" ref="controllerRef">
 | 
			
		||||
			<img :src="getCurrentTrack().album?.coverUrl" class="rounded-2xl shadow-2xl border border-white/20 w-96 h-96" />
 | 
			
		||||
			<div class="flex justify-between items-center gap-2">
 | 
			
		||||
				<div class="relative flex-auto w-0">
 | 
			
		||||
					<div class="">
 | 
			
		||||
						<div class="text-black/90 blur-lg text-lg font-medium truncate w-80">
 | 
			
		||||
							{{ getCurrentTrack().song.name }}
 | 
			
		||||
	<div class="absolute top-0 left-0 flex justify-center  h-screen w-screen overflow-y-auto z-10 select-none">
 | 
			
		||||
		<div class="sticky w-96" :style="{
 | 
			
		||||
			top: `${calculateStickyTop()}px`,
 | 
			
		||||
			height: `${controllerRef?.clientHeight?? 0}px`
 | 
			
		||||
		}">
 | 
			
		||||
			<div class="flex flex-col w-96 gap-4" ref="controllerRef">
 | 
			
		||||
				<img :src="getCurrentTrack().album?.coverUrl" class="rounded-2xl shadow-2xl border border-white/20 w-96 h-96" />
 | 
			
		||||
				<div class="flex justify-between items-center gap-2">
 | 
			
		||||
					<div class="relative flex-auto w-0">
 | 
			
		||||
						<div class="">
 | 
			
		||||
							<div class="text-black/90 blur-lg text-lg font-medium truncate w-80">
 | 
			
		||||
								{{ getCurrentTrack().song.name }}
 | 
			
		||||
							</div>
 | 
			
		||||
							<div class="text-black/90 blur-lg text-base truncate w-80">
 | 
			
		||||
								{{ getCurrentTrack().song.artists ?? [] }} —
 | 
			
		||||
								{{ getCurrentTrack().album?.name ?? '未知专辑' }}
 | 
			
		||||
							</div>
 | 
			
		||||
						</div>
 | 
			
		||||
						<div class="text-black/90 blur-lg text-base truncate w-80">
 | 
			
		||||
							{{ getCurrentTrack().song.artists ?? [] }} —
 | 
			
		||||
							{{ getCurrentTrack().album?.name ?? '未知专辑' }}
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
 | 
			
		||||
					<div class="absolute top-0">
 | 
			
		||||
						<div class="text-white text-lg font-medium truncate w-80">
 | 
			
		||||
							{{ getCurrentTrack().song.name }}
 | 
			
		||||
						</div>
 | 
			
		||||
						<div class="text-white/75 text-base truncate w-80">
 | 
			
		||||
							{{ artistsOrganize(getCurrentTrack().song.artists ?? []) }} —
 | 
			
		||||
							{{ getCurrentTrack().album?.name ?? '未知专辑' }}
 | 
			
		||||
 | 
			
		||||
						<div class="absolute top-0">
 | 
			
		||||
							<div class="text-white text-lg font-medium truncate w-80">
 | 
			
		||||
								{{ getCurrentTrack().song.name }}
 | 
			
		||||
							</div>
 | 
			
		||||
							<div class="text-white/75 text-base truncate w-80">
 | 
			
		||||
								{{ artistsOrganize(getCurrentTrack().song.artists ?? []) }} —
 | 
			
		||||
								{{ getCurrentTrack().album?.name ?? '未知专辑' }}
 | 
			
		||||
							</div>
 | 
			
		||||
						</div>
 | 
			
		||||
 | 
			
		||||
					</div>
 | 
			
		||||
 | 
			
		||||
					<button class="h-10 w-10 flex justify-center items-center rounded-full bg-black/10 backdrop-blur-3xl">
 | 
			
		||||
						<span class="text-white">
 | 
			
		||||
							<StarEmptyIcon :size="6" />
 | 
			
		||||
						</span>
 | 
			
		||||
					</button>
 | 
			
		||||
				</div>
 | 
			
		||||
 | 
			
		||||
				<button class="h-10 w-10 flex justify-center items-center rounded-full bg-black/10 backdrop-blur-3xl">
 | 
			
		||||
					<span class="text-white">
 | 
			
		||||
						<StarEmptyIcon :size="6" />
 | 
			
		||||
					</span>
 | 
			
		||||
				</button>
 | 
			
		||||
			</div>
 | 
			
		||||
				<div class="flex flex-col gap-1">
 | 
			
		||||
					<div class="w-full p-[0.125rem] bg-white/20 shadow-[0_.125rem_1rem_0_#00000010] rounded-full backdrop-blur-3xl">
 | 
			
		||||
						<div class="w-full" ref="progressBarContainer">
 | 
			
		||||
							<div class="w-2 h-2 bg-white rounded-full shadow-md" ref="progressBarThumb" />
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
 | 
			
		||||
			<div class="flex flex-col gap-1">
 | 
			
		||||
				<div class="w-full p-[0.125rem] bg-white/20 shadow-[0_.125rem_1rem_0_#00000010] rounded-full backdrop-blur-3xl">
 | 
			
		||||
					<div class="w-full" ref="progressBarContainer">
 | 
			
		||||
						<div class="w-2 h-2 bg-white rounded-full shadow-md" ref="progressBarThumb" />
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
 | 
			
		||||
				<div class="w-full flex justify-between">
 | 
			
		||||
					<div class="font-medium flex-1 text-left relative">
 | 
			
		||||
						<span
 | 
			
		||||
							class="text-black blur-lg absolute top-0">{{ timeFormatter(Math.floor(playQueueStore.currentTime)) }}</span>
 | 
			
		||||
						<span class="text-white/90">{{ timeFormatter(Math.floor(playQueueStore.currentTime)) }}</span>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div class="text-xs text-center relative">
 | 
			
		||||
						<span class="text-black blur-lg absolute top-0">{{ formatDetector() }}</span>
 | 
			
		||||
						<span class="text-white">{{ formatDetector() }}</span>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div class="flex flex-1">
 | 
			
		||||
						<div class="flex-1" />
 | 
			
		||||
						<button class="text-white/90 font-medium text-right relative" @click="preferences.displayTimeLeft = !preferences.displayTimeLeft">
 | 
			
		||||
					<div class="w-full flex justify-between">
 | 
			
		||||
						<div class="font-medium flex-1 text-left relative">
 | 
			
		||||
							<span
 | 
			
		||||
								class="text-black blur-lg absolute top-0">{{ `${preferences.displayTimeLeft ? '-' : ''}${timeFormatter(preferences.displayTimeLeft ? Math.floor(playQueueStore.duration) - Math.floor(playQueueStore.currentTime) : playQueueStore.duration)}` }}</span>
 | 
			
		||||
							<span>{{ `${preferences.displayTimeLeft ? '-' : ''}${timeFormatter(preferences.displayTimeLeft ? Math.floor(playQueueStore.duration) - Math.floor(playQueueStore.currentTime) : playQueueStore.duration)}` }}</span>
 | 
			
		||||
								class="text-black blur-lg absolute top-0">{{ timeFormatter(Math.floor(playQueueStore.currentTime)) }}</span>
 | 
			
		||||
							<span class="text-white/90">{{ timeFormatter(Math.floor(playQueueStore.currentTime)) }}</span>
 | 
			
		||||
						</div>
 | 
			
		||||
						<div class="text-xs text-center relative">
 | 
			
		||||
							<span class="text-black blur-lg absolute top-0">{{ formatDetector() }}</span>
 | 
			
		||||
							<span class="text-white">{{ formatDetector() }}</span>
 | 
			
		||||
						</div>
 | 
			
		||||
						<div class="flex flex-1">
 | 
			
		||||
							<div class="flex-1" />
 | 
			
		||||
							<button class="text-white/90 font-medium text-right relative" @click="preferences.displayTimeLeft = !preferences.displayTimeLeft">
 | 
			
		||||
								<span
 | 
			
		||||
									class="text-black blur-lg absolute top-0">{{ `${preferences.displayTimeLeft ? '-' : ''}${timeFormatter(preferences.displayTimeLeft ? Math.floor(playQueueStore.duration) - Math.floor(playQueueStore.currentTime) : playQueueStore.duration)}` }}</span>
 | 
			
		||||
								<span>{{ `${preferences.displayTimeLeft ? '-' : ''}${timeFormatter(preferences.displayTimeLeft ? Math.floor(playQueueStore.duration) - Math.floor(playQueueStore.currentTime) : playQueueStore.duration)}` }}</span>
 | 
			
		||||
							</button>
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
				</div>
 | 
			
		||||
 | 
			
		||||
				<div class="w-full flex justify-between items-center">
 | 
			
		||||
					<div class="flex-1 text-left flex gap-1">
 | 
			
		||||
						<button class="h-8 w-8 flex justify-center items-center rounded-full hover:bg-white/25">
 | 
			
		||||
							<div class="w-6 h-6 relative">
 | 
			
		||||
								<span class="text-black blur-md absolute top-0 left-0">
 | 
			
		||||
									<SpeakerIcon :size="6" />
 | 
			
		||||
								</span>
 | 
			
		||||
								<span class="text-white">
 | 
			
		||||
									<SpeakerIcon :size="6" />
 | 
			
		||||
								</span>
 | 
			
		||||
							</div>
 | 
			
		||||
						</button>
 | 
			
		||||
						<button class="text-white h-8 w-8 flex justify-center items-center rounded-full hover:bg-white/25"
 | 
			
		||||
							@click="makePlayQueueListPresent">
 | 
			
		||||
							<div class="w-6 h-6 relative">
 | 
			
		||||
								<span class="text-black blur-md absolute top-0 left-0">
 | 
			
		||||
									<MusicListIcon :size="6" />
 | 
			
		||||
								</span>
 | 
			
		||||
								<span class="text-white">
 | 
			
		||||
									<MusicListIcon :size="6" />
 | 
			
		||||
								</span>
 | 
			
		||||
							</div>
 | 
			
		||||
						</button>
 | 
			
		||||
					</div>
 | 
			
		||||
 | 
			
		||||
					<div class="flex-2 text-center align-center justify-center gap-2 flex">
 | 
			
		||||
 | 
			
		||||
						<button class="text-white flex-1 h-10 flex justify-center items-center rounded-lg hover:bg-white/25"
 | 
			
		||||
							@click="playPrevious">
 | 
			
		||||
							<div class="w-8 h-8 relative">
 | 
			
		||||
								<span class="text-black/80 blur-lg absolute top-0 left-0">
 | 
			
		||||
									<RewindIcon :size="8" />
 | 
			
		||||
								</span>
 | 
			
		||||
								<span class="text-white">
 | 
			
		||||
									<RewindIcon :size="8" />
 | 
			
		||||
								</span>
 | 
			
		||||
							</div>
 | 
			
		||||
						</button>
 | 
			
		||||
 | 
			
		||||
						<button class="text-white flex-1 h-10 flex justify-center items-center rounded-lg hover:bg-white/25"
 | 
			
		||||
							@click="playQueueStore.isPlaying = !playQueueStore.isPlaying">
 | 
			
		||||
							<div v-if="playQueueStore.isPlaying">
 | 
			
		||||
								<div v-if="playQueueStore.isBuffering" class="w-6 h-6 relative">
 | 
			
		||||
									<span class="text-black/80 blur-lg absolute top-0 left-0">
 | 
			
		||||
										<LoadingIndicator :size="6" />
 | 
			
		||||
									</span>
 | 
			
		||||
									<span class="text-white">
 | 
			
		||||
										<LoadingIndicator :size="6" />
 | 
			
		||||
									</span>
 | 
			
		||||
								</div>
 | 
			
		||||
								<div v-else class="w-8 h-8 relative">
 | 
			
		||||
									<span class="text-black blur-md absolute top-0 left-0">
 | 
			
		||||
										<PauseIcon :size="8" />
 | 
			
		||||
									</span>
 | 
			
		||||
									<span class="text-white">
 | 
			
		||||
										<PauseIcon :size="8" />
 | 
			
		||||
									</span>
 | 
			
		||||
								</div>
 | 
			
		||||
							</div>
 | 
			
		||||
							<div v-else>
 | 
			
		||||
								<div class="w-8 h-8 relative">
 | 
			
		||||
									<span class="text-black/80 blur-lg absolute top-0 left-0">
 | 
			
		||||
										<PlayIcon :size="8" />
 | 
			
		||||
									</span>
 | 
			
		||||
									<span class="text-white">
 | 
			
		||||
										<PlayIcon :size="8" />
 | 
			
		||||
									</span>
 | 
			
		||||
								</div>
 | 
			
		||||
							</div>
 | 
			
		||||
						</button>
 | 
			
		||||
 | 
			
		||||
						<button class="text-white flex-1 h-10 flex justify-center items-center rounded-lg hover:bg-white/25"
 | 
			
		||||
							@click="playNext">
 | 
			
		||||
							<div class="w-8 h-8 relative">
 | 
			
		||||
								<span class="text-black/80 blur-lg absolute top-0 left-0">
 | 
			
		||||
									<ForwardIcon :size="8" />
 | 
			
		||||
								</span>
 | 
			
		||||
								<span class="text-white">
 | 
			
		||||
									<ForwardIcon :size="8" />
 | 
			
		||||
								</span>
 | 
			
		||||
							</div>
 | 
			
		||||
						</button>
 | 
			
		||||
					</div>
 | 
			
		||||
 | 
			
		||||
					<div class="flex-1 text-right flex gap-1">
 | 
			
		||||
						<div class="flex-1" />
 | 
			
		||||
						<button class="text-white h-8 w-8 flex justify-center items-center rounded-full hover:bg-white/25">
 | 
			
		||||
							<div class="w-6 h-6 relative">
 | 
			
		||||
								<span class="text-black blur-md absolute top-0 left-0">
 | 
			
		||||
									<ChatBubbleQuoteIcon :size="6" />
 | 
			
		||||
								</span>
 | 
			
		||||
								<span class="text-white">
 | 
			
		||||
									<ChatBubbleQuoteIcon :size="6" />
 | 
			
		||||
								</span>
 | 
			
		||||
							</div>
 | 
			
		||||
						</button>
 | 
			
		||||
						<button class="text-white h-8 w-8 flex justify-center items-center rounded-full hover:bg-white/25">
 | 
			
		||||
							<div class="w-6 h-6 relative">
 | 
			
		||||
								<span class="text-black blur-sm absolute top-0 left-0">
 | 
			
		||||
									<EllipsisHorizontalIcon :size="6" />
 | 
			
		||||
								</span>
 | 
			
		||||
								<span class="text-white">
 | 
			
		||||
									<EllipsisHorizontalIcon :size="6" />
 | 
			
		||||
								</span>
 | 
			
		||||
							</div>
 | 
			
		||||
						</button>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
			</div>
 | 
			
		||||
 | 
			
		||||
			<div class="w-full flex justify-between items-center">
 | 
			
		||||
				<div class="flex-1 text-left flex gap-1">
 | 
			
		||||
					<button class="h-8 w-8 flex justify-center items-center rounded-full hover:bg-white/25">
 | 
			
		||||
						<div class="w-6 h-6 relative">
 | 
			
		||||
							<span class="text-black blur-md absolute top-0 left-0">
 | 
			
		||||
								<SpeakerIcon :size="6" />
 | 
			
		||||
							</span>
 | 
			
		||||
							<span class="text-white">
 | 
			
		||||
								<SpeakerIcon :size="6" />
 | 
			
		||||
							</span>
 | 
			
		||||
						</div>
 | 
			
		||||
					</button>
 | 
			
		||||
					<button class="text-white h-8 w-8 flex justify-center items-center rounded-full hover:bg-white/25"
 | 
			
		||||
						@click="makePlayQueueListPresent">
 | 
			
		||||
						<div class="w-6 h-6 relative">
 | 
			
		||||
							<span class="text-black blur-md absolute top-0 left-0">
 | 
			
		||||
								<MusicListIcon :size="6" />
 | 
			
		||||
							</span>
 | 
			
		||||
							<span class="text-white">
 | 
			
		||||
								<MusicListIcon :size="6" />
 | 
			
		||||
							</span>
 | 
			
		||||
						</div>
 | 
			
		||||
					</button>
 | 
			
		||||
				</div>
 | 
			
		||||
 | 
			
		||||
				<div class="flex-2 text-center align-center justify-center gap-2 flex">
 | 
			
		||||
 | 
			
		||||
					<button class="text-white flex-1 h-10 flex justify-center items-center rounded-lg hover:bg-white/25"
 | 
			
		||||
						@click="playPrevious">
 | 
			
		||||
						<div class="w-8 h-8 relative">
 | 
			
		||||
							<span class="text-black/80 blur-lg absolute top-0 left-0">
 | 
			
		||||
								<RewindIcon :size="8" />
 | 
			
		||||
							</span>
 | 
			
		||||
							<span class="text-white">
 | 
			
		||||
								<RewindIcon :size="8" />
 | 
			
		||||
							</span>
 | 
			
		||||
						</div>
 | 
			
		||||
					</button>
 | 
			
		||||
 | 
			
		||||
					<button class="text-white flex-1 h-10 flex justify-center items-center rounded-lg hover:bg-white/25"
 | 
			
		||||
						@click="playQueueStore.isPlaying = !playQueueStore.isPlaying">
 | 
			
		||||
						<div v-if="playQueueStore.isPlaying">
 | 
			
		||||
							<div v-if="playQueueStore.isBuffering" class="w-6 h-6 relative">
 | 
			
		||||
								<span class="text-black/80 blur-lg absolute top-0 left-0">
 | 
			
		||||
									<LoadingIndicator :size="6" />
 | 
			
		||||
								</span>
 | 
			
		||||
								<span class="text-white">
 | 
			
		||||
									<LoadingIndicator :size="6" />
 | 
			
		||||
								</span>
 | 
			
		||||
							</div>
 | 
			
		||||
							<div v-else class="w-8 h-8 relative">
 | 
			
		||||
								<span class="text-black blur-md absolute top-0 left-0">
 | 
			
		||||
									<PauseIcon :size="8" />
 | 
			
		||||
								</span>
 | 
			
		||||
								<span class="text-white">
 | 
			
		||||
									<PauseIcon :size="8" />
 | 
			
		||||
								</span>
 | 
			
		||||
							</div>
 | 
			
		||||
						</div>
 | 
			
		||||
						<div v-else>
 | 
			
		||||
							<div class="w-8 h-8 relative">
 | 
			
		||||
								<span class="text-black/80 blur-lg absolute top-0 left-0">
 | 
			
		||||
									<PlayIcon :size="8" />
 | 
			
		||||
								</span>
 | 
			
		||||
								<span class="text-white">
 | 
			
		||||
									<PlayIcon :size="8" />
 | 
			
		||||
								</span>
 | 
			
		||||
							</div>
 | 
			
		||||
						</div>
 | 
			
		||||
					</button>
 | 
			
		||||
 | 
			
		||||
					<button class="text-white flex-1 h-10 flex justify-center items-center rounded-lg hover:bg-white/25"
 | 
			
		||||
						@click="playNext">
 | 
			
		||||
						<div class="w-8 h-8 relative">
 | 
			
		||||
							<span class="text-black/80 blur-lg absolute top-0 left-0">
 | 
			
		||||
								<ForwardIcon :size="8" />
 | 
			
		||||
							</span>
 | 
			
		||||
							<span class="text-white">
 | 
			
		||||
								<ForwardIcon :size="8" />
 | 
			
		||||
							</span>
 | 
			
		||||
						</div>
 | 
			
		||||
					</button>
 | 
			
		||||
				</div>
 | 
			
		||||
 | 
			
		||||
				<div class="flex-1 text-right flex gap-1">
 | 
			
		||||
					<div class="flex-1" />
 | 
			
		||||
					<button class="text-white h-8 w-8 flex justify-center items-center rounded-full hover:bg-white/25">
 | 
			
		||||
						<div class="w-6 h-6 relative">
 | 
			
		||||
							<span class="text-black blur-md absolute top-0 left-0">
 | 
			
		||||
								<ChatBubbleQuoteIcon :size="6" />
 | 
			
		||||
							</span>
 | 
			
		||||
							<span class="text-white">
 | 
			
		||||
								<ChatBubbleQuoteIcon :size="6" />
 | 
			
		||||
							</span>
 | 
			
		||||
						</div>
 | 
			
		||||
					</button>
 | 
			
		||||
					<button class="text-white h-8 w-8 flex justify-center items-center rounded-full hover:bg-white/25">
 | 
			
		||||
						<div class="w-6 h-6 relative">
 | 
			
		||||
							<span class="text-black blur-sm absolute top-0 left-0">
 | 
			
		||||
								<EllipsisHorizontalIcon :size="6" />
 | 
			
		||||
							</span>
 | 
			
		||||
							<span class="text-white">
 | 
			
		||||
								<EllipsisHorizontalIcon :size="6" />
 | 
			
		||||
							</span>
 | 
			
		||||
						</div>
 | 
			
		||||
					</button>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
		
 | 
			
		||||
		<div>
 | 
			
		||||
			<ScrollingLyrics :lrcSrc="getCurrentTrack().song.lyricUrl ?? undefined" />
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<!-- Queue list -->
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										14
									
								
								src/vite-env.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										14
									
								
								src/vite-env.d.ts
									
									
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -38,4 +38,18 @@ interface ApiResponse {
 | 
			
		|||
interface QueueItem {
 | 
			
		||||
  song: Song
 | 
			
		||||
  album?: Album
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface LyricsLine {
 | 
			
		||||
  type: 'lyric'
 | 
			
		||||
  time: number
 | 
			
		||||
  text: string
 | 
			
		||||
  originalTime: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface GapLine {
 | 
			
		||||
  type: 'gap'
 | 
			
		||||
  time: number
 | 
			
		||||
  originalTime: string
 | 
			
		||||
  duration?: number // 添加间隔持续时间
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user