- 在 MusicPlayer 组件中添加点赞功能,使用 Check 和 Pointer 图标表示点赞状态 - 引入 toggleLikeMusic API 接口 - 优化歌词解析逻辑,提高代码可读性和性能 - 修改 UserLikeMusic 实体类,使用复合主键设计
1099 lines
27 KiB
Vue
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>
|