music / src /components /layout /DesktopSidebar.vue
ahutchen's picture
feat(player): 优化播放逻辑并添加状态过期处理
25e7862
<template>
<div class="desktop-sidebar hidden-mobile">
<!-- 应用Logo和标题 -->
<div class="sidebar-header">
<div class="app-logo">
<i class="fas fa-music"></i>
</div>
<div class="app-title">云音乐</div>
</div>
<!-- 导航菜单 -->
<nav class="sidebar-nav">
<router-link
v-for="item in navItems"
:key="item.name"
:to="item.path"
class="nav-item"
:class="{ active: currentRoute === item.name }"
@click="handleNavClick(item)"
>
<i :class="item.icon" class="nav-icon"></i>
<span class="nav-label">{{ item.label }}</span>
<div class="nav-indicator" v-if="currentRoute === item.name"></div>
</router-link>
</nav>
<!-- 播放控制区域 -->
<div class="sidebar-player" v-if="currentSong">
<!-- 当前播放歌曲信息 -->
<div class="current-song" @click="openFullPlayer">
<div class="song-cover">
<img
:src="currentCoverUrl || defaultCover"
:alt="currentSong.name"
class="cover-image"
@error="handleImageError"
/>
<div class="play-overlay" @click.stop="togglePlay">
<i :class="isPlaying ? 'fas fa-pause' : 'fas fa-play'"></i>
</div>
</div>
<div class="song-details">
<div class="song-name" :title="currentSong.name">{{ currentSong.name }}</div>
<div class="song-artist" :title="formattedArtist">{{ formattedArtist }}</div>
</div>
</div>
<!-- 播放控制按钮 -->
<div class="player-controls">
<button class="control-btn" @click="playPrevious" title="上一首">
<i class="fas fa-step-backward"></i>
</button>
<button class="control-btn play-btn" @click="togglePlay" :title="isPlaying ? '暂停' : '播放'">
<i :class="isPlaying ? 'fas fa-pause' : 'fas fa-play'"></i>
</button>
<button class="control-btn" @click="playNext" title="下一首">
<i class="fas fa-step-forward"></i>
</button>
<button class="control-btn" @click="openFullPlayer" title="打开播放器">
<i class="fas fa-expand"></i>
</button>
</div>
<!-- 进度条 -->
<div class="progress-container">
<div class="progress-background" @click="handleProgressClick" ref="progressRef">
<div
class="progress-fill"
:style="{ width: `${progress}%` }"
></div>
</div>
</div>
<!-- 音量控制 -->
<div class="volume-control">
<button class="volume-btn" @click="toggleMute">
<i :class="volumeIcon"></i>
</button>
<input
type="range"
min="0"
max="100"
:value="volume"
@input="handleVolumeChange"
class="volume-slider"
/>
</div>
</div>
<!-- 播放统计 -->
<div class="sidebar-stats" v-if="!currentSong">
<div class="stats-item">
<i class="fas fa-music"></i>
<div class="stats-info">
<div class="stats-label">暂无播放</div>
<div class="stats-value">选择音乐开始播放</div>
</div>
</div>
</div>
<!-- 快捷操作 -->
<div class="sidebar-actions">
<button class="action-btn" @click="toggleTheme" :title="isDarkTheme ? '切换到亮色主题' : '切换到暗色主题'">
<i :class="isDarkTheme ? 'fas fa-sun' : 'fas fa-moon'"></i>
</button>
<button class="action-btn" @click="openSettings" title="设置">
<i class="fas fa-cog"></i>
</button>
</div>
</div>
</template>
<script setup>
import { computed, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { usePlayerStore } from '@/stores/player'
import { usePlayQueueStore } from '@/stores/playqueue'
import { useSettingsStore } from '@/stores/settings'
import { utils } from '@/services/musicApi'
const route = useRoute()
const router = useRouter()
const playerStore = usePlayerStore()
const playQueueStore = usePlayQueueStore()
const settingsStore = useSettingsStore()
// 播放器相关状态
const currentCoverUrl = ref('')
const isMuted = ref(false)
const lastVolume = ref(80)
const progressRef = ref(null)
// 导航项配置
const navItems = [
{
name: 'Home',
path: '/home',
label: '首页',
icon: 'fas fa-home'
},
{
name: 'Favorites',
path: '/favorites',
label: '我喜欢',
icon: 'fas fa-heart'
},
{
name: 'Playlists',
path: '/playlists',
label: '歌单',
icon: 'fas fa-music'
},
{
name: 'PlayQueue',
path: '/play-queue',
label: '播放列表',
icon: 'fas fa-list'
},
{
name: 'History',
path: '/history',
label: '播放历史',
icon: 'fas fa-history'
}
]
// 计算属性
const currentRoute = computed(() => route.name)
const currentSong = computed(() => playerStore.currentSong)
const isPlaying = computed(() => playerStore.isPlaying)
const progress = computed(() => playerStore.progress)
const volume = computed(() => playerStore.volume || 80)
const isDarkTheme = computed(() => settingsStore.settings.theme === 'dark')
const formattedArtist = computed(() => {
if (!currentSong.value?.artist) return ''
return utils.formatArtist(currentSong.value.artist)
})
// 默认封面
const defaultCover = computed(() => {
return 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjQiIGhlaWdodD0iNjQiIHZpZXdCb3g9IjAgMCA2NCA2NCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjY0IiBoZWlnaHQ9IjY0IiBmaWxsPSJyZ2JhKDI1NSwyNTUsMjU1LDAuMSkiIHJ4PSI4Ii8+CjxwYXRoIGQ9Ik0zMiAyMEw0MCAzMkgzNlY0NEgyOFYzMkgyNEwzMiAyMFoiIGZpbGw9InJnYmEoMjU1LDI1NSwyNTUsMC4zKSIvPgo8L3N2Zz4K'
})
// 音量图标
const volumeIcon = computed(() => {
if (isMuted.value || volume.value === 0) {
return 'fas fa-volume-mute'
} else if (volume.value < 30) {
return 'fas fa-volume-off'
} else if (volume.value < 70) {
return 'fas fa-volume-down'
} else {
return 'fas fa-volume-up'
}
})
// 方法
const handleNavClick = (item) => {
console.log('导航到:', item.label)
}
// 播放器控制方法
const togglePlay = () => {
// 通过全局事件触发App.vue中的播放控制
window.dispatchEvent(new CustomEvent('sidebarTogglePlay'))
}
const playPrevious = () => {
const prevSong = playQueueStore.playPrevious()
if (prevSong) {
// 强制从头开始播放上一首歌曲
playerStore.setCurrentTime(0)
playerStore.playSong(prevSong)
}
}
const playNext = () => {
const nextSong = playQueueStore.playNext()
if (nextSong) {
// 强制从头开始播放下一首歌曲
playerStore.setCurrentTime(0)
playerStore.playSong(nextSong)
}
}
const openFullPlayer = () => {
if (currentSong.value) {
router.push('/player')
}
}
// 进度条控制
const handleProgressClick = (event) => {
if (!progressRef.value || !playerStore.duration) return
const rect = progressRef.value.getBoundingClientRect()
const percent = (event.clientX - rect.left) / rect.width
const newTime = percent * playerStore.duration
playerStore.seekTo(newTime)
}
// 音量控制
const toggleMute = () => {
if (isMuted.value) {
playerStore.setVolume(lastVolume.value)
isMuted.value = false
} else {
lastVolume.value = volume.value
playerStore.setVolume(0)
isMuted.value = true
}
}
const handleVolumeChange = (event) => {
const newVolume = parseInt(event.target.value)
playerStore.setVolume(newVolume)
isMuted.value = newVolume === 0
}
// 图片加载错误处理
const handleImageError = () => {
currentCoverUrl.value = defaultCover.value
}
// 加载专辑封面
const loadAlbumCover = async () => {
if (!currentSong.value) {
currentCoverUrl.value = defaultCover.value
return
}
try {
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 toggleTheme = () => {
const newTheme = isDarkTheme.value ? 'light' : 'dark'
settingsStore.updateSetting('theme', newTheme)
settingsStore.applyTheme(newTheme)
}
const openSettings = () => {
router.push('/settings')
}
// 监听当前歌曲变化
watch(currentSong, (newSong) => {
if (newSong) {
loadAlbumCover()
} else {
currentCoverUrl.value = defaultCover.value
}
}, { immediate: true })
</script>
<style scoped>
.desktop-sidebar {
width: 280px;
height: 100vh;
background: var(--bg-card);
border-right: 1px solid var(--border-light);
display: flex;
flex-direction: column;
position: fixed;
left: 0;
top: 0;
z-index: 100;
backdrop-filter: blur(20px);
}
/* 应用头部 */
.sidebar-header {
display: flex;
align-items: center;
gap: 12px;
padding: 24px 20px;
border-bottom: 1px solid var(--border-lighter);
}
.app-logo {
width: 40px;
height: 40px;
background: linear-gradient(135deg, var(--primary-color), var(--primary-color-hover));
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 18px;
box-shadow: 0 4px 12px var(--glow-color);
}
.app-title {
font-size: 20px;
font-weight: 700;
color: var(--text-primary);
}
/* 导航菜单 */
.sidebar-nav {
flex: 1;
padding: 16px 0;
overflow-y: auto;
}
.nav-item {
display: flex;
align-items: center;
gap: 12px;
padding: 16px 20px;
text-decoration: none;
color: var(--text-secondary);
transition: all var(--transition-fast);
position: relative;
margin: 0 12px;
border-radius: 12px;
}
.nav-item:hover {
background: var(--overlay-lighter);
color: var(--text-primary);
transform: translateX(4px);
}
.nav-item.active {
background: linear-gradient(135deg, var(--primary-color), var(--primary-color-hover));
color: white;
box-shadow: 0 4px 12px var(--glow-color);
}
.nav-item.active .nav-icon {
color: white;
}
.nav-icon {
font-size: 18px;
width: 20px;
text-align: center;
transition: var(--transition-fast);
}
.nav-label {
font-size: 16px;
font-weight: 500;
}
.nav-indicator {
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 20px;
background: white;
border-radius: 2px;
opacity: 0.8;
}
/* 播放统计 */
.sidebar-stats {
padding: 16px 20px;
border-top: 1px solid var(--border-lighter);
border-bottom: 1px solid var(--border-lighter);
}
.stats-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 0;
}
.stats-item i {
font-size: 16px;
color: var(--primary-color);
width: 20px;
text-align: center;
}
.stats-info {
flex: 1;
min-width: 0;
}
.stats-label {
font-size: 12px;
color: var(--text-tertiary);
margin-bottom: 2px;
}
.stats-value {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* 快捷操作 */
.sidebar-actions {
display: flex;
gap: 8px;
padding: 16px 20px;
}
.action-btn {
flex: 1;
height: 40px;
border: none;
background: var(--overlay-lighter);
color: var(--text-secondary);
border-radius: 10px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
transition: var(--transition-fast);
}
.action-btn:hover {
background: var(--overlay-light);
color: var(--text-primary);
transform: scale(1.05);
}
/* 滚动条样式 */
.sidebar-nav::-webkit-scrollbar {
width: 4px;
}
.sidebar-nav::-webkit-scrollbar-track {
background: transparent;
}
.sidebar-nav::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 2px;
}
.sidebar-nav::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.4);
}
/* 黑色主题适配 */
[data-theme="dark"] .sidebar-nav::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
}
[data-theme="dark"] .sidebar-nav::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.2);
}
/* 侧边栏播放器 */
.sidebar-player {
padding: 20px;
border-top: 1px solid var(--border-lighter);
border-bottom: 1px solid var(--border-lighter);
background: var(--overlay-lighter);
backdrop-filter: blur(10px);
}
.current-song {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
cursor: pointer;
transition: var(--transition-fast);
border-radius: 8px;
padding: 4px;
}
.current-song:hover {
background: var(--overlay-lighter);
transform: scale(1.02);
}
.song-cover {
position: relative;
width: 48px;
height: 48px;
border-radius: 8px;
overflow: hidden;
flex-shrink: 0;
}
.cover-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.play-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: var(--transition-fast);
cursor: pointer;
}
.song-cover:hover .play-overlay {
opacity: 1;
}
.play-overlay i {
color: white;
font-size: 14px;
}
.song-details {
flex: 1;
min-width: 0;
}
.song-name {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.song-artist {
font-size: 12px;
color: var(--text-secondary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* 播放控制按钮 */
.player-controls {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin-bottom: 16px;
}
.control-btn {
width: 32px;
height: 32px;
border-radius: 50%;
background: var(--overlay-light);
border: 1px solid var(--border-light);
color: var(--text-secondary);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
transition: var(--transition-fast);
}
.control-btn:hover {
background: var(--overlay-light);
color: var(--text-primary);
transform: scale(1.1);
}
.control-btn.play-btn {
width: 36px;
height: 36px;
font-size: 14px;
background: var(--primary-color);
color: white;
border-color: var(--primary-color);
}
.control-btn.play-btn:hover {
background: var(--primary-color-hover);
border-color: var(--primary-color-hover);
box-shadow: 0 0 12px var(--glow-color);
}
/* 进度条 */
.progress-container {
margin-bottom: 16px;
}
.progress-background {
height: 4px;
background: var(--border-light);
border-radius: 2px;
cursor: pointer;
position: relative;
transition: var(--transition-fast);
}
.progress-background:hover {
height: 6px;
transform: scaleY(1.2);
}
.progress-fill {
height: 100%;
background: var(--primary-color);
border-radius: 2px;
transition: width 0.1s ease;
position: relative;
}
.progress-fill::after {
content: '';
position: absolute;
right: -2px;
top: 50%;
transform: translateY(-50%);
width: 8px;
height: 8px;
background: var(--primary-color);
border-radius: 50%;
opacity: 0;
transition: var(--transition-fast);
}
.progress-background:hover .progress-fill::after {
opacity: 1;
}
/* 音量控制 */
.volume-control {
display: flex;
align-items: center;
gap: 12px;
}
.volume-btn {
width: 28px;
height: 28px;
border-radius: 50%;
background: var(--overlay-lighter);
border: none;
color: var(--text-secondary);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
transition: var(--transition-fast);
flex-shrink: 0;
}
.volume-btn:hover {
background: var(--overlay-light);
color: var(--text-primary);
}
.volume-slider {
flex: 1;
height: 3px;
background: var(--border-light);
border-radius: 2px;
outline: none;
appearance: none;
cursor: pointer;
transition: var(--transition-fast);
}
.volume-slider::-webkit-slider-thumb {
appearance: none;
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--primary-color);
cursor: pointer;
border: 2px solid white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
transition: var(--transition-fast);
}
.volume-slider::-webkit-slider-thumb:hover {
transform: scale(1.2);
}
.volume-slider::-moz-range-thumb {
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--primary-color);
cursor: pointer;
border: 2px solid white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
transition: var(--transition-fast);
}
/* 响应式 - 只在PC端显示 */
@media (max-width: 1023px) {
.desktop-sidebar {
display: none;
}
}
</style>