| <template> |
| <div class="desktop-full-player"> |
| |
| <button class="floating-back-btn" @click="handleBack"> |
| <i class="fas fa-arrow-left"></i> |
| </button> |
| |
| |
| <main class="player-main"> |
| |
| <section class="left-panel"> |
| |
| <button class="panel-more-btn" @click="showMoreActions = true" v-if="currentSong"> |
| <i class="fas fa-ellipsis-h"></i> |
| </button> |
| |
| |
| <div class="album-cover-section"> |
| <div class="album-cover-wrapper"> |
| <img |
| :src="currentCover" |
| :alt="currentSong?.name" |
| class="album-cover" |
| :class="{ rotating: isPlaying }" |
| @error="handleImageError" |
| /> |
| |
| <div class="cd-center"> |
| <div class="center-dot"></div> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div class="song-info-section"> |
| <div class="song-title-row"> |
| <h2 class="song-title">{{ currentSong?.name || '暂无播放' }}</h2> |
| <button |
| class="action-button favorite-btn" |
| :class="{ active: isFavorite }" |
| @click="toggleFavorite" |
| v-if="currentSong" |
| > |
| <i :class="isFavorite ? 'fas fa-heart' : 'far fa-heart'"></i> |
| </button> |
| </div> |
| <p class="artist-name">{{ formattedArtist }}</p> |
| <p class="album-name" v-if="currentSong?.album">{{ currentSong.album }}</p> |
| </div> |
| |
| |
| <div class="progress-section"> |
| <ProgressBar @seek="handleSeek"/> |
| </div> |
| |
| |
| <div class="controls-section"> |
| <PlayControls |
| :loading="loading" |
| @togglePlay="handleTogglePlay" |
| @previous="handlePrevious" |
| @next="handleNext" |
| @togglePlayMode="handleTogglePlayMode" |
| @showPlaylist="handleShowPlaylist" |
| /> |
| </div> |
| </section> |
| |
| |
| <section class="right-panel"> |
| <div class="lyrics-content"> |
| <div class="lyrics-scroll-container"> |
| <LyricsView |
| :lyrics="currentLyrics" |
| :currentTime="currentTime" |
| @seekTo="handleSeekTo" |
| /> |
| </div> |
| </div> |
| </section> |
| </main> |
| |
| |
| <div class="quality-selector" v-if="showQualitySelector" @click.self="showQualitySelector = false"> |
| <div class="quality-panel" @click.stop> |
| <div class="panel-header"> |
| <h3>音质选择</h3> |
| <button class="close-btn" @click.stop="showQualitySelector = false"> |
| <i class="fas fa-times"></i> |
| </button> |
| </div> |
| |
| <div class="quality-list"> |
| <div |
| v-for="quality in availableQualities" |
| :key="quality.value" |
| class="quality-item" |
| :class="{ |
| 'active': quality.value === currentQuality, |
| 'loading': qualityLoading && selectedQuality === quality.value |
| }" |
| @click.stop="selectQuality(quality.value)" |
| > |
| <div class="quality-main"> |
| <div class="quality-name">{{ quality.name }}</div> |
| <div class="quality-desc">{{ quality.description }}</div> |
| </div> |
| |
| <div class="quality-actions"> |
| <div class="quality-size" v-if="quality.size">{{ quality.size }}</div> |
| <i v-if="quality.value === currentQuality" class="fas fa-check quality-check"></i> |
| <i v-else-if="qualityLoading && selectedQuality === quality.value" class="fas fa-spinner fa-spin"></i> |
| </div> |
| </div> |
| </div> |
| |
| <div class="quality-note"> |
| <i class="fas fa-info-circle"></i> |
| <span>更高音质需要更多流量,请在WiFi环境下使用</span> |
| </div> |
| </div> |
| </div> |
| |
| |
| <PlaylistPanel |
| v-if="showPlaylist" |
| @close="showPlaylist = false" |
| @play="handlePlayFromList" |
| @remove="handleRemoveFromList" |
| /> |
| |
| |
| <MoreActionsPanel |
| v-if="showMoreActions" |
| :song="currentSong" |
| @close="showMoreActions = false" |
| @action="handleMoreAction" |
| /> |
| </div> |
| </template> |
| |
| <script setup> |
| import {ref, computed, onMounted, onUnmounted, watch} from 'vue' |
| import {useRouter} from 'vue-router' |
| import {usePlayerStore} from '@/stores/player' |
| import {usePlayQueueStore} from '@/stores/playqueue' |
| import {useFavoritesStore} from '@/stores/favorites' |
| import {useHistoryStore} from '@/stores/history' |
| import {useToastStore} from '@/stores/toast' |
| import {useSettingsStore} from '@/stores/settings' |
| import {musicApi, utils} from '@/services/musicApi' |
| import ProgressBar from '@/components/player/ProgressBar.vue' |
| import PlayControls from '@/components/player/PlayControls.vue' |
| import LyricsView from '@/components/player/LyricsView.vue' |
| import PlaylistPanel from '@/components/player/PlaylistPanel.vue' |
| import MoreActionsPanel from '@/components/player/MoreActionsPanel.vue' |
| |
| |
| const emit = defineEmits(['close']) |
| |
| const router = useRouter() |
| const playerStore = usePlayerStore() |
| const playQueueStore = usePlayQueueStore() |
| const favoritesStore = useFavoritesStore() |
| const historyStore = useHistoryStore() |
| const toastStore = useToastStore() |
| const settingsStore = useSettingsStore() |
| |
| |
| const loading = ref(false) |
| const showPlaylist = ref(false) |
| const showMoreActions = ref(false) |
| const showQualitySelector = ref(false) |
| const currentLyrics = ref([]) |
| const qualityLoading = ref(false) |
| const selectedQuality = ref(null) |
| const currentCoverUrl = ref('') |
| |
| |
| const availableQualities = ref([ |
| { |
| value: 128, |
| name: '标准', |
| description: '128K 省流模式', |
| size: '约3MB/首' |
| }, |
| { |
| value: 192, |
| name: '较高', |
| description: '192K 均衡模式', |
| size: '约4MB/首' |
| }, |
| { |
| value: 320, |
| name: 'HQ', |
| description: '320K 高品质', |
| size: '约7MB/首' |
| }, |
| { |
| value: 740, |
| name: '无损', |
| description: 'FLAC 原音质', |
| size: '约30MB/首' |
| }, |
| { |
| value: 999, |
| name: 'Hi-Res', |
| description: '高解析度音频', |
| size: '约60MB/首' |
| } |
| ]) |
| |
| |
| const currentSong = computed(() => playerStore.currentSong) |
| const isPlaying = computed(() => playerStore.isPlaying) |
| const currentTime = computed(() => playerStore.currentTime) |
| const currentQuality = computed(() => settingsStore.settings.defaultQuality || 320) |
| |
| const isFavorite = computed(() => { |
| return currentSong.value ? favoritesStore.isFavorite(currentSong.value) : false |
| }) |
| |
| const formattedArtist = computed(() => { |
| if (!currentSong.value?.artist) return '' |
| return utils.formatArtist(currentSong.value.artist) |
| }) |
| |
| const currentCover = computed(() => { |
| |
| if (currentCoverUrl.value) { |
| return currentCoverUrl.value |
| } |
| |
| if (!currentSong.value) return defaultCover.value |
| |
| |
| const cachedCover = playerStore.getCachedCover(currentSong.value, 300) |
| if (cachedCover) { |
| return cachedCover |
| } |
| |
| return defaultCover.value |
| }) |
| |
| const defaultCover = computed(() => { |
| return 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjgwIiBoZWlnaHQ9IjI4MCIgdmlld0JveD0iMCAwIDI4MCAyODAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyODAiIGhlaWdodD0iMjgwIiBmaWxsPSJyZ2JhKDIwMCwyMDAsMjAwLDAuMykiIHJ4PSI0MCIvPgo8Y2lyY2xlIGN4PSIxNDAiIGN5PSIxNDAiIHI9IjgwIiBmaWxsPSJyZ2JhKDI1NSwyNTUsMjU1LDAuMSkiLz4KPHN2ZyB4PSIxMTAiIHk9IjExMCIgd2lkdGg9IjYwIiBoZWlnaHQ9IjYwIiB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9InJnYmEoMTYwLDE2MCwxNjAsMC42KSI+CjxwYXRoIGQ9Ik0xMiAzdjEwLjU1Yy0uNTktLjM0LTEuMjctLjU1LTItLjU1QzcuNzkgMTMgNiAxNC43OSA2IDE3czEuNzkgNCA0IDRDMS45NCAwIDMuNS0xLjI5IDMuOTEtM0gxNFYzeiIvPgo8L3N2Zz4KPC9zdmc+' |
| }) |
| |
| |
| const loadAlbumCover = async () => { |
| if (!currentSong.value) { |
| currentCoverUrl.value = defaultCover.value |
| return |
| } |
| |
| try { |
| |
| const cachedCover = playerStore.getCachedCover(currentSong.value, 300) |
| if (cachedCover) { |
| currentCoverUrl.value = cachedCover |
| return |
| } |
| |
| const coverUrlResult = await playerStore.getAlbumCover(currentSong.value, 300) |
| if (coverUrlResult) { |
| currentCoverUrl.value = coverUrlResult |
| } else { |
| currentCoverUrl.value = defaultCover.value |
| } |
| } catch (error) { |
| console.error('加载专辑封面失败:', error) |
| currentCoverUrl.value = defaultCover.value |
| } |
| } |
| |
| |
| const handleImageError = () => { |
| currentCoverUrl.value = defaultCover.value |
| } |
| |
| |
| const handleBack = () => { |
| emit('close') |
| } |
| |
| const handleSeek = (time) => { |
| playerStore.seekTo(time) |
| } |
| |
| const handleSeekTo = (time) => { |
| playerStore.seekTo(time) |
| } |
| |
| const handleTogglePlay = async () => { |
| const audioElement = playerStore.audioElement || document.querySelector('audio') |
| |
| if (!audioElement || !currentSong.value) { |
| return |
| } |
| |
| try { |
| if (isPlaying.value) { |
| audioElement.pause() |
| playerStore.setPlayingState(false) |
| } else { |
| if (!playerStore.audioSrc || !audioElement.src || audioElement.src === location.href) { |
| window.dispatchEvent(new CustomEvent('loadAndPlaySong', { |
| detail: {song: currentSong.value} |
| })) |
| return |
| } |
| |
| if (audioElement.src !== playerStore.audioSrc) { |
| audioElement.src = playerStore.audioSrc |
| audioElement.load() |
| } |
| |
| await audioElement.play() |
| playerStore.setPlayingState(true) |
| } |
| } catch (error) { |
| console.error('播放失败:', error) |
| } |
| } |
| |
| const handlePrevious = () => { |
| const result = playQueueStore.playPrevious() |
| if (result) { |
| |
| playerStore.setCurrentTime(0) |
| playerStore.playSong(result) |
| } |
| } |
| |
| const handleNext = () => { |
| const result = playQueueStore.playNext() |
| if (result) { |
| |
| playerStore.setCurrentTime(0) |
| playerStore.playSong(result) |
| } |
| } |
| |
| const handleTogglePlayMode = () => { |
| playQueueStore.togglePlayMode() |
| } |
| |
| const handleShowPlaylist = () => { |
| showPlaylist.value = true |
| } |
| |
| const handlePlayFromList = async (song, index) => { |
| try { |
| const selectedSong = playQueueStore.playAtIndex(index) |
| if (selectedSong) { |
| await playerStore.playSong(selectedSong, true) |
| historyStore.addToHistory(selectedSong) |
| toastStore.success(`开始播放 "${selectedSong.name}"`) |
| } |
| showPlaylist.value = false |
| } catch (error) { |
| console.error('播放失败:', error) |
| toastStore.error('播放失败,请重试') |
| } |
| } |
| |
| const handleRemoveFromList = (index) => { |
| try { |
| const result = playQueueStore.removeFromQueue(index) |
| if (result.success) { |
| toastStore.success('已从播放列表移除') |
| } else { |
| toastStore.error(result.message) |
| } |
| } catch (error) { |
| console.error('移除失败:', error) |
| toastStore.error('操作失败,请重试') |
| } |
| } |
| |
| const toggleFavorite = async () => { |
| if (!currentSong.value) return |
| |
| try { |
| const result = await favoritesStore.toggleFavorite(currentSong.value) |
| const message = result ? '已添加到我喜欢的音乐' : '已从我喜欢的音乐中移除' |
| toastStore.success(message) |
| } catch (error) { |
| console.error('收藏操作失败:', error) |
| toastStore.error('操作失败,请重试') |
| } |
| } |
| |
| |
| const handleMoreAction = (action) => { |
| switch (action) { |
| case 'addToPlaylist': |
| |
| break |
| case 'viewAlbum': |
| |
| break |
| case 'viewArtist': |
| |
| break |
| case 'selectQuality': |
| showQualitySelector.value = true |
| break |
| case 'report': |
| |
| break |
| } |
| showMoreActions.value = false |
| } |
| |
| const selectQuality = async (quality) => { |
| if (quality === currentQuality.value || qualityLoading.value) return |
| |
| selectedQuality.value = quality |
| qualityLoading.value = true |
| |
| try { |
| await settingsStore.updateSetting('defaultQuality', quality) |
| |
| if (currentSong.value && isPlaying.value) { |
| const currentTime = playerStore.currentTime |
| await playerStore.changeQuality(quality) |
| |
| if (currentTime > 0) { |
| setTimeout(() => { |
| playerStore.seekTo(currentTime) |
| }, 500) |
| } |
| } |
| |
| showQualitySelector.value = false |
| |
| } catch (error) { |
| console.error('切换音质失败:', error) |
| } finally { |
| qualityLoading.value = false |
| selectedQuality.value = null |
| } |
| } |
| |
| |
| |
| const getCurrentLyric = () => { |
| if (!currentLyrics.value || currentLyrics.value.length === 0) { |
| return '' |
| } |
| |
| const currentTimeSeconds = currentTime.value |
| let currentLyric = '' |
| |
| for (let i = 0; i < currentLyrics.value.length; i++) { |
| const lyric = currentLyrics.value[i] |
| if (lyric.time <= currentTimeSeconds) { |
| currentLyric = lyric.text |
| } else { |
| break |
| } |
| } |
| |
| return currentLyric |
| } |
| |
| |
| const loadLyrics = async (song) => { |
| if (!song) { |
| currentLyrics.value = [] |
| return |
| } |
| |
| const cachedLyrics = playerStore.getCachedLyrics(song) |
| if (cachedLyrics) { |
| const parsedLyrics = musicApi.parseLyrics(cachedLyrics.lyric) |
| currentLyrics.value = parsedLyrics.lyrics |
| return |
| } |
| |
| try { |
| loading.value = true |
| const lyricsData = await playerStore.getLyricsWithDedup(song) |
| |
| const parsedLyrics = musicApi.parseLyrics(lyricsData.lyric) |
| currentLyrics.value = parsedLyrics.lyrics |
| } catch (error) { |
| console.error('加载歌词失败:', error) |
| currentLyrics.value = [] |
| } finally { |
| loading.value = false |
| } |
| } |
| |
| |
| let lyricsTimer = null |
| |
| |
| watch(currentSong, (newSong, oldSong) => { |
| if (newSong && newSong !== oldSong) { |
| if (lyricsTimer) { |
| clearTimeout(lyricsTimer) |
| lyricsTimer = null |
| } |
| |
| loadAlbumCover() |
| |
| lyricsTimer = setTimeout(() => { |
| loadLyrics(newSong) |
| lyricsTimer = null |
| }, 200) |
| } |
| }, {immediate: true}) |
| |
| |
| onMounted(() => { |
| if (currentSong.value) { |
| loadAlbumCover() |
| loadLyrics(currentSong.value) |
| } |
| }) |
| |
| onUnmounted(() => { |
| if (lyricsTimer) { |
| clearTimeout(lyricsTimer) |
| lyricsTimer = null |
| } |
| }) |
| </script> |
| |
| <style scoped> |
| .desktop-full-player { |
| position: fixed; |
| top: 0; |
| left: 280px; |
| right: 0; |
| bottom: 0; |
| z-index: 1000; |
| background: var(--bg-primary); |
| display: flex; |
| flex-direction: column; |
| overflow: hidden; |
| } |
| |
| |
| .floating-back-btn { |
| position: absolute; |
| top: 20px; |
| left: 20px; |
| width: 44px; |
| height: 44px; |
| border: none; |
| background: rgba(255, 255, 255, 0.15); |
| color: var(--text-primary); |
| border-radius: 50%; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| cursor: pointer; |
| backdrop-filter: blur(20px); |
| transition: var(--transition-fast); |
| z-index: 20; |
| font-size: 16px; |
| border: 1px solid rgba(255, 255, 255, 0.1); |
| } |
| |
| .floating-back-btn:hover { |
| background: rgba(255, 255, 255, 0.25); |
| transform: scale(1.05); |
| border-color: rgba(255, 255, 255, 0.2); |
| } |
| |
| |
| .player-main { |
| display: grid; |
| grid-template-columns: 400px 1fr; |
| overflow: hidden; |
| height: 100%; |
| flex: 1; |
| } |
| |
| |
| .left-panel { |
| display: flex; |
| flex-direction: column; |
| padding: 20px; |
| background: var(--bg-card); |
| border-right: 1px solid var(--border-light); |
| overflow: hidden; |
| height: 100%; |
| position: relative; |
| } |
| |
| .panel-more-btn { |
| position: absolute; |
| top: 16px; |
| right: 16px; |
| width: 32px; |
| height: 32px; |
| border: none; |
| background: rgba(255, 255, 255, 0.1); |
| color: var(--text-secondary); |
| border-radius: 50%; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| cursor: pointer; |
| transition: var(--transition-fast); |
| z-index: 10; |
| font-size: 14px; |
| } |
| |
| .panel-more-btn:hover { |
| background: rgba(255, 255, 255, 0.2); |
| color: var(--text-primary); |
| transform: scale(1.05); |
| } |
| |
| .album-cover-section { |
| flex: 1; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| text-align: center; |
| } |
| |
| .album-cover-wrapper { |
| position: relative; |
| display: inline-block; |
| width: 180px; |
| height: 180px; |
| border-radius: 50%; |
| overflow: hidden; |
| box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); |
| background: var(--overlay-light); |
| border: 3px solid var(--border-light); |
| } |
| |
| .album-cover { |
| width: 100%; |
| height: 100%; |
| object-fit: cover; |
| border-radius: 50%; |
| transition: var(--transition-slow); |
| } |
| |
| .album-cover.rotating { |
| animation: cd-rotate 10s linear infinite; |
| } |
| |
| @keyframes cd-rotate { |
| from { transform: rotate(0deg); } |
| to { transform: rotate(360deg); } |
| } |
| |
| .album-cover:hover { |
| transform: scale(1.02); |
| } |
| |
| .cd-center { |
| position: absolute; |
| top: 50%; |
| left: 50%; |
| transform: translate(-50%, -50%); |
| width: 36px; |
| height: 36px; |
| border-radius: 50%; |
| background: rgba(0, 0, 0, 0.7); |
| backdrop-filter: blur(8px); |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| z-index: 2; |
| border: 2px solid var(--border-light); |
| } |
| |
| .center-dot { |
| width: 12px; |
| height: 12px; |
| border-radius: 50%; |
| background: rgba(255, 255, 255, 0.9); |
| } |
| |
| .song-info-section { |
| text-align: center; |
| flex-shrink: 0; |
| max-width: 100%; |
| margin-bottom: 16px; |
| } |
| |
| .song-title-row { |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| gap: 8px; |
| margin-bottom: 4px; |
| position: relative; |
| } |
| |
| .song-title { |
| font-size: 16px; |
| font-weight: 600; |
| color: var(--text-primary); |
| margin: 0; |
| line-height: 1.3; |
| word-wrap: break-word; |
| overflow: hidden; |
| text-overflow: ellipsis; |
| white-space: nowrap; |
| text-align: center; |
| flex: 1; |
| } |
| |
| .song-title-row .action-button { |
| position: absolute; |
| right: -8px; |
| flex-shrink: 0; |
| } |
| |
| .artist-name { |
| font-size: 13px; |
| color: var(--text-secondary); |
| margin: 0 0 3px; |
| font-weight: 500; |
| overflow: hidden; |
| text-overflow: ellipsis; |
| white-space: nowrap; |
| } |
| |
| .album-name { |
| font-size: 11px; |
| color: var(--text-tertiary); |
| margin: 0 0 12px; |
| overflow: hidden; |
| text-overflow: ellipsis; |
| white-space: nowrap; |
| } |
| |
| |
| .action-button { |
| width: 32px; |
| height: 32px; |
| border: none; |
| background: rgba(255, 255, 255, 0.1); |
| color: var(--text-primary); |
| border-radius: 50%; |
| font-size: 13px; |
| cursor: pointer; |
| transition: var(--transition-fast); |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| } |
| |
| .action-button:hover { |
| background: rgba(255, 255, 255, 0.2); |
| transform: scale(1.05); |
| } |
| |
| .favorite-btn.active { |
| background: rgba(255, 107, 107, 0.2); |
| color: var(--accent-red); |
| } |
| |
| .favorite-btn.active:hover { |
| background: rgba(255, 107, 107, 0.3); |
| } |
| |
| .progress-section { |
| flex-shrink: 0; |
| width: 100%; |
| margin-bottom: 6px; |
| } |
| |
| .controls-section { |
| flex-shrink: 0; |
| } |
| |
| |
| |
| .right-panel { |
| display: flex; |
| flex-direction: column; |
| background: var(--bg-primary); |
| overflow: hidden; |
| } |
| |
| |
| .lyrics-content { |
| flex: 1; |
| overflow: hidden; |
| padding: 0; |
| background: var(--bg-primary); |
| border: 1px solid var(--border-light); |
| } |
| |
| .lyrics-scroll-container { |
| height: 100%; |
| overflow-y: auto; |
| padding: 20px; |
| background: var(--bg-card); |
| border-radius: 12px; |
| position: relative; |
| } |
| |
| .lyrics-scroll-container::-webkit-scrollbar { |
| width: 6px; |
| } |
| |
| .lyrics-scroll-container::-webkit-scrollbar-track { |
| background: transparent; |
| border-radius: 3px; |
| } |
| |
| .lyrics-scroll-container::-webkit-scrollbar-thumb { |
| background: rgba(255, 107, 107, 0.3); |
| border-radius: 3px; |
| transition: var(--transition-fast); |
| } |
| |
| .lyrics-scroll-container::-webkit-scrollbar-thumb:hover { |
| background: rgba(255, 107, 107, 0.5); |
| } |
| |
| |
| .lyrics-scroll-container :deep(.lyrics-view) { |
| padding: 0; |
| height: 100%; |
| display: flex; |
| flex-direction: column; |
| } |
| |
| .lyrics-scroll-container :deep(.lyrics-list) { |
| flex: 1; |
| display: flex; |
| flex-direction: column; |
| justify-content: flex-start; |
| min-height: 100%; |
| gap: 8px; |
| } |
| |
| .lyrics-scroll-container :deep(.lyric-line) { |
| padding: 12px 16px; |
| font-size: 16px; |
| line-height: 1.8; |
| color: var(--text-secondary); |
| text-align: center; |
| transition: all 0.3s ease; |
| cursor: pointer; |
| border-radius: 8px; |
| margin: 0; |
| word-wrap: break-word; |
| white-space: normal; |
| } |
| |
| .lyrics-scroll-container :deep(.lyric-line:hover) { |
| color: var(--text-primary); |
| background: rgba(255, 107, 107, 0.05); |
| } |
| |
| .lyrics-scroll-container :deep(.lyric-line.active) { |
| color: var(--accent-red); |
| font-weight: 600; |
| font-size: 18px; |
| background: rgba(255, 107, 107, 0.1); |
| text-shadow: none; |
| box-shadow: 0 2px 8px rgba(255, 107, 107, 0.2); |
| transform: scale(1.02); |
| } |
| |
| .lyrics-scroll-container :deep(.lyric-line.next) { |
| color: var(--text-primary); |
| font-weight: 500; |
| background: rgba(255, 255, 255, 0.02); |
| } |
| |
| .lyrics-scroll-container :deep(.empty-lyrics) { |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| height: 100%; |
| color: var(--text-tertiary); |
| font-size: 16px; |
| font-style: italic; |
| } |
| |
| |
| .lyrics-scroll-container :deep(.lyric-line) { |
| opacity: 0.7; |
| } |
| |
| .lyrics-scroll-container :deep(.lyric-line.active), |
| .lyrics-scroll-container :deep(.lyric-line.next) { |
| opacity: 1; |
| } |
| |
| |
| .quality-selector { |
| position: fixed; |
| top: 0; |
| left: 0; |
| right: 0; |
| bottom: 0; |
| background: rgba(0, 0, 0, 0.8); |
| backdrop-filter: blur(20px); |
| z-index: 2000; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| animation: fadeIn 0.3s ease-out; |
| } |
| |
| .quality-panel { |
| background: var(--bg-card); |
| border-radius: 16px; |
| padding: 0; |
| width: 100%; |
| max-width: 480px; |
| max-height: 70vh; |
| overflow-y: auto; |
| animation: slideUpPanel 0.3s ease-out; |
| } |
| |
| .panel-header { |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| padding: 24px; |
| border-bottom: 1px solid var(--border-light); |
| } |
| |
| .panel-header h3 { |
| font-size: 18px; |
| font-weight: 600; |
| color: var(--text-primary); |
| margin: 0; |
| } |
| |
| .close-btn { |
| width: 32px; |
| height: 32px; |
| border: none; |
| background: rgba(255, 255, 255, 0.1); |
| color: var(--text-secondary); |
| border-radius: 50%; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| cursor: pointer; |
| transition: var(--transition-fast); |
| } |
| |
| .close-btn:hover { |
| background: rgba(255, 255, 255, 0.2); |
| color: var(--text-primary); |
| } |
| |
| .quality-list { |
| padding: 12px 0; |
| } |
| |
| .quality-item { |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| padding: 16px 24px; |
| cursor: pointer; |
| transition: var(--transition-fast); |
| } |
| |
| .quality-item:hover { |
| background: var(--overlay-lighter); |
| } |
| |
| .quality-item.active { |
| background: var(--bg-gradient-3); |
| } |
| |
| .quality-item.loading { |
| pointer-events: none; |
| } |
| |
| .quality-main { |
| flex: 1; |
| } |
| |
| .quality-name { |
| font-size: 16px; |
| font-weight: 600; |
| color: var(--text-primary); |
| margin-bottom: 2px; |
| } |
| |
| .quality-item.active .quality-name { |
| color: var(--accent-red); |
| } |
| |
| .quality-desc { |
| font-size: 13px; |
| color: var(--text-secondary); |
| } |
| |
| .quality-actions { |
| display: flex; |
| align-items: center; |
| gap: 12px; |
| } |
| |
| .quality-size { |
| font-size: 11px; |
| color: var(--text-tertiary); |
| min-width: 60px; |
| text-align: right; |
| } |
| |
| .quality-check { |
| color: var(--accent-red); |
| font-size: 14px; |
| } |
| |
| .quality-note { |
| display: flex; |
| align-items: center; |
| gap: 8px; |
| padding: 16px 24px; |
| background: rgba(255, 255, 255, 0.02); |
| border-top: 1px solid var(--border-lighter); |
| } |
| |
| .quality-note i { |
| color: var(--text-tertiary); |
| font-size: 12px; |
| } |
| |
| .quality-note span { |
| font-size: 12px; |
| color: var(--text-tertiary); |
| line-height: 1.4; |
| } |
| |
| @keyframes fadeIn { |
| from { |
| opacity: 0; |
| } |
| to { |
| opacity: 1; |
| } |
| } |
| |
| @keyframes slideUpPanel { |
| from { |
| transform: translateY(100%); |
| opacity: 0; |
| } |
| to { |
| transform: translateY(0); |
| opacity: 1; |
| } |
| } |
| </style> |
| |