feat(Player, Playroom): add volume control functionality with persistent settings
This commit is contained in:
		
							parent
							
								
									1cecf52dfd
								
							
						
					
					
						commit
						b9a8ca7ae4
					
				| 
						 | 
				
			
			@ -236,6 +236,11 @@ watch(() => playQueueStore.list.length, async (newLength) => {
 | 
			
		|||
	setTimeout(() => {
 | 
			
		||||
		playQueueStore.preloadNext()
 | 
			
		||||
	}, 2000)
 | 
			
		||||
 | 
			
		||||
	// 初始化音量
 | 
			
		||||
	if (player.value) {
 | 
			
		||||
		initializeVolume()
 | 
			
		||||
	}
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// 监听音频元素变化
 | 
			
		||||
| 
						 | 
				
			
			@ -303,6 +308,48 @@ function getCurrentTrack() {
 | 
			
		|||
	return currentTrack.value
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 初始化音量
 | 
			
		||||
function initializeVolume() {
 | 
			
		||||
	if (player.value) {
 | 
			
		||||
		const savedVolume = localStorage.getItem('audioVolume')
 | 
			
		||||
		if (savedVolume) {
 | 
			
		||||
			const volumeValue = parseFloat(savedVolume)
 | 
			
		||||
			player.value.volume = volumeValue
 | 
			
		||||
			console.log('[Player] 初始化音量:', volumeValue)
 | 
			
		||||
		} else {
 | 
			
		||||
			// 设置默认音量
 | 
			
		||||
			player.value.volume = 1
 | 
			
		||||
			localStorage.setItem('audioVolume', '1')
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 监听音量变化事件
 | 
			
		||||
function handleVolumeChange(event: Event) {
 | 
			
		||||
	const target = event.target as HTMLAudioElement
 | 
			
		||||
	if (target) {
 | 
			
		||||
		// 保存音量变化到localStorage
 | 
			
		||||
		localStorage.setItem('audioVolume', target.volume.toString())
 | 
			
		||||
		console.log('[Player] 音量变化:', target.volume)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 监听localStorage中音量的变化,同步到音频元素
 | 
			
		||||
function syncVolumeFromStorage() {
 | 
			
		||||
	if (player.value) {
 | 
			
		||||
		const savedVolume = localStorage.getItem('audioVolume')
 | 
			
		||||
		if (savedVolume) {
 | 
			
		||||
			const volumeValue = parseFloat(savedVolume)
 | 
			
		||||
			if (player.value.volume !== volumeValue) {
 | 
			
		||||
				player.value.volume = volumeValue
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 定期检查音量同步(可选,或者使用storage事件)
 | 
			
		||||
setInterval(syncVolumeFromStorage, 100)
 | 
			
		||||
 | 
			
		||||
// 组件卸载时清理预加载
 | 
			
		||||
// onUnmounted(() => {
 | 
			
		||||
//   playQueueStore.clearAllPreloadedAudio()
 | 
			
		||||
| 
						 | 
				
			
			@ -312,16 +359,18 @@ function getCurrentTrack() {
 | 
			
		|||
<template>
 | 
			
		||||
	<div>
 | 
			
		||||
		<audio :src="currentAudioSrc" ref="playerRef" :autoplay="playQueueStore.isPlaying"
 | 
			
		||||
			v-if="playQueueStore.list.length !== 0" @ended="() => {
 | 
			
		||||
			v-if="playQueueStore.list.length !== 0" @volumechange="handleVolumeChange" @ended="() => {
 | 
			
		||||
				if (playQueueStore.playMode.repeat === 'single') { playQueueStore.isPlaying = true }
 | 
			
		||||
				else { playNext() }
 | 
			
		||||
			}" @pause="playQueueStore.isPlaying = false" @play="playQueueStore.isPlaying = true" @playing="() => {
 | 
			
		||||
				console.log('[Player] 音频开始播放事件')
 | 
			
		||||
				playQueueStore.isBuffering = false
 | 
			
		||||
				setMetadata()
 | 
			
		||||
				initializeVolume()
 | 
			
		||||
			}" @waiting="playQueueStore.isBuffering = true" @loadeddata="() => {
 | 
			
		||||
				console.log('[Player] 音频数据加载完成')
 | 
			
		||||
				playQueueStore.isBuffering = false
 | 
			
		||||
				initializeVolume()
 | 
			
		||||
			}" @canplay="() => {
 | 
			
		||||
				console.log('[Player] 音频可以播放')
 | 
			
		||||
				playQueueStore.isBuffering = false
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -47,11 +47,15 @@ const songInfo = useTemplateRef('songInfo')
 | 
			
		|||
const moreOptionsDialog = useTemplateRef('moreOptionsDialog')
 | 
			
		||||
 | 
			
		||||
const playButton = useTemplateRef('playButton')
 | 
			
		||||
const volumeSliderThumb = useTemplateRef('volumeSliderThumb')
 | 
			
		||||
const volumeSliderContainer = useTemplateRef('volumeSliderContainer')
 | 
			
		||||
 | 
			
		||||
const presentQueueListDialog = ref(false)
 | 
			
		||||
const presentLyrics = ref(false)
 | 
			
		||||
const showLyricsTooltip = ref(false)
 | 
			
		||||
const showMoreOptions = ref(false)
 | 
			
		||||
const presentVolumeControl = ref(false)
 | 
			
		||||
const volume = ref(1) // 音量值 0-1
 | 
			
		||||
 | 
			
		||||
import PlayQueueItem from '../components/PlayQueueItem.vue'
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -66,6 +70,16 @@ onMounted(async () => {
 | 
			
		|||
			playQueueStore.updatedCurrentTime = newTime
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	// 等待DOM完全渲染后再初始化拖拽
 | 
			
		||||
	await nextTick()
 | 
			
		||||
 | 
			
		||||
	// 初始化音量从localStorage或默认值
 | 
			
		||||
	const savedVolume = localStorage.getItem('audioVolume')
 | 
			
		||||
	if (savedVolume) {
 | 
			
		||||
		volume.value = parseFloat(savedVolume)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	thumbUpdate()
 | 
			
		||||
 | 
			
		||||
	setupEntranceAnimations()
 | 
			
		||||
| 
						 | 
				
			
			@ -93,6 +107,71 @@ function thumbUpdate() {
 | 
			
		|||
	gsap.to(progressBarThumb.value, { x: newPosition, duration: 0.1 })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function volumeThumbUpdate() {
 | 
			
		||||
	const containerWidth = volumeSliderContainer.value?.clientWidth || 0
 | 
			
		||||
	const thumbWidth = volumeSliderThumb.value?.clientWidth || 0
 | 
			
		||||
	const newPosition = (containerWidth - thumbWidth) * volume.value
 | 
			
		||||
	gsap.to(volumeSliderThumb.value, { x: newPosition, duration: 0.1 })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function toggleVolumeControl() {
 | 
			
		||||
	if (!presentVolumeControl.value) {
 | 
			
		||||
		presentVolumeControl.value = true
 | 
			
		||||
		nextTick(() => {
 | 
			
		||||
			// 在音量控制显示后再创建Draggable
 | 
			
		||||
			createVolumeDraggable()
 | 
			
		||||
			volumeThumbUpdate()
 | 
			
		||||
		})
 | 
			
		||||
	} else {
 | 
			
		||||
		presentVolumeControl.value = false
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function createVolumeDraggable() {
 | 
			
		||||
	if (!volumeSliderThumb.value || !volumeSliderContainer.value) {
 | 
			
		||||
		console.warn('Volume slider elements not found')
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 确保容器有宽度
 | 
			
		||||
	const containerWidth = volumeSliderContainer.value.clientWidth
 | 
			
		||||
	if (containerWidth === 0) {
 | 
			
		||||
		console.warn('Volume slider container has no width')
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 创建音量滑杆的 Draggable
 | 
			
		||||
	Draggable.create(volumeSliderThumb.value, {
 | 
			
		||||
		type: 'x',
 | 
			
		||||
		bounds: volumeSliderContainer.value,
 | 
			
		||||
		onDrag: function () {
 | 
			
		||||
			const thumbPosition = this.x
 | 
			
		||||
			const containerWidth = volumeSliderContainer.value?.clientWidth || 0
 | 
			
		||||
			const thumbWidth = volumeSliderThumb.value?.clientWidth || 0
 | 
			
		||||
			// 确保音量值在0-1之间
 | 
			
		||||
			const newVolume = Math.max(0, Math.min(1, thumbPosition / (containerWidth - thumbWidth)))
 | 
			
		||||
			volume.value = newVolume
 | 
			
		||||
			updateAudioVolume()
 | 
			
		||||
			// 保存音量到localStorage
 | 
			
		||||
			localStorage.setItem('audioVolume', newVolume.toString())
 | 
			
		||||
		},
 | 
			
		||||
		onDragEnd: function () {
 | 
			
		||||
			// 拖拽结束时也保存一次
 | 
			
		||||
			localStorage.setItem('audioVolume', volume.value.toString())
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	console.log('Volume draggable created successfully')
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function updateAudioVolume() {
 | 
			
		||||
	// 通过事件或直接访问音频元素来设置音量
 | 
			
		||||
	const audioElement = document.querySelector('audio')
 | 
			
		||||
	if (audioElement) {
 | 
			
		||||
		audioElement.volume = volume.value
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function formatDetector() {
 | 
			
		||||
	const format = playQueueStore.list[playQueueStore.currentIndex].song.sourceUrl?.split('.').pop()
 | 
			
		||||
	if (format === 'mp3') { return 'MP3' }
 | 
			
		||||
| 
						 | 
				
			
			@ -450,17 +529,26 @@ watch(() => playQueueStore.currentIndex, () => {
 | 
			
		|||
				<!-- Control buttons -->
 | 
			
		||||
				<div class="w-full flex justify-between items-center" ref="controlButtons">
 | 
			
		||||
					<div class="flex-1 text-left flex gap-1">
 | 
			
		||||
						<button
 | 
			
		||||
							class="h-8 w-8 flex justify-center items-center rounded-full hover:bg-white/25 transition-all duration-200 hover:scale-110"
 | 
			
		||||
							ref="volumeButton">
 | 
			
		||||
						<button class="h-8 w-8 flex justify-center items-center rounded-full hover:bg-white/25 relative"
 | 
			
		||||
							ref="volumeButton" @click="toggleVolumeControl">
 | 
			
		||||
							<div class="w-6 h-6 relative">
 | 
			
		||||
								<span class="text-black blur-md absolute top-0 left-0">
 | 
			
		||||
								<span class="text-black blur-md absolute top-0 left-0 transition-all duration-200 hover:scale-110">
 | 
			
		||||
									<SpeakerIcon :size="6" />
 | 
			
		||||
								</span>
 | 
			
		||||
								<span class="text-white absolute top-0 left-0">
 | 
			
		||||
								<span class="text-white absolute top-0 left-0 transition-all duration-200 hover:scale-110">
 | 
			
		||||
									<SpeakerIcon :size="6" />
 | 
			
		||||
								</span>
 | 
			
		||||
							</div>
 | 
			
		||||
 | 
			
		||||
							<transition name="volume-control-fade">
 | 
			
		||||
								<div v-if="presentVolumeControl" @click.stop
 | 
			
		||||
									class="absolute bg-black/60 backdrop-blur-3xl rounded-md shadow-2xl border border-[#ffffff39] w-64 h-10 bottom-10 left-[-0.3rem] flex items-center justify-between px-4 z-50">
 | 
			
		||||
									<div class="w-full py-[0.125rem] px-[0.25rem] bg-white/20 rounded-full" ref="volumeSliderContainer">
 | 
			
		||||
										<div class="w-2 h-2 bg-white rounded-full shadow-md cursor-pointer" ref="volumeSliderThumb" />
 | 
			
		||||
									</div>
 | 
			
		||||
									<!-- <span class="text-white text-xs w-8 text-right flex-shrink-0">{{ Math.round(volume * 100) }}%</span> -->
 | 
			
		||||
								</div>
 | 
			
		||||
							</transition>
 | 
			
		||||
						</button>
 | 
			
		||||
						<button
 | 
			
		||||
							class="text-white h-8 w-8 flex justify-center items-center rounded-full hover:bg-white/25 transition-all duration-200 hover:scale-110"
 | 
			
		||||
| 
						 | 
				
			
			@ -681,4 +769,21 @@ watch(() => playQueueStore.currentIndex, () => {
 | 
			
		|||
	opacity: 1;
 | 
			
		||||
	transform: translateY(0) scale(1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.volume-control-fade-enter-active,
 | 
			
		||||
.volume-control-fade-leave-active {
 | 
			
		||||
	transition: opacity 0.2s cubic-bezier(.4, 0, .2, 1), transform 0.2s cubic-bezier(.4, 0, .2, 1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.volume-control-fade-enter-from,
 | 
			
		||||
.volume-control-fade-leave-to {
 | 
			
		||||
	opacity: 0;
 | 
			
		||||
	transform: translateY(10px) scale(0.95);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.volume-control-fade-enter-to,
 | 
			
		||||
.volume-control-fade-leave-from {
 | 
			
		||||
	opacity: 1;
 | 
			
		||||
	transform: translateY(0) scale(1);
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user