|
|
<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> |