fix: update language attribute in HTML and adjust script source path for consistency
refactor(ScrollingLyrics): simplify markup and improve readability of lyrics rendering logic
This commit is contained in:
		
							parent
							
								
									5a200edd11
								
							
						
					
					
						commit
						d8d1284c56
					
				
							
								
								
									
										21
									
								
								src/App.vue
									
									
									
									
									
								
							
							
						
						
									
										21
									
								
								src/App.vue
									
									
									
									
									
								
							| 
						 | 
				
			
			@ -14,39 +14,46 @@ const route = useRoute()
 | 
			
		|||
		<div class="flex flex-col w-full h-full overflow-y-auto pb-24">
 | 
			
		||||
			<div class="py-8 px-4 md:px-8 w-screen bg-gradient-to-b from-[#00000080] to-transparent z-20 absolute top-0">
 | 
			
		||||
				<div class="flex justify-between align-center h-[2.625rem] items-center">
 | 
			
		||||
					<ul class="flex gap-4"  v-if="(() => {
 | 
			
		||||
					<ul class="flex gap-4" v-if="(() => {
 | 
			
		||||
						if (route.path === '/lucky' || route.path === '/library' || route.path === '/') { return true }
 | 
			
		||||
						else { return false }
 | 
			
		||||
					})()">
 | 
			
		||||
						<li>
 | 
			
		||||
							<RouterLink to="/">
 | 
			
		||||
								<span class="text-4xl" :class="route.path === '/' ? 'font-semibold text-white' : 'text-white/50 hover:text-white/80'">浏览</span>
 | 
			
		||||
								<span class="text-4xl"
 | 
			
		||||
									:class="route.path === '/' ? 'font-semibold text-white' : 'text-white/50 hover:text-white/80'">浏览</span>
 | 
			
		||||
							</RouterLink>
 | 
			
		||||
						</li>
 | 
			
		||||
						<li>
 | 
			
		||||
							<RouterLink to="/lucky" :class="route.path === '/lucky'? 'font-semibold text-white' : 'text-white/50 hover:text-white/80'">
 | 
			
		||||
							<RouterLink to="/lucky"
 | 
			
		||||
								:class="route.path === '/lucky' ? 'font-semibold text-white' : 'text-white/50 hover:text-white/80'">
 | 
			
		||||
								<span class="text-4xl">手气不错</span>
 | 
			
		||||
							</RouterLink>
 | 
			
		||||
						</li>
 | 
			
		||||
						<li>
 | 
			
		||||
							<RouterLink to="/library" :class="route.path === '/library'? 'font-semibold text-white' : 'text-white/50 hover:text-white/80'">
 | 
			
		||||
							<RouterLink to="/library"
 | 
			
		||||
								:class="route.path === '/library' ? 'font-semibold text-white' : 'text-white/50 hover:text-white/80'">
 | 
			
		||||
								<span class="text-4xl">收藏库</span>
 | 
			
		||||
							</RouterLink>
 | 
			
		||||
						</li>
 | 
			
		||||
					</ul>
 | 
			
		||||
 | 
			
		||||
					<div v-else>
 | 
			
		||||
						<button class="text-white w-9 h-9 bg-white/5 border border-[#ffffff39] rounded-full text-center backdrop-blur-3xl flex justify-center items-center" @click="$router.back()">
 | 
			
		||||
						<button
 | 
			
		||||
							class="text-white w-9 h-9 bg-white/5 border border-[#ffffff39] rounded-full text-center backdrop-blur-3xl flex justify-center items-center"
 | 
			
		||||
							@click="$router.back()">
 | 
			
		||||
							<LeftArrowIcon :size="4" />
 | 
			
		||||
						</button>
 | 
			
		||||
					</div>
 | 
			
		||||
 | 
			
		||||
					<div class="flex gap-2">
 | 
			
		||||
						<button class="text-white w-9 h-9 bg-neutral-800/80 border border-[#ffffff39] rounded-full text-center backdrop-blur-3xl flex justify-center items-center">
 | 
			
		||||
						<button
 | 
			
		||||
							class="text-white w-9 h-9 bg-neutral-800/80 border border-[#ffffff39] rounded-full text-center backdrop-blur-3xl flex justify-center items-center">
 | 
			
		||||
							<SearchIcon :size="4" />
 | 
			
		||||
						</button>
 | 
			
		||||
 | 
			
		||||
						<button class="text-white w-9 h-9 bg-neutral-800/80 border border-[#ffffff39] rounded-full text-center backdrop-blur-3xl flex justify-center items-center">
 | 
			
		||||
						<button
 | 
			
		||||
							class="text-white w-9 h-9 bg-neutral-800/80 border border-[#ffffff39] rounded-full text-center backdrop-blur-3xl flex justify-center items-center">
 | 
			
		||||
							<CorgIcon :size="4" />
 | 
			
		||||
						</button>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,102 +1,73 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <div 
 | 
			
		||||
    class="relative overflow-hidden h-full w-[40rem]"
 | 
			
		||||
    ref="lyricsContainer"
 | 
			
		||||
    @wheel="handleWheel"
 | 
			
		||||
  >
 | 
			
		||||
  <div class="relative overflow-hidden h-full w-[40rem]" ref="lyricsContainer" @wheel="handleWheel">
 | 
			
		||||
    <!-- 歌词滚动区域 -->
 | 
			
		||||
    <div
 | 
			
		||||
      class="relative"
 | 
			
		||||
      ref="lyricsWrapper"
 | 
			
		||||
    >
 | 
			
		||||
    <div class="relative" ref="lyricsWrapper">
 | 
			
		||||
      <!-- 顶部填充 -->
 | 
			
		||||
      <div class="h-1/2 pointer-events-none"></div>
 | 
			
		||||
      
 | 
			
		||||
      <div 
 | 
			
		||||
        v-for="(line, index) in parsedLyrics" 
 | 
			
		||||
        :key="index"
 | 
			
		||||
        :ref="el => setLineRef(el as HTMLElement, index)"
 | 
			
		||||
 | 
			
		||||
      <div v-for="(line, index) in parsedLyrics" :key="index" :ref="el => setLineRef(el as HTMLElement, index)"
 | 
			
		||||
        class="py-8 px-16 cursor-pointer transition-all duration-300 hover:scale-105"
 | 
			
		||||
        @click="handleLineClick(line, index)"
 | 
			
		||||
      >
 | 
			
		||||
        @click="handleLineClick(line, index)">
 | 
			
		||||
        <div v-if="line.type === 'lyric'" class="relative">
 | 
			
		||||
          <!-- 背景模糊文字 -->
 | 
			
		||||
          <div 
 | 
			
		||||
            class="text-3xl font-bold transition-all duration-500"
 | 
			
		||||
            :class="[
 | 
			
		||||
              currentLineIndex === index ? 'text-black/80 blur-xl' : 'text-black/20 blur-2xl'
 | 
			
		||||
            ]"
 | 
			
		||||
          >
 | 
			
		||||
          <div class="text-3xl font-bold transition-all duration-500" :class="[
 | 
			
		||||
            currentLineIndex === index ? 'text-black/80 blur-xl' : 'text-black/20 blur-2xl'
 | 
			
		||||
          ]">
 | 
			
		||||
            {{ line.text }}
 | 
			
		||||
          </div>
 | 
			
		||||
          <!-- 前景清晰文字 -->
 | 
			
		||||
          <div 
 | 
			
		||||
            class="absolute top-0 left-0 w-full text-3xl font-bold transition-all duration-500"
 | 
			
		||||
            :class="[
 | 
			
		||||
              currentLineIndex === index 
 | 
			
		||||
                ? 'text-white scale-110' 
 | 
			
		||||
                : index < currentLineIndex 
 | 
			
		||||
                  ? userScrolling ? 'text-white/60' : 'text-white/60 blur-sm'
 | 
			
		||||
                  : userScrolling ? 'text-white/40' : 'text-white/40 blur-sm'
 | 
			
		||||
            ]"
 | 
			
		||||
          >
 | 
			
		||||
          <div class="absolute top-0 left-0 w-full text-3xl font-bold transition-all duration-500" :class="[
 | 
			
		||||
            currentLineIndex === index
 | 
			
		||||
              ? 'text-white scale-110'
 | 
			
		||||
              : index < currentLineIndex
 | 
			
		||||
                ? userScrolling ? 'text-white/60' : 'text-white/60 blur-sm'
 | 
			
		||||
                : userScrolling ? 'text-white/40' : 'text-white/40 blur-sm'
 | 
			
		||||
          ]">
 | 
			
		||||
            {{ line.text }}
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        
 | 
			
		||||
        <div v-else-if="line.type === 'gap'" class="flex justify-center items-center py-4">
 | 
			
		||||
          <div class="w-16 h-px rounded-full"></div>
 | 
			
		||||
 | 
			
		||||
        <div v-else-if="line.type === 'gap'" class="flex items-center gap-2">
 | 
			
		||||
          <div v-for="dot in 3" :key="dot" class="bg-white rounded-full"
 | 
			
		||||
            :class="currentLineIndex === index ? 'w-4 h-4' : ''"
 | 
			
		||||
            :style="{ opacity: getGapDotOpacities(line)[dot - 1] }" />
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      
 | 
			
		||||
 | 
			
		||||
      <!-- 底部填充 -->
 | 
			
		||||
      <div class="h-96 pointer-events-none"></div>
 | 
			
		||||
    </div>
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    <!-- 歌词控制面板 -->
 | 
			
		||||
    <div 
 | 
			
		||||
      class="absolute top-4 right-4 flex gap-2 opacity-0 transition-opacity duration-300 hover:opacity-100"
 | 
			
		||||
      ref="controlPanel"
 | 
			
		||||
    >
 | 
			
		||||
      <button
 | 
			
		||||
        @click="toggleAutoScroll"
 | 
			
		||||
    <div class="absolute top-4 right-4 flex gap-2 opacity-0 transition-opacity duration-300 hover:opacity-100"
 | 
			
		||||
      ref="controlPanel">
 | 
			
		||||
      <button @click="toggleAutoScroll"
 | 
			
		||||
        class="px-3 py-1 rounded-full text-xs backdrop-blur-md text-white/80 hover:text-white transition-all duration-200"
 | 
			
		||||
        :class="[
 | 
			
		||||
          autoScroll 
 | 
			
		||||
            ? 'bg-white/20 shadow-lg' 
 | 
			
		||||
          autoScroll
 | 
			
		||||
            ? 'bg-white/20 shadow-lg'
 | 
			
		||||
            : 'bg-black/20 hover:bg-black/30'
 | 
			
		||||
        ]"
 | 
			
		||||
      >
 | 
			
		||||
        ]">
 | 
			
		||||
        {{ autoScroll ? '自动' : '手动' }}
 | 
			
		||||
      </button>
 | 
			
		||||
      <button
 | 
			
		||||
        @click="resetScroll"
 | 
			
		||||
        class="px-3 py-1 rounded-full text-xs bg-black/20 backdrop-blur-md text-white/80 hover:bg-black/30 hover:text-white transition-all duration-200"
 | 
			
		||||
      >
 | 
			
		||||
      <button @click="resetScroll"
 | 
			
		||||
        class="px-3 py-1 rounded-full text-xs bg-black/20 backdrop-blur-md text-white/80 hover:bg-black/30 hover:text-white transition-all duration-200">
 | 
			
		||||
        重置
 | 
			
		||||
      </button>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- 滚动指示器 -->
 | 
			
		||||
    <div 
 | 
			
		||||
      class="absolute right-2 top-1/4 bottom-1/4 w-1 bg-white/10 rounded-full overflow-hidden"
 | 
			
		||||
      v-if="parsedLyrics.length > 5"
 | 
			
		||||
    >
 | 
			
		||||
      <div 
 | 
			
		||||
        class="w-full bg-white/40 rounded-full transition-all duration-300"
 | 
			
		||||
        :style="{ 
 | 
			
		||||
          height: scrollIndicatorHeight + '%',
 | 
			
		||||
          transform: `translateY(${scrollIndicatorPosition}px)` 
 | 
			
		||||
        }"
 | 
			
		||||
      ></div>
 | 
			
		||||
    <div class="absolute right-2 top-1/4 bottom-1/4 w-1 bg-white/10 rounded-full overflow-hidden"
 | 
			
		||||
      v-if="parsedLyrics.length > 5">
 | 
			
		||||
      <div class="w-full bg-white/40 rounded-full transition-all duration-300" :style="{
 | 
			
		||||
        height: scrollIndicatorHeight + '%',
 | 
			
		||||
        transform: `translateY(${scrollIndicatorPosition}px)`
 | 
			
		||||
      }"></div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- 加载状态 -->
 | 
			
		||||
    <div 
 | 
			
		||||
      v-if="loading" 
 | 
			
		||||
      class="absolute inset-0 flex items-center justify-center backdrop-blur-sm"
 | 
			
		||||
      ref="loadingIndicator"
 | 
			
		||||
    >
 | 
			
		||||
    <div v-if="loading" class="absolute inset-0 flex items-center justify-center backdrop-blur-sm"
 | 
			
		||||
      ref="loadingIndicator">
 | 
			
		||||
      <div class="flex items-center gap-3 px-6 py-3 rounded-full bg-black/20 backdrop-blur-md">
 | 
			
		||||
        <div class="w-4 h-4 border-2 border-white/60 border-t-white rounded-full animate-spin"></div>
 | 
			
		||||
        <div class="text-white/80 text-sm font-medium">加载歌词中...</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -104,11 +75,8 @@
 | 
			
		|||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- 无歌词状态 -->
 | 
			
		||||
    <div 
 | 
			
		||||
      v-if="!loading && parsedLyrics.length === 0" 
 | 
			
		||||
      class="absolute inset-0 flex items-center justify-center"
 | 
			
		||||
      ref="noLyricsIndicator"
 | 
			
		||||
    >
 | 
			
		||||
    <div v-if="!loading && parsedLyrics.length === 0" class="absolute inset-0 flex items-center justify-center"
 | 
			
		||||
      ref="noLyricsIndicator">
 | 
			
		||||
      <div class="text-center">
 | 
			
		||||
        <div class="text-white/40 text-lg font-medium mb-2">暂无歌词</div>
 | 
			
		||||
        <div class="text-white/30 text-sm">享受纯音乐的美妙</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -196,25 +164,25 @@ function parseLyrics(lrcText: string, minGapDuration: number = 5): (LyricsLine |
 | 
			
		|||
      originalTime: '[00:00]'
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  const lines = lrcText.split('\n')
 | 
			
		||||
  const tempParsedLines: (LyricsLine | GapLine)[] = []
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  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',
 | 
			
		||||
| 
						 | 
				
			
			@ -231,29 +199,29 @@ function parseLyrics(lrcText: string, minGapDuration: number = 5): (LyricsLine |
 | 
			
		|||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  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)
 | 
			
		||||
  const sortedLines = finalLines.sort((a, b) => a.time - b.time)
 | 
			
		||||
  // 在最前面插入一个空行
 | 
			
		||||
| 
						 | 
				
			
			@ -285,24 +253,24 @@ function findCurrentLineIndex(time: number): number {
 | 
			
		|||
// 使用 GSAP 滚动到指定行
 | 
			
		||||
function scrollToLine(lineIndex: number, smooth = true) {
 | 
			
		||||
  if (!lyricsContainer.value || !lyricsWrapper.value || !lineRefs.value[lineIndex]) return
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  const container = lyricsContainer.value
 | 
			
		||||
  const wrapper = lyricsWrapper.value
 | 
			
		||||
  const lineElement = lineRefs.value[lineIndex]
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  const containerHeight = container.clientHeight
 | 
			
		||||
  const containerCenter = containerHeight / 2
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  // 计算目标位置
 | 
			
		||||
  const lineOffsetTop = lineElement.offsetTop
 | 
			
		||||
  const lineHeight = lineElement.clientHeight
 | 
			
		||||
  const targetY = containerCenter - lineOffsetTop - lineHeight / 2
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  // 停止之前的滚动动画
 | 
			
		||||
  if (scrollTween) {
 | 
			
		||||
    scrollTween.kill()
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  if (smooth) {
 | 
			
		||||
    // 使用 GSAP 平滑滚动
 | 
			
		||||
    scrollTween = gsap.to(wrapper, {
 | 
			
		||||
| 
						 | 
				
			
			@ -321,14 +289,14 @@ function scrollToLine(lineIndex: number, smooth = true) {
 | 
			
		|||
// 高亮当前行动画
 | 
			
		||||
function highlightCurrentLine(lineIndex: number) {
 | 
			
		||||
  if (!lineRefs.value[lineIndex]) return
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  const lineElement = lineRefs.value[lineIndex]
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  // 停止之前的高亮动画
 | 
			
		||||
  if (highlightTween) {
 | 
			
		||||
    highlightTween.kill()
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  // 重置所有行的样式
 | 
			
		||||
  lineRefs.value.forEach((el, index) => {
 | 
			
		||||
    if (el && index !== lineIndex) {
 | 
			
		||||
| 
						 | 
				
			
			@ -340,7 +308,7 @@ function highlightCurrentLine(lineIndex: number) {
 | 
			
		|||
      })
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  // 高亮当前行
 | 
			
		||||
  highlightTween = gsap.to(lineElement, {
 | 
			
		||||
    scale: 1.05,
 | 
			
		||||
| 
						 | 
				
			
			@ -356,7 +324,7 @@ function highlightCurrentLine(lineIndex: number) {
 | 
			
		|||
// 处理鼠标滚轮
 | 
			
		||||
function handleWheel(event: WheelEvent) {
 | 
			
		||||
  event.preventDefault()
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  if (!lyricsWrapper.value || !lyricsContainer.value) return
 | 
			
		||||
 | 
			
		||||
  userScrolling.value = true
 | 
			
		||||
| 
						 | 
				
			
			@ -403,18 +371,18 @@ function handleLineClick(line: LyricsLine | GapLine, index: number) {
 | 
			
		|||
    // 这里可以发出事件让父组件处理音频跳转
 | 
			
		||||
    // emit('seek', line.time)
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  // 滚动到点击的行
 | 
			
		||||
  scrollToLine(index, true)
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  // 添加点击反馈动画
 | 
			
		||||
  if (lineRefs.value[index]) {
 | 
			
		||||
    gsap.fromTo(lineRefs.value[index], 
 | 
			
		||||
    gsap.fromTo(lineRefs.value[index],
 | 
			
		||||
      { scale: 1 },
 | 
			
		||||
      { 
 | 
			
		||||
        scale: 1.1, 
 | 
			
		||||
        duration: 0.1, 
 | 
			
		||||
        yoyo: true, 
 | 
			
		||||
      {
 | 
			
		||||
        scale: 1.1,
 | 
			
		||||
        duration: 0.1,
 | 
			
		||||
        yoyo: true,
 | 
			
		||||
        repeat: 1,
 | 
			
		||||
        ease: "power2.inOut"
 | 
			
		||||
      }
 | 
			
		||||
| 
						 | 
				
			
			@ -426,21 +394,21 @@ function handleLineClick(line: LyricsLine | GapLine, index: number) {
 | 
			
		|||
function toggleAutoScroll() {
 | 
			
		||||
  autoScroll.value = !autoScroll.value
 | 
			
		||||
  userScrolling.value = false
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  // 按钮点击动画
 | 
			
		||||
  if (controlPanel.value) {
 | 
			
		||||
    gsap.fromTo(controlPanel.value.children[0],
 | 
			
		||||
      { scale: 1 },
 | 
			
		||||
      { 
 | 
			
		||||
        scale: 0.95, 
 | 
			
		||||
        duration: 0.1, 
 | 
			
		||||
        yoyo: true, 
 | 
			
		||||
      {
 | 
			
		||||
        scale: 0.95,
 | 
			
		||||
        duration: 0.1,
 | 
			
		||||
        yoyo: true,
 | 
			
		||||
        repeat: 1,
 | 
			
		||||
        ease: "power2.inOut"
 | 
			
		||||
      }
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  if (autoScroll.value && currentLineIndex.value >= 0) {
 | 
			
		||||
    nextTick(() => {
 | 
			
		||||
      scrollToLine(currentLineIndex.value, true)
 | 
			
		||||
| 
						 | 
				
			
			@ -451,35 +419,35 @@ function toggleAutoScroll() {
 | 
			
		|||
// 重置滚动
 | 
			
		||||
function resetScroll() {
 | 
			
		||||
  if (!lyricsWrapper.value) return
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  // 停止所有动画
 | 
			
		||||
  if (scrollTween) scrollTween.kill()
 | 
			
		||||
  if (highlightTween) highlightTween.kill()
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  // 重置位置
 | 
			
		||||
  gsap.to(lyricsWrapper.value, {
 | 
			
		||||
    y: 0,
 | 
			
		||||
    duration: 0.3,
 | 
			
		||||
    ease: "power2.out"
 | 
			
		||||
  })
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  autoScroll.value = true
 | 
			
		||||
  userScrolling.value = false
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  // 按钮点击动画
 | 
			
		||||
  if (controlPanel.value) {
 | 
			
		||||
    gsap.fromTo(controlPanel.value.children[1],
 | 
			
		||||
      { scale: 1 },
 | 
			
		||||
      { 
 | 
			
		||||
        scale: 0.95, 
 | 
			
		||||
        duration: 0.1, 
 | 
			
		||||
        yoyo: true, 
 | 
			
		||||
      {
 | 
			
		||||
        scale: 0.95,
 | 
			
		||||
        duration: 0.1,
 | 
			
		||||
        yoyo: true,
 | 
			
		||||
        repeat: 1,
 | 
			
		||||
        ease: "power2.inOut"
 | 
			
		||||
      }
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  if (currentLineIndex.value >= 0) {
 | 
			
		||||
    nextTick(() => {
 | 
			
		||||
      scrollToLine(currentLineIndex.value, true)
 | 
			
		||||
| 
						 | 
				
			
			@ -487,16 +455,34 @@ function resetScroll() {
 | 
			
		|||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// gap 圆点透明度计算
 | 
			
		||||
function getGapDotOpacities(line: GapLine) {
 | 
			
		||||
  // 获取 gap 的持续时间
 | 
			
		||||
  const duration = line.duration ?? 0
 | 
			
		||||
  if (duration <= 0) return [0.3, 0.3, 0.3]
 | 
			
		||||
  // 当前播放时间
 | 
			
		||||
  const now = playQueueStore.currentTime
 | 
			
		||||
  // gap 起止时间
 | 
			
		||||
  const start = line.time
 | 
			
		||||
  // 计算进度
 | 
			
		||||
  let progress = (now - start) / duration
 | 
			
		||||
  progress = Math.max(0, Math.min(1, progress))
 | 
			
		||||
  // 每个圆点的阈值
 | 
			
		||||
  const thresholds = [1 / 4, 2 / 4, 3 / 4]
 | 
			
		||||
  // 透明度从 0.3 到 1
 | 
			
		||||
  return thresholds.map(t => progress >= t ? 1 : progress >= t - 1 / 3 ? 0.6 : 0.3)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 监听播放时间变化
 | 
			
		||||
watch(() => playQueueStore.currentTime, (time) => {
 | 
			
		||||
  const newIndex = findCurrentLineIndex(time)
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  if (newIndex !== currentLineIndex.value && newIndex >= 0) {
 | 
			
		||||
    currentLineIndex.value = newIndex
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    // 高亮动画
 | 
			
		||||
    highlightCurrentLine(newIndex)
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    // 自动滚动
 | 
			
		||||
    if (autoScroll.value && !userScrolling.value) {
 | 
			
		||||
      nextTick(() => {
 | 
			
		||||
| 
						 | 
				
			
			@ -512,14 +498,14 @@ watch(() => props.lrcSrc, async (newSrc) => {
 | 
			
		|||
  // 重置状态
 | 
			
		||||
  currentLineIndex.value = -1
 | 
			
		||||
  lineRefs.value = []
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  // 停止所有动画
 | 
			
		||||
  if (scrollTween) scrollTween.kill()
 | 
			
		||||
  if (highlightTween) highlightTween.kill()
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  if (newSrc) {
 | 
			
		||||
    loading.value = true
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    // 加载动画
 | 
			
		||||
    if (loadingIndicator.value) {
 | 
			
		||||
      gsap.fromTo(loadingIndicator.value,
 | 
			
		||||
| 
						 | 
				
			
			@ -527,20 +513,20 @@ watch(() => props.lrcSrc, async (newSrc) => {
 | 
			
		|||
        { opacity: 1, scale: 1, duration: 0.3, ease: "back.out(1.7)" }
 | 
			
		||||
      )
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      const response = await axios.get(newSrc)
 | 
			
		||||
      parsedLyrics.value = parseLyrics(response.data)
 | 
			
		||||
      console.log('Parsed lyrics:', parsedLyrics.value)
 | 
			
		||||
      
 | 
			
		||||
 | 
			
		||||
      autoScroll.value = true
 | 
			
		||||
      userScrolling.value = false
 | 
			
		||||
      
 | 
			
		||||
 | 
			
		||||
      // 重置滚动位置
 | 
			
		||||
      if (lyricsWrapper.value) {
 | 
			
		||||
        gsap.set(lyricsWrapper.value, { y: 0 })
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('Failed to load lyrics:', error)
 | 
			
		||||
      parsedLyrics.value = []
 | 
			
		||||
| 
						 | 
				
			
			@ -549,7 +535,7 @@ watch(() => props.lrcSrc, async (newSrc) => {
 | 
			
		|||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    parsedLyrics.value = []
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    // 重置滚动位置
 | 
			
		||||
    if (lyricsWrapper.value) {
 | 
			
		||||
      gsap.set(lyricsWrapper.value, { y: 0 })
 | 
			
		||||
| 
						 | 
				
			
			@ -566,17 +552,17 @@ onMounted(() => {
 | 
			
		|||
      { opacity: 0, x: 0, duration: 0.2, ease: "power2.out", delay: 0.2 }
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  // 歌词行入场动画
 | 
			
		||||
  nextTick(() => {
 | 
			
		||||
    lineRefs.value.forEach((el, index) => {
 | 
			
		||||
      if (el) {
 | 
			
		||||
        gsap.fromTo(el,
 | 
			
		||||
          { opacity: 0, y: 30 },
 | 
			
		||||
          { 
 | 
			
		||||
            opacity: 1, 
 | 
			
		||||
            y: 0, 
 | 
			
		||||
            duration: 0.2, 
 | 
			
		||||
          {
 | 
			
		||||
            opacity: 1,
 | 
			
		||||
            y: 0,
 | 
			
		||||
            duration: 0.2,
 | 
			
		||||
            ease: "power2.out",
 | 
			
		||||
            delay: index * 0.1
 | 
			
		||||
          }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue
	
	Block a user