music / src /components /player /PlayControls.vue
ahutchen's picture
feat(components): 优化多个组件的样式和功能
9e40388
<template>
<div class="play-controls">
<button
class="control-btn mode-btn"
@click="togglePlayMode"
:title="playModeText"
>
<i :class="playModeIcon"></i>
</button>
<button
class="control-btn prev-btn"
@click="$emit('previous')"
:disabled="!hasPrevious"
title="上一首"
>
<i class="fas fa-step-backward"></i>
</button>
<button
class="control-btn play-btn"
@click="$emit('togglePlay')"
:disabled="!hasAudio"
:title="isPlaying ? '暂停' : '播放'"
>
<i :class="isPlaying ? 'fas fa-pause' : 'fas fa-play'"></i>
<div v-if="loading" class="loading-spinner">
<i class="fas fa-spinner fa-spin"></i>
</div>
</button>
<button
class="control-btn next-btn"
@click="$emit('next')"
:disabled="!hasNext"
title="下一首"
>
<i class="fas fa-step-forward"></i>
</button>
<button
class="control-btn playlist-btn"
@click="$emit('showPlaylist')"
title="播放列表"
>
<i class="fas fa-list"></i>
<span v-if="playlistCount > 0" class="playlist-count">{{ playlistCount }}</span>
</button>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { usePlayerStore } from '@/stores/player'
import { usePlayQueueStore } from '@/stores/playqueue'
const props = defineProps({
loading: {
type: Boolean,
default: false
}
})
const emit = defineEmits([
'togglePlay',
'previous',
'next',
'togglePlayMode',
'showPlaylist'
])
const playerStore = usePlayerStore()
const playQueueStore = usePlayQueueStore()
// 计算属性
const isPlaying = computed(() => playerStore.isPlaying)
const playMode = computed(() => playQueueStore.playMode)
const hasPrevious = computed(() => playQueueStore.hasPrevious)
const hasNext = computed(() => playQueueStore.hasNext)
const playlistCount = computed(() => playQueueStore.queueLength)
const hasAudio = computed(() => !!playerStore.audioSrc)
const togglePlayMode = () => {
playQueueStore.togglePlayMode()
}
const playModeIcon = computed(() => {
switch (playMode.value) {
case 'single':
return 'fas fa-redo'
case 'random':
return 'fas fa-random'
case 'list':
default:
return 'fas fa-retweet'
}
})
const playModeText = computed(() => {
switch (playMode.value) {
case 'single':
return '单曲循环'
case 'random':
return '随机播放'
case 'list':
default:
return '列表循环'
}
})
</script>
<style scoped>
.play-controls {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
padding: 20px 0;
}
.control-btn {
border: none;
background: var(--bg-overlay);
color: var(--text-primary);
border-radius: 50%;
cursor: pointer;
transition: var(--transition-fast);
display: flex;
align-items: center;
justify-content: center;
position: relative;
backdrop-filter: blur(10px);
border: 1px solid var(--border-light);
}
.control-btn:hover:not(:disabled) {
background: var(--bg-card);
transform: scale(1.05);
border-color: var(--border-card);
}
.control-btn:active:not(:disabled) {
transform: scale(0.95);
}
.control-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
/* 按钮尺寸 */
.mode-btn,
.prev-btn,
.next-btn,
.playlist-btn {
width: 48px;
height: 48px;
font-size: 18px;
}
.play-btn {
width: 72px;
height: 72px;
font-size: 28px;
background: linear-gradient(135deg, var(--accent-red), var(--accent-red-hover));
color: white;
box-shadow: var(--shadow-button);
position: relative;
}
.play-btn:hover:not(:disabled) {
background: linear-gradient(135deg, var(--accent-red-hover), #ff4444);
box-shadow: 0 8px 25px rgba(255, 107, 107, 0.6);
transform: scale(1.08);
}
.play-btn:disabled {
background: var(--bg-overlay);
box-shadow: none;
border-color: var(--border-light);
}
/* 加载动画 */
.loading-spinner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 24px;
color: rgba(255, 255, 255, 0.8);
}
/* 播放列表计数 */
.playlist-count {
position: absolute;
top: -4px;
right: -4px;
background: var(--accent-red);
color: white;
border-radius: 50%;
width: 20px;
height: 20px;
font-size: 10px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
line-height: 1;
min-width: 20px;
}
/* 播放模式按钮状态 */
.mode-btn[title*="单曲"] {
color: var(--accent-red);
}
.mode-btn[title*="随机"] {
color: #4CAF50;
}
.mode-btn[title*="列表"] {
color: var(--text-primary);
}
/* 响应式 */
@media (max-width: 375px) {
.play-controls {
gap: 12px;
padding: 16px 0;
}
.mode-btn,
.prev-btn,
.next-btn,
.playlist-btn {
width: 44px;
height: 44px;
font-size: 16px;
}
.play-btn {
width: 64px;
height: 64px;
font-size: 24px;
}
.loading-spinner {
font-size: 20px;
}
.playlist-count {
width: 18px;
height: 18px;
font-size: 9px;
top: -3px;
right: -3px;
}
}
/* 按钮动画效果 */
.control-btn::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
background: rgba(255, 255, 255, 0.3);
border-radius: 50%;
transform: translate(-50%, -50%);
transition: all 0.3s ease;
}
.control-btn:active:not(:disabled)::before {
width: 100%;
height: 100%;
}
/* 播放按钮特殊效果 */
.play-btn::after {
content: '';
position: absolute;
top: -2px;
left: -2px;
right: -2px;
bottom: -2px;
border-radius: 50%;
background: linear-gradient(135deg, var(--accent-red), var(--accent-red-hover));
z-index: -1;
opacity: 0;
transition: opacity var(--transition-fast);
}
.play-btn:hover:not(:disabled)::after {
opacity: 0.3;
animation: pulse-ring 1.5s ease-out infinite;
}
@keyframes pulse-ring {
0% {
transform: scale(1);
opacity: 0.3;
}
100% {
transform: scale(1.3);
opacity: 0;
}
}
</style>