Files
graduation-design/music-qianduan/src/components/MusicPlayer.vue
ikmkj ce33571719 feat(music): 添加点赞功能并优化音乐解析
- 在 MusicPlayer 组件中添加点赞功能,使用 Check 和 Pointer 图标表示点赞状态
- 引入 toggleLikeMusic API 接口
- 优化歌词解析逻辑,提高代码可读性和性能
- 修改 UserLikeMusic 实体类,使用复合主键设计
2025-07-26 00:44:34 +08:00

1099 lines
27 KiB
Vue

<template>
<div class="music-player" :class="{ 'has-music': currentMusic, 'expanded': isExpanded }"
:style="{
background: isExpanded ? 'rgba(255,255,255,0.9)' : 'linear-gradient(to right, #fff0f5, #ffeef3)',
borderTop: '1px solid #ffd6e0',
boxShadow: '0 -2px 15px rgba(255,107,129,0.15)'
}">
<div class="player-content">
<!-- 音乐信息 -->
<div class="music-info">
<template v-if="currentMusic">
<div class="music-cover" @click="toggleExpand">
<img :src="currentMusic.cover || '/default-cover.jpg'" alt="封面">
</div>
<div class="music-detail">
<div class="music-name" :title="currentMusic.name" @click="toggleExpand">{{ currentMusic.name }}</div>
<div class="music-singer" :title="getSingerNames">{{ getSingerNames }}</div>
</div>
</template>
<template v-else>
<div class="no-music">暂无播放音乐</div>
</template>
</div>
<!-- 播放控制 -->
<div class="player-controls">
<div class="control-buttons">
<el-tooltip content="上一首" placement="top" :disabled="!currentMusic">
<el-button circle @click="prevSong" :disabled="!currentMusic">
<el-icon><Back /></el-icon>
</el-button>
</el-tooltip>
<el-tooltip :content="isPlaying ? '暂停' : '播放'" placement="top" :disabled="!currentMusic">
<el-button circle @click="togglePlay" :disabled="!currentMusic">
<el-icon>
<component :is="isPlaying ? 'VideoPause' : 'VideoPlay'" />
</el-icon>
</el-button>
</el-tooltip>
<el-tooltip content="下一首" placement="top" :disabled="!currentMusic">
<el-button circle @click="nextSong" :disabled="!currentMusic">
<el-icon><Right /></el-icon>
</el-button>
</el-tooltip>
</div>
<div class="progress-bar">
<span class="time current">{{ formatTime(currentTime) }}</span>
<el-slider
v-model="sliderTime"
:max="duration"
:disabled="!currentMusic"
@change="handleSliderChange"
@input="handleSliderInput"
:format-tooltip="formatSliderTooltip"
:show-tooltip="true"
></el-slider>
<span class="time duration">{{ formatTime(duration) }}</span>
</div>
</div>
<!-- 音量和其他控制 -->
<div class="player-options">
<div class="volume-control">
<el-tooltip content="音量" placement="top">
<el-button circle @click="toggleMute">
<el-icon>
<component :is="volumeIcon" />
</el-icon>
</el-button>
</el-tooltip>
<el-slider
v-model="volume"
:max="100"
:min="0"
@change="handleVolumeChange"
class="volume-slider"
></el-slider>
</div>
<div class="play-mode">
<el-tooltip :content="loopModeText" placement="top">
<el-button circle @click="toggleLoopMode">
<el-icon>
<component :is="loopModeIcon" />
</el-icon>
</el-button>
</el-tooltip>
</div>
<div class="collect-button" v-if="currentMusic" style="margin-right: 15px;">
<el-tooltip :content="currentMusic.collected ? '取消收藏' : '收藏'" placement="top">
<el-button circle @click="toggleCollect">
<el-icon>
<component :is="currentMusic.collected ? 'StarFilled' : 'Star'" />
</el-icon>
</el-button>
</el-tooltip>
</div>
<div class="like-button" v-if="currentMusic" style="margin-right: 15px;">
<el-tooltip :content="currentMusic.liked ? '取消点赞' : '点赞'" placement="top">
<el-button circle @click="toggleLike">
<el-icon>
<component :is="currentMusic.liked ? 'Check' : 'Pointer'" />
</el-icon>
</el-button>
</el-tooltip>
</div>
<div class="playlist-toggle">
<el-tooltip content="播放列表" placement="top">
<el-button circle @click="togglePlaylist">
<el-icon><List /></el-icon>
<span v-if="playlist.length > 0" class="playlist-count">{{ playlist.length }}</span>
</el-button>
</el-tooltip>
</div>
</div>
</div>
<!-- 播放列表抽屉 -->
<el-drawer
v-model="showPlaylist"
title="播放列表"
direction="rtl"
size="300px"
:with-header="true"
:before-close="() => showPlaylist = false"
>
<div class="playlist-header">
<div class="playlist-title">
<span>当前播放列表 ({{ playlist.length }}首)</span>
</div>
<div class="playlist-actions">
<el-button type="text" @click="clearPlaylist" :disabled="playlist.length === 0">
<el-icon><Delete /></el-icon>
清空
</el-button>
</div>
</div>
<el-scrollbar height="calc(100vh - 200px)">
<ul class="playlist-items">
<li
v-for="(item, index) in playlist"
:key="item.id"
:class="{ 'active': currentMusic && currentMusic.id === item.id }"
@click="playFromPlaylist(index)"
>
<div class="playlist-item-info">
<div class="playlist-item-name">{{ item.name }}</div>
<div class="playlist-item-singer">{{ getSingerNamesFromMusic(item) }}</div>
</div>
<div class="playlist-item-actions">
<el-button
type="text"
@click.stop="removeFromPlaylist(index)"
class="remove-btn"
>
<el-icon><Close /></el-icon>
</el-button>
</div>
</li>
</ul>
</el-scrollbar>
</el-drawer>
<!-- 展开的播放器 -->
<teleport to="body" v-if="isExpanded && currentMusic">
<div class="expanded-player-overlay">
<div class="expanded-player">
<div class="expanded-header">
<el-button type="text" @click="toggleExpand" class="close-btn">
<el-icon><ArrowDown /></el-icon>
</el-button>
</div>
<div class="expanded-content">
<div class="expanded-left">
<div class="expanded-cover">
<img :src="currentMusic.cover || '/default-cover.jpg'" alt="封面" class="rotating" :class="{ 'paused': !isPlaying }">
</div>
<div class="expanded-info">
<h2 class="expanded-name">{{ currentMusic.name }}</h2>
<div class="expanded-singer">{{ getSingerNames }}</div>
<div class="expanded-album" v-if="currentMusic.album">专辑:{{ currentMusic.album }}</div>
<div class="expanded-type" v-if="currentMusic.typeName">类型:{{ currentMusic.typeName }}</div>
<div class="expanded-date" v-if="currentMusic.createTime">发行时间:{{ formatDate(currentMusic.createTime) }}</div>
<div class="expanded-progress">
<span class="time current">{{ formatTime(currentTime) }}</span>
<el-slider
v-model="sliderTime"
:max="duration"
@change="handleSliderChange"
@input="handleSliderInput"
:format-tooltip="formatSliderTooltip"
:show-tooltip="true"
class="expanded-slider"
></el-slider>
<span class="time duration">{{ formatTime(duration) }}</span>
</div>
<div class="expanded-controls">
<el-button circle @click="prevSong">
<el-icon><Back /></el-icon>
</el-button>
<el-button circle @click="togglePlay" class="play-btn">
<el-icon>
<component :is="isPlaying ? 'VideoPause' : 'VideoPlay'" />
</el-icon>
</el-button>
<el-button circle @click="nextSong">
<el-icon><Right /></el-icon>
</el-button>
</div>
</div>
</div>
<div class="expanded-right">
<el-tabs v-model="expandedActiveTab">
<el-tab-pane label="歌词" name="lyric">
<div class="expanded-lyric" v-if="currentMusic.lyric">
<el-scrollbar height="400px" ref="lyricScrollbar">
<div class="lyric-container">
<p
v-for="(line, index) in parsedLyric"
:key="index"
:class="{ 'active': currentLyricIndex === index }"
:ref="el => { if (el) lyricLines[index] = el }"
>
{{ line.text }}
</p>
</div>
</el-scrollbar>
</div>
<div class="no-lyric" v-else>暂无歌词</div>
</el-tab-pane>
<el-tab-pane label="播放列表" name="playlist">
<div class="expanded-playlist">
<div class="playlist-header">
<div class="playlist-title">
<span>当前播放列表 ({{ playlist.length }}首)</span>
</div>
<div class="playlist-actions">
<el-button type="text" @click="clearPlaylist" :disabled="playlist.length === 0">
<el-icon><Delete /></el-icon>
清空
</el-button>
</div>
</div>
<el-scrollbar height="400px">
<ul class="playlist-items">
<li
v-for="(item, index) in playlist"
:key="item.id"
:class="{ 'active': currentMusic && currentMusic.id === item.id }"
@click="playFromPlaylist(index)"
>
<div class="playlist-item-info">
<div class="playlist-item-name">{{ item.name }}</div>
<div class="playlist-item-singer">{{ getSingerNamesFromMusic(item) }}</div>
</div>
<div class="playlist-item-actions">
<el-button
type="text"
@click.stop="removeFromPlaylist(index)"
class="remove-btn"
>
<el-icon><Close /></el-icon>
</el-button>
</div>
</li>
</ul>
</el-scrollbar>
</div>
</el-tab-pane>
</el-tabs>
</div>
</div>
</div>
</div>
</teleport>
</div>
</template>
<script setup>
import { ref, computed, watch, nextTick } from 'vue'
import { useRouter } from 'vue-router'
import { usePlayerStore } from '../stores/player'
import { useUserStore } from '../stores/user'
import { processMusicUrl, toggleCollectMusic, getMusicStatus, toggleLikeMusic } from '../api/music'
import { ElMessage } from 'element-plus'
import {
VideoPlay, VideoPause, Back, Right, List, Close, Delete,
Mute, VideoCamera, Microphone, Star, StarFilled, ArrowDown, Pointer, Check
} from '@element-plus/icons-vue'
const router = useRouter()
const userStore = useUserStore()
// 播放器状态
const playerStore = usePlayerStore()
const isExpanded = ref(false)
const sliderTime = ref(0)
const lyricScrollbar = ref(null)
const lyricLines = ref({})
const currentLyricIndex = ref(-1)
const expandedActiveTab = ref('lyric')
// 计算属性
const currentMusic = computed(() => playerStore.getCurrentMusic)
const playlist = computed(() => playerStore.getPlaylist)
const isPlaying = computed(() => playerStore.getIsPlaying)
const currentTime = computed(() => playerStore.getCurrentTime)
const duration = computed(() => playerStore.getDuration)
const volume = computed({
get: () => playerStore.getVolume,
set: (val) => playerStore.setVolume(val)
})
const showPlaylist = computed({
get: () => playerStore.getShowPlaylist,
set: (val) => playerStore.setShowPlaylist(val)
})
const loopMode = computed(() => playerStore.getLoop)
// 获取歌手名称
const getSingerNames = computed(() => {
if (!currentMusic.value) return ''
if (currentMusic.value.singerNames && currentMusic.value.singerNames.length > 0) {
return currentMusic.value.singerNames.join('、')
}
return currentMusic.value.singer || '未知歌手'
})
// 获取音乐的歌手名称
const getSingerNamesFromMusic = (music) => {
if (!music) return ''
if (music.singerNames && music.singerNames.length > 0) {
return music.singerNames.join('、')
}
return music.singer || '未知歌手'
}
// 音量图标
const volumeIcon = computed(() => {
if (volume.value === 0) {
return 'Mute'
} else if (volume.value < 50) {
return 'VideoCamera'
} else {
return 'Microphone'
}
})
// 循环模式图标
const loopModeIcon = computed(() => {
switch (loopMode.value) {
case 'single': return 'Refresh'
case 'list': return 'Sort'
default: return 'Connection'
}
})
// 循环模式文本
const loopModeText = computed(() => {
switch (loopMode.value) {
case 'single': return '单曲循环'
case 'list': return '列表循环'
default: return '顺序播放'
}
})
// 解析歌词
const parsedLyric = computed(() => {
if (!currentMusic.value || !currentMusic.value.lyric) return [];
const lyric = currentMusic.value.lyric;
const lines = lyric.split('\n');
const result = [];
const timeRegExp = /\[(\d{2}):(\d{2})\.(\d{2,3})\]/g;
for (const line of lines) {
const text = line.replace(timeRegExp, '').trim();
if (text) {
timeRegExp.lastIndex = 0;
let match;
while ((match = timeRegExp.exec(line)) !== null) {
const min = parseInt(match, 10);
const sec = parseInt(match, 10);
const ms = parseInt((match || '0').padEnd(3, '0'), 10);
const time = min * 60 + sec + ms / 1000;
result.push({ time, text });
}
}
}
return result.sort((a, b) => a.time - b.time);
})
// 格式化时间
const formatTime = (seconds) => {
if (isNaN(seconds) || seconds < 0) return '00:00'
const mins = Math.floor(seconds / 60)
const secs = Math.floor(seconds % 60)
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
}
// 格式化日期
const formatDate = (dateStr) => {
if (!dateStr) return ''
try {
return new Date(dateStr).toLocaleDateString()
} catch (e) {
return dateStr
}
}
// 播放/暂停
const togglePlay = () => {
if (isPlaying.value) {
playerStore.pause()
} else {
playerStore.play()
}
}
// 上一首/下一首
const prevSong = () => playerStore.playPrev()
const nextSong = () => playerStore.playNext()
// 静音切换
const toggleMute = () => {
playerStore.setVolume(volume.value > 0 ? 0 : 80)
}
// 音量变化
const handleVolumeChange = (val) => playerStore.setVolume(val)
// 循环模式切换
const toggleLoopMode = () => {
const modes = ['none', 'list', 'single']
const currentIndex = modes.indexOf(loopMode.value)
const nextIndex = (currentIndex + 1) % modes.length
playerStore.setLoop(modes[nextIndex])
}
// 收藏/取消收藏
const toggleCollect = async () => {
if (!userStore.isLoggedIn) {
ElMessage.warning('请先登录')
router.push('/login')
return
}
if (!currentMusic.value || !currentMusic.value.id) return
try {
const res = await toggleCollectMusic(currentMusic.value.id)
if (res.code === 200) {
playerStore.updateCurrentMusicStatus({ collected: res.data })
ElMessage.success(res.message || (res.data ? '收藏成功' : '已取消收藏'))
} else {
ElMessage.error(res.message || '操作失败')
}
} catch (error) {
console.error('收藏操作失败:', error)
ElMessage.error('操作失败,请稍后重试')
}
}
// 点赞/取消点赞
const toggleLike = async () => {
if (!userStore.isLoggedIn) {
ElMessage.warning('请先登录')
router.push('/login')
return
}
if (!currentMusic.value || !currentMusic.value.id) return
try {
const res = await toggleLikeMusic(currentMusic.value.id)
if (res.code === 200) {
playerStore.updateCurrentMusicStatus({ liked: res.data })
ElMessage.success(res.message || (res.data ? '点赞成功' : '已取消点赞'))
} else {
ElMessage.error(res.message || '操作失败')
}
} catch (error) {
console.error('点赞操作失败:', error)
ElMessage.error('操作失败,请稍后重试')
}
}
// 播放列表相关
const togglePlaylist = () => playerStore.setShowPlaylist(!showPlaylist.value)
const playFromPlaylist = (index) => playerStore.playFromPlaylist(index)
const removeFromPlaylist = (index) => playerStore.removeFromPlaylist(index)
const clearPlaylist = () => playerStore.clearPlaylist()
// 展开/收起播放器
const toggleExpand = () => {
isExpanded.value = !isExpanded.value
document.body.style.overflow = isExpanded.value ? 'hidden' : ''
}
// 进度条处理
const isUserSeeking = ref(false)
const handleSliderChange = (val) => {
isUserSeeking.value = true
playerStore.setCurrentTime(val)
setTimeout(() => { isUserSeeking.value = false }, 500)
}
const handleSliderInput = () => { isUserSeeking.value = true }
const formatSliderTooltip = (val) => formatTime(val)
// 监听 Pinia store 的时间更新
watch(currentTime, (newVal) => {
if (!isUserSeeking.value) {
sliderTime.value = newVal
}
// 更新歌词
if (parsedLyric.value.length > 0) {
let newIndex = parsedLyric.value.findIndex(line => line.time > newVal) - 1
if (newIndex < 0) newIndex = parsedLyric.value.length - 1
if (currentLyricIndex.value !== newIndex) {
currentLyricIndex.value = newIndex
if (isExpanded.value) scrollToActiveLyric()
}
}
})
// 滚动到当前歌词
const scrollToActiveLyric = async () => {
await nextTick()
if (lyricScrollbar.value && currentLyricIndex.value >= 0) {
const element = lyricLines.value[currentLyricIndex.value]
if (element) {
lyricScrollbar.value.setScrollTop(element.offsetTop - 150)
}
}
}
// 监听当前播放歌曲的变化
watch(currentMusic, (newMusic) => {
if (newMusic && newMusic.id) {
// 处理 URL
playerStore.updateMusicUrl(newMusic.id, processMusicUrl(newMusic.url))
playerStore.updateMusicCover(newMusic.id, processMusicUrl(newMusic.cover))
// 主动获取最新的收藏和点赞状态
if (userStore.isLoggedIn) {
getMusicStatus(newMusic.id).then(res => {
if (res.code === 200 && res.data) {
playerStore.updateCurrentMusicStatus({
collected: res.data.collected,
liked: res.data.liked
});
}
}).catch(err => console.error("获取播放器音乐状态失败:", err));
} else {
playerStore.updateCurrentMusicStatus({ collected: false, liked: false });
}
// 重置歌词
currentLyricIndex.value = -1
lyricLines.value = {}
if (isExpanded.value) expandedActiveTab.value = 'lyric'
}
}, { deep: true })
</script>
<style scoped>
.music-player {
width: 100%;
height: 100%;
background-color: #fff;
border-top: 1px solid #eee;
padding: 10px 20px;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
z-index: 1000;
box-sizing: border-box;
margin-top: 0;
}
.player-content {
display: flex;
align-items: center;
justify-content: space-between;
height: 100%;
}
.music-info {
display: flex;
align-items: center;
width: 25%;
min-width: 200px;
}
.music-cover {
width: 50px;
height: 50px;
border-radius: 4px;
overflow: hidden;
margin-right: 10px;
cursor: pointer;
}
.music-cover img {
width: 100%;
height: 100%;
object-fit: cover;
}
.music-detail {
flex: 1;
overflow: hidden;
}
.music-name {
font-weight: bold;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 5px;
}
.music-singer {
font-size: 12px;
color: #666;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.no-music {
color: #999;
font-size: 14px;
}
.player-controls {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 0 20px;
}
.control-buttons {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.control-buttons .el-button {
margin: 0 10px;
}
.progress-bar {
width: 100%;
display: flex;
align-items: center;
}
.time {
font-size: 12px;
color: #666;
width: 40px;
text-align: center;
}
.el-slider {
flex: 1;
margin: 0 10px;
}
.player-options {
display: flex;
align-items: center;
width: 25%;
min-width: 200px;
justify-content: flex-end;
}
.volume-control {
display: flex;
align-items: center;
margin-right: 15px;
}
.volume-slider {
width: 80px;
margin-left: 5px;
}
.play-mode {
margin-right: 15px;
}
.collect-button {
margin-right: 15px;
}
.playlist-toggle {
position: relative;
}
.playlist-count {
position: absolute;
top: -5px;
right: -5px;
background-color: #f56c6c;
color: white;
font-size: 10px;
border-radius: 50%;
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
}
/* 播放列表样式 */
.playlist-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px 10px;
border-bottom: 1px solid #eee;
margin-bottom: 10px;
}
.el-drawer {
z-index: 2001; /* 确保抽屉在播放器上方显示 */
}
.playlist-items {
list-style: none;
padding: 0;
margin: 0;
}
.playlist-items li {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
border-bottom: 1px solid #f5f5f5;
cursor: pointer;
}
.playlist-items li:hover {
background-color: #f5f5f5;
}
.playlist-items li.active {
background-color: #ecf5ff;
color: #409eff;
}
.playlist-item-info {
flex: 1;
overflow: hidden;
}
.playlist-item-name {
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 5px;
}
.playlist-item-singer {
font-size: 12px;
color: #666;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.remove-btn {
opacity: 0;
transition: opacity 0.3s;
}
.playlist-items li:hover .remove-btn {
opacity: 1;
}
/* 展开的播放器样式 */
.expanded-player-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
height: 100vh;
width: 100vw;
background-color: rgba(255, 255, 255, 0.95);
z-index: 2000;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
.expanded-player {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.expanded-header {
display: flex;
justify-content: flex-end;
padding: 20px;
}
.expanded-content {
flex: 1;
display: flex;
padding: 0 20px;
overflow: hidden;
}
.expanded-left {
width: 40%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-right: 20px;
}
.expanded-right {
width: 60%;
padding-left: 20px;
border-left: 1px solid #eee;
overflow: hidden;
}
.expanded-cover {
width: 300px;
height: 300px;
border-radius: 50%;
overflow: hidden;
margin-bottom: 30px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
}
.expanded-cover img {
width: 100%;
height: 100%;
object-fit: cover;
}
.expanded-info {
text-align: center;
margin-top: 30px;
width: 100%;
max-width: 500px;
}
.expanded-type,
.expanded-date {
font-size: 14px;
color: #999;
margin-bottom: 5px;
}
.expanded-name {
font-size: 24px;
font-weight: bold;
margin-bottom: 10px;
}
.expanded-singer {
font-size: 16px;
color: #666;
margin-bottom: 5px;
}
.expanded-album {
font-size: 14px;
color: #999;
margin-bottom: 20px;
}
.expanded-progress {
display: flex;
align-items: center;
margin-bottom: 20px;
}
.expanded-slider {
flex: 1;
margin: 0 15px;
}
.expanded-controls {
display: flex;
justify-content: center;
align-items: center;
}
/* 底部音乐控制器整体样式 */
.music-player {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 80px;
z-index: 1000;
transition: all 0.3s ease;
display: flex;
align-items: center;
padding: 0 15px;
}
.player-content {
display: flex;
align-items: center;
width: 100%;
height: 100%;
backdrop-filter: blur(5px);
gap: 10px;
}
.control-buttons {
display: flex;
align-items: center;
gap: 8px;
margin: 0 10px;
}
.control-buttons .el-button {
width: 36px;
height: 36px;
flex-shrink: 0;
}
.music-cover {
width: 50px;
height: 50px;
border-radius: 8px;
overflow: hidden;
margin-right: 15px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.music-name {
color: #ff6b81;
font-weight: 600;
font-size: 0.95rem;
}
.control-buttons .el-button {
width: 40px;
height: 40px;
background-color: #ffb6c1;
border: none;
color: white;
transition: all 0.3s;
}
.control-buttons .el-button:hover {
background-color: #ff8a9b;
transform: scale(1.1);
}
.progress-bar {
flex: 1;
padding: 0 15px;
}
.progress-bar .el-slider {
--el-slider-main-bg-color: #ff6b81;
}
.player-options .el-button {
background-color: rgba(255,255,255,0.7);
border: 1px solid #ffd6e0;
color: #ff6b81;
}
.player-options .el-button:hover {
background-color: #ffb6c1;
color: white;
}
.expanded-controls .el-button {
width: 50px;
height: 50px;
background-color: #ffb6c1;
border: none;
color: white;
transition: all 0.3s ease;
margin: 0 15px;
}
.expanded-controls .el-button:hover {
background-color: #ff8a9b;
transform: scale(1.1);
}
.play-btn {
transform: scale(1.5);
background-color: #ff6b81 !important;
}
.expanded-lyric {
width: 100%;
height: 100%;
}
.no-lyric {
text-align: center;
color: #999;
padding: 50px 0;
}
.expanded-playlist {
width: 100%;
height: 100%;
}
.lyric-container {
padding: 20px 0;
}
.lyric-container p {
text-align: center;
margin: 15px 0;
color: #666;
transition: all 0.3s;
}
.lyric-container p.active {
color: #ff6b81;
font-size: 18px;
font-weight: bold;
transition: all 0.3s;
}
/* 旋转动画 */
.rotating {
animation: rotate 20s linear infinite;
}
.rotating.paused {
animation-play-state: paused;
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* 响应式调整 */
@media (max-width: 768px) {
.player-content {
flex-direction: column;
}
.music-info, .player-controls, .player-options {
width: 100%;
margin-bottom: 10px;
}
.player-options {
justify-content: center;
}
.player-options .el-button {
width: 30px;
height: 30px;
}
.control-buttons .el-button {
width: 35px;
height: 35px;
}
.music-cover {
width: 40px;
height: 40px;
}
}
</style>