添加播放器ui

This commit is contained in:
2026-01-08 22:06:48 +08:00
parent c7d3f1e9db
commit aae6a08850
3 changed files with 333 additions and 0 deletions

View File

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

View 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
View 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 }
})