添加播放器ui
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { RouterLink, RouterView } from 'vue-router'
|
import { RouterLink, RouterView } from 'vue-router'
|
||||||
|
import AudioPlayer from './components/AudioPlayer.vue'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -12,6 +13,7 @@ import { RouterLink, RouterView } from 'vue-router'
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<RouterView />
|
<RouterView />
|
||||||
|
<AudioPlayer />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
320
web/src/components/AudioPlayer.vue
Normal file
320
web/src/components/AudioPlayer.vue
Normal file
@@ -0,0 +1,320 @@
|
|||||||
|
<template>
|
||||||
|
<div class="player">
|
||||||
|
<div class="player-info">
|
||||||
|
<div class="track-info">
|
||||||
|
<span class="track-title">{{ currentTrack.title || '未播放' }}</span>
|
||||||
|
<span class="track-artist">{{ currentTrack.artist || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="player-controls">
|
||||||
|
<button @click="seekBackward" class="control-btn">
|
||||||
|
<SkipBack />
|
||||||
|
</button>
|
||||||
|
<button @click="togglePlay" class="play-btn">
|
||||||
|
<Play v-if="!isPlaying" />
|
||||||
|
<Pause v-else />
|
||||||
|
</button>
|
||||||
|
<button @click="seekForward" class="control-btn">
|
||||||
|
<SkipForward />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="player-progress">
|
||||||
|
<span class="time">{{ formatTime(currentTime) }}</span>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
:value="progress"
|
||||||
|
@input="seekTo"
|
||||||
|
class="progress-bar"
|
||||||
|
/>
|
||||||
|
<span class="time">{{ formatTime(duration) }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="player-volume">
|
||||||
|
<button @click="toggleMute" class="volume-btn">
|
||||||
|
<Volume2 v-if="!isMuted && volume > 0" />
|
||||||
|
<VolumeX v-else />
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
:value="isMuted ? 0 : volume"
|
||||||
|
@input="setVolume"
|
||||||
|
class="volume-slider"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import { Play, Pause, SkipBack, SkipForward, Volume2, VolumeX } from 'lucide-vue-next'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
src: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
artist: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const audio = ref(null)
|
||||||
|
const isPlaying = ref(false)
|
||||||
|
const currentTime = ref(0)
|
||||||
|
const duration = ref(0)
|
||||||
|
const volume = ref(80)
|
||||||
|
const isMuted = ref(false)
|
||||||
|
|
||||||
|
const currentTrack = computed(() => ({
|
||||||
|
title: props.title,
|
||||||
|
artist: props.artist,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const progress = computed(() => {
|
||||||
|
if (duration.value === 0) return 0
|
||||||
|
return (currentTime.value / duration.value) * 100
|
||||||
|
})
|
||||||
|
|
||||||
|
const togglePlay = () => {
|
||||||
|
if (!audio.value) {
|
||||||
|
audio.value = new Audio(props.src)
|
||||||
|
setupAudioListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPlaying.value) {
|
||||||
|
audio.value.pause()
|
||||||
|
} else {
|
||||||
|
audio.value.play()
|
||||||
|
}
|
||||||
|
isPlaying.value = !isPlaying.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const setupAudioListeners = () => {
|
||||||
|
if (!audio.value) return
|
||||||
|
|
||||||
|
audio.value.addEventListener('timeupdate', () => {
|
||||||
|
currentTime.value = audio.value.currentTime
|
||||||
|
})
|
||||||
|
|
||||||
|
audio.value.addEventListener('loadedmetadata', () => {
|
||||||
|
duration.value = audio.value.duration
|
||||||
|
})
|
||||||
|
|
||||||
|
audio.value.addEventListener('ended', () => {
|
||||||
|
isPlaying.value = false
|
||||||
|
currentTime.value = 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const seekTo = (e) => {
|
||||||
|
if (!audio.value) return
|
||||||
|
const time = (e.target.value / 100) * duration.value
|
||||||
|
audio.value.currentTime = time
|
||||||
|
}
|
||||||
|
|
||||||
|
const seekForward = () => {
|
||||||
|
if (!audio.value) return
|
||||||
|
audio.value.currentTime = Math.min(audio.value.currentTime + 10, duration.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const seekBackward = () => {
|
||||||
|
if (!audio.value) return
|
||||||
|
audio.value.currentTime = Math.max(audio.value.currentTime - 10, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setVolume = (e) => {
|
||||||
|
const newVolume = e.target.value
|
||||||
|
volume.value = newVolume
|
||||||
|
if (audio.value) {
|
||||||
|
audio.value.volume = newVolume / 100
|
||||||
|
}
|
||||||
|
if (newVolume > 0 && isMuted.value) {
|
||||||
|
isMuted.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleMute = () => {
|
||||||
|
isMuted.value = !isMuted.value
|
||||||
|
if (audio.value) {
|
||||||
|
audio.value.volume = isMuted.value ? 0 : volume.value / 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatTime = (seconds) => {
|
||||||
|
if (!seconds || isNaN(seconds)) return '0:00'
|
||||||
|
const mins = Math.floor(seconds / 60)
|
||||||
|
const secs = Math.floor(seconds % 60)
|
||||||
|
return `${mins}:${secs.toString().padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.src,
|
||||||
|
(newSrc) => {
|
||||||
|
if (audio.value) {
|
||||||
|
audio.value.src = newSrc
|
||||||
|
currentTime.value = 0
|
||||||
|
isPlaying.value = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(volume, (newVolume) => {
|
||||||
|
if (audio.value && !isMuted.value) {
|
||||||
|
audio.value.volume = newVolume / 100
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.player {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
/*flex-direction: column;*/
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--size-3);
|
||||||
|
padding: var(--size-4);
|
||||||
|
background-color: var(--stone-2);
|
||||||
|
border-top: 1px solid var(--stone-4);
|
||||||
|
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-info {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-title {
|
||||||
|
display: block;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: var(--font-size-1);
|
||||||
|
color: var(--stone-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-artist {
|
||||||
|
font-size: var(--font-size-0);
|
||||||
|
color: var(--stone-7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-controls {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--size-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn {
|
||||||
|
padding: var(--size-2);
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--stone-9);
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn:hover {
|
||||||
|
color: var(--stone-11);
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-btn {
|
||||||
|
width: var(--size-8);
|
||||||
|
height: var(--size-8);
|
||||||
|
border: none;
|
||||||
|
background: var(--stone-9);
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--stone-1);
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-btn:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-progress {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--size-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
font-size: var(--font-size-0);
|
||||||
|
color: var(--stone-7);
|
||||||
|
min-width: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
flex: 1;
|
||||||
|
height: 4px;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
background: var(--stone-4);
|
||||||
|
border-radius: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
background: var(--stone-9);
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-volume {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--size-2);
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.volume-btn {
|
||||||
|
padding: var(--size-1);
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--stone-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.volume-slider {
|
||||||
|
width: 80px;
|
||||||
|
height: 4px;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
background: var(--stone-4);
|
||||||
|
border-radius: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.volume-slider::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
background: var(--stone-9);
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
11
web/src/stores/player.js
Normal file
11
web/src/stores/player.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export const usePlayerStore = defineStore('counter', () => {
|
||||||
|
const currentTime = ref(0)
|
||||||
|
const duration = ref(0)
|
||||||
|
const volume = ref(80)
|
||||||
|
const progress = computed(() => (currentTime.value / (duration.value + 1)) * 100)
|
||||||
|
|
||||||
|
return { currentTime, duration, progress, volume }
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user