Files
Henry_Du 9fc5e3e607
构建并部署 BYW VoiceHub / 构建并推送容器镜像 (push) Canceled after 0s
构建并部署 BYW VoiceHub / 部署到生产服务器 (push) Canceled after 0s
构建并部署 BYW VoiceHub / Preflight 语法与构建检查 (push) Canceled after 1m27s
fix(ui): 批量替换所有组件中残留的硬编码深色值以彻底支持浅色主题
2026-09-12 15:56:10 +08:00

206 lines
5.7 KiB
Vue

<template>
<div
class="lyric-am"
:class="{
'align-right': settings.lyricAlignRight.value
}"
:style="{
'--amll-lp-color': 'rgb(var(--main-cover-color, 239 239 239))',
'--amll-lp-hover-bg-color': 'rgba(255,255,255,0.08)',
'--amll-lyric-left-padding': settings.lyricAlignRight.value
? ''
: `${settings.lyricHorizontalOffset.value}px`,
'--amll-lyric-right-padding': settings.lyricAlignRight.value
? `${settings.lyricHorizontalOffset.value}px`
: ''
}"
>
<Transition name="fade" mode="out-in">
<div v-if="lyricManager.loading.value" class="lyric-message-container">
<Icon name="loader" :size="32" class="spin-animation" />
<div class="message-text">歌词加载中...</div>
</div>
<div v-else-if="lyricManager.error.value" class="lyric-message-container">
<Icon name="alert-circle" :size="32" />
<div class="message-text">{{ lyricManager.error.value }}</div>
</div>
<div v-else-if="!lyricLines || lyricLines.length === 0" class="lyric-message-container">
<Icon name="music" :size="48" style="opacity: 0.5" />
<div class="message-text">暂无歌词</div>
</div>
<LyricPlayer
v-else
ref="lyricPlayerRef"
:lyric-lines="lyricLines"
:current-time="currentTime * 1000"
:playing="audioPlayer.getPlayingStatus().value"
:enable-spring="settings.useAMSpring.value"
:enable-scale="settings.useAMSpring.value"
:align-position="settings.lyricsScrollOffset.value"
:align-anchor="settings.lyricsScrollOffset.value > 0.4 ? 'center' : 'top'"
:enable-blur="settings.lyricsBlur.value"
:hide-passed-lines="settings.hidePassedLines.value"
:word-fade-width="settings.wordFadeWidth.value"
:style="{
'--display-count-down-show': settings.countDownShow.value ? 'flex' : 'none',
'--amll-lp-font-size': `${settings.lyricFontSize.value}px`,
'font-weight': settings.lyricFontWeight.value,
'font-family': settings.LyricFont.value !== 'follow' ? settings.LyricFont.value : ''
}"
class="am-lyric"
@line-click="jumpSeek"
/>
</Transition>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import LyricPlayer, { type LyricPlayerRef } from '~/components/AMLL/LyricPlayer.vue'
import Icon from '~/components/UI/Icon.vue'
import { useLyricManager } from '~/composables/useLyricManager'
import { useLyricSettings } from '~/composables/useLyricSettings'
import { useAudioPlayer } from '~/composables/useAudioPlayer'
import { useAudioPlayerControl } from '~/composables/useAudioPlayerControl'
import type { LyricLineMouseEvent } from '@applemusic-like-lyrics/core'
import { cloneDeep } from 'lodash-es'
const props = defineProps({
currentTime: {
type: Number,
default: 0
}
})
const lyricManager = useLyricManager()
const settings = useLyricSettings()
const audioPlayer = useAudioPlayer()
const audioPlayerControl = useAudioPlayerControl()
const lyricPlayerRef = ref<LyricPlayerRef | null>(null)
const lyricLines = computed(() => {
// 使用 cloneDeep 剥离 Vue 响应式代理,防止 structuredClone 错误
return cloneDeep(lyricManager.lyrics.value)
})
// 进度跳转
const jumpSeek = (line: LyricLineMouseEvent) => {
// 移除焦点,防止组件内部焦点导致键盘事件异常
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur()
}
const lineContent = line.line.getLine()
if (!lineContent?.startTime) return
const time = lineContent.startTime
const offsetMs = settings.lyricOffset.value
const targetTime = (time - offsetMs) / 1000
audioPlayerControl.seekAndPlay(targetTime)
}
</script>
<style scoped>
.lyric-am {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
isolation: isolate;
}
.am-lyric {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
padding-left: var(--amll-lyric-left-padding, 10px);
padding-right: 80px;
}
/* 移动端适配 */
@media (max-width: 990px) {
.am-lyric {
padding: 0;
margin-left: 0;
}
}
.lyric-am.align-right .am-lyric {
padding-left: 80px;
padding-right: var(--amll-lyric-right-padding, 10px);
}
@media (max-width: 990px) {
.lyric-am.align-right .am-lyric {
padding: 0;
margin-right: -20px;
}
}
/* 高亮效果 */
:deep(.am-lyric .current),
:deep(.am-lyric .is-current),
:deep(.am-lyric .active),
:deep(.am-lyric .is-active),
:deep(.am-lyric .lyric-line.current),
:deep(.am-lyric .lyric-line.is-current) {
mix-blend-mode: plus-lighter;
color: rgba(255, 255, 255, 0.95);
text-shadow: 0 2px 12px rgba(255, 255, 255, 0.06);
will-change: transform, opacity, color;
}
@supports not (mix-blend-mode: plus-lighter) {
:deep(.am-lyric .current),
:deep(.am-lyric .is-current),
:deep(.am-lyric .active),
:deep(.am-lyric .is-active),
:deep(.am-lyric .lyric-line.current),
:deep(.am-lyric .lyric-line.is-current) {
color: var(--text-primary, #ffffff);
text-shadow: 0 4px 18px rgba(0, 0, 0, 0.35);
}
}
:deep(.am-lyric div[class*='lyricMainLine'] span) {
text-align: start;
}
.lyric-message-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: var(--amll-lp-color, #efefef);
gap: 16px;
opacity: 0.8;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
}
.message-text {
font-size: 18px;
font-weight: 500;
letter-spacing: 0.5px;
opacity: 0.8;
}
.spin-animation {
animation: spin 1s linear infinite;
opacity: 0.9;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
</style>