feat(PlayQueue): implement PlayQueueItem component for better queue management and UI
This commit is contained in:
		
							parent
							
								
									f8b860adf3
								
							
						
					
					
						commit
						8eee570200
					
				
							
								
								
									
										13
									
								
								src/assets/icons/downhyphen.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/assets/icons/downhyphen.vue
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,13 @@
 | 
			
		|||
<script setup lang="ts">
 | 
			
		||||
defineProps<{
 | 
			
		||||
	size: number
 | 
			
		||||
}>()
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
	<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" :class="`w-${size} h-${size}`">
 | 
			
		||||
		<path
 | 
			
		||||
			d="M12 10.0858L7.20711 5.29291L5.79289 6.70712L12 12.9142L18.2071 6.70712L16.7929 5.29291L12 10.0858ZM18 17L6 17L6 15L18 15V17Z">
 | 
			
		||||
		</path>
 | 
			
		||||
	</svg>
 | 
			
		||||
</template>
 | 
			
		||||
							
								
								
									
										13
									
								
								src/assets/icons/uphypen.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/assets/icons/uphypen.vue
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,13 @@
 | 
			
		|||
<script setup lang="ts">
 | 
			
		||||
defineProps<{
 | 
			
		||||
	size: number
 | 
			
		||||
}>()
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
	<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" :class="`w-${size} h-${size}`">
 | 
			
		||||
		<path
 | 
			
		||||
			d="M12 13.9142L16.7929 18.7071L18.2071 17.2929L12 11.0858L5.79289 17.2929L7.20711 18.7071L12 13.9142ZM6 7L18 7V9L6 9L6 7Z">
 | 
			
		||||
		</path>
 | 
			
		||||
	</svg>
 | 
			
		||||
</template>
 | 
			
		||||
							
								
								
									
										121
									
								
								src/components/PlayQueueItem.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								src/components/PlayQueueItem.vue
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,121 @@
 | 
			
		|||
<script setup lang="ts">
 | 
			
		||||
import { usePlayQueueStore } from '../stores/usePlayQueueStore'
 | 
			
		||||
import { artistsOrganize } from '../utils'
 | 
			
		||||
 | 
			
		||||
import XIcon from '../assets/icons/x.vue'
 | 
			
		||||
import UpHyphenIcon from '../assets/icons/uphypen.vue'
 | 
			
		||||
import DownHyphenIcon from '../assets/icons/downhyphen.vue'
 | 
			
		||||
 | 
			
		||||
import { ref } from 'vue'
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	queueItem: QueueItem
 | 
			
		||||
	isCurrent: boolean
 | 
			
		||||
	index: number
 | 
			
		||||
}>()
 | 
			
		||||
 | 
			
		||||
const playQueueStore = usePlayQueueStore()
 | 
			
		||||
 | 
			
		||||
const hover = ref(false)
 | 
			
		||||
 | 
			
		||||
function removeItem() {
 | 
			
		||||
	const queue = [...playQueueStore.list]
 | 
			
		||||
 | 
			
		||||
	if (!playQueueStore.playMode.shuffle) {
 | 
			
		||||
		// 非shuffle模式:直接操作原始列表
 | 
			
		||||
		queue.splice(props.index, 1)
 | 
			
		||||
		playQueueStore.list = queue
 | 
			
		||||
 | 
			
		||||
		if (props.index < playQueueStore.currentIndex) {
 | 
			
		||||
			playQueueStore.currentIndex--
 | 
			
		||||
		} else if (props.index === playQueueStore.currentIndex) {
 | 
			
		||||
			if (queue.length > 0) {
 | 
			
		||||
				playQueueStore.currentIndex = Math.min(playQueueStore.currentIndex, queue.length - 1)
 | 
			
		||||
			} else {
 | 
			
		||||
				playQueueStore.currentIndex = 0
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		// Shuffle模式:需要同时维护两个列表
 | 
			
		||||
		const originalIndex = playQueueStore.shuffleList[props.index]
 | 
			
		||||
		const shuffleList = [...playQueueStore.shuffleList]
 | 
			
		||||
 | 
			
		||||
		// 从原始列表中删除
 | 
			
		||||
		queue.splice(originalIndex, 1)
 | 
			
		||||
 | 
			
		||||
		// 从shuffle列表中删除当前项
 | 
			
		||||
		shuffleList.splice(props.index, 1)
 | 
			
		||||
 | 
			
		||||
		// 更新shuffle列表中所有大于被删除原始索引的值
 | 
			
		||||
		for (let i = 0; i < shuffleList.length; i++) {
 | 
			
		||||
			if (shuffleList[i] > originalIndex) {
 | 
			
		||||
				shuffleList[i]--
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		playQueueStore.list = queue
 | 
			
		||||
		playQueueStore.shuffleList = shuffleList
 | 
			
		||||
 | 
			
		||||
		// 更新currentIndex
 | 
			
		||||
		if (props.index < playQueueStore.currentIndex) {
 | 
			
		||||
			playQueueStore.currentIndex--
 | 
			
		||||
		} else if (props.index === playQueueStore.currentIndex) {
 | 
			
		||||
			if (queue.length > 0) {
 | 
			
		||||
				playQueueStore.currentIndex = Math.min(playQueueStore.currentIndex, queue.length - 1)
 | 
			
		||||
			} else {
 | 
			
		||||
				playQueueStore.currentIndex = 0
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
	<button class="p-4 w-full rounded-md hover:bg-white/5 first:mt-2 flex gap-2 items-center" @click="() => {
 | 
			
		||||
		if (isCurrent) { return }
 | 
			
		||||
		playQueueStore.currentIndex = index
 | 
			
		||||
		playQueueStore.isPlaying = true
 | 
			
		||||
	}" @mouseenter="hover = true" @mouseleave="hover = false">
 | 
			
		||||
		<div class="flex gap-2 flex-auto w-0">
 | 
			
		||||
			<div class="relative w-12 h-12 rounded-md shadow-xl overflow-hidden">
 | 
			
		||||
				<img :src="queueItem.album?.coverUrl" />
 | 
			
		||||
				<div class="w-full h-full absolute top-0 left-0 bg-neutral-900/75 flex justify-center items-center"
 | 
			
		||||
					v-if="isCurrent">
 | 
			
		||||
					<div style="height: 1rem;" class="flex justify-center items-center gap-[.125rem]">
 | 
			
		||||
						<div class="bg-white w-[.125rem] rounded-full" v-for="(bar, index) in playQueueStore.visualizer"
 | 
			
		||||
							:key="index" :style="{
 | 
			
		||||
								height: `${Math.max(10, bar)}%`
 | 
			
		||||
							}" />
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="flex flex-col text-left flex-auto w-0">
 | 
			
		||||
				<div class="text-white text-base font-medium truncate">{{ queueItem.song.name }}</div>
 | 
			
		||||
				<div class="text-white/75 text-sm truncate">
 | 
			
		||||
					{{ artistsOrganize(queueItem.song.artists ?? []) }} —
 | 
			
		||||
					{{ queueItem.album?.name ?? '未知专辑' }}
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
 | 
			
		||||
		<div class="flex gap-1" v-if="hover">
 | 
			
		||||
			<button
 | 
			
		||||
				class="text-white/90 w-4 h-4 hover:scale-110 hover:text-white active:scale-95 active:text-white/85 transition-all"
 | 
			
		||||
				@click.stop>
 | 
			
		||||
				<UpHyphenIcon :size="4" />
 | 
			
		||||
			</button>
 | 
			
		||||
 | 
			
		||||
			<button
 | 
			
		||||
				class="text-white/90 w-4 h-4 hover:scale-110 hover:text-white active:scale-95 active:text-white/85 transition-all"
 | 
			
		||||
				@click.stop>
 | 
			
		||||
				<DownHyphenIcon :size="4" />
 | 
			
		||||
			</button>
 | 
			
		||||
 | 
			
		||||
			<button
 | 
			
		||||
				class="text-white/90 w-4 h-4 hover:scale-110 hover:text-white active:scale-95 active:text-white/85 transition-all"
 | 
			
		||||
				@click.stop="removeItem">
 | 
			
		||||
				<XIcon :size="4" />
 | 
			
		||||
			</button>
 | 
			
		||||
		</div>
 | 
			
		||||
	</button>
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			@ -45,6 +45,8 @@ const presentQueueListDialog = ref(false)
 | 
			
		|||
const presentLyrics = ref(false)
 | 
			
		||||
const showLyricsTooltip = ref(false)
 | 
			
		||||
 | 
			
		||||
import PlayQueueItem from '../components/PlayQueueItem.vue'
 | 
			
		||||
 | 
			
		||||
onMounted(async () => {
 | 
			
		||||
	Draggable.create(progressBarThumb.value, {
 | 
			
		||||
		type: 'x',
 | 
			
		||||
| 
						 | 
				
			
			@ -569,63 +571,13 @@ watch(() => playQueueStore.currentIndex, () => {
 | 
			
		|||
			<hr class="border-[#ffffff39]" />
 | 
			
		||||
 | 
			
		||||
			<div class="flex-auto h-0 overflow-y-auto px-4 flex flex-col gap-2" v-if="playQueueStore.playMode.shuffle">
 | 
			
		||||
				<button v-for="(oriIndex, shuffledIndex) in playQueueStore.shuffleList"
 | 
			
		||||
					class="p-4 w-full rounded-md hover:bg-white/5 first:mt-2" :key="oriIndex" @click="() => {
 | 
			
		||||
						if (playQueueStore.currentIndex === shuffledIndex) { return }
 | 
			
		||||
						playQueueStore.currentIndex = shuffledIndex
 | 
			
		||||
						playQueueStore.isPlaying = true
 | 
			
		||||
					}">
 | 
			
		||||
					<div class="flex gap-2">
 | 
			
		||||
						<div class="relative w-12 h-12 rounded-md shadow-xl overflow-hidden">
 | 
			
		||||
							<img :src="playQueueStore.list[oriIndex].album?.coverUrl" />
 | 
			
		||||
							<div class="w-full h-full absolute top-0 left-0 bg-neutral-900/75 flex justify-center items-center"
 | 
			
		||||
								v-if="shuffledIndex === playQueueStore.currentIndex">
 | 
			
		||||
								<div style="height: 1rem;" class="flex justify-center items-center gap-[.125rem]">
 | 
			
		||||
									<div class="bg-white w-[.125rem] rounded-full" v-for="(bar, index) in playQueueStore.visualizer"
 | 
			
		||||
										:key="index" :style="{
 | 
			
		||||
											height: `${Math.max(10, bar)}%`
 | 
			
		||||
										}" />
 | 
			
		||||
								</div>
 | 
			
		||||
							</div>
 | 
			
		||||
						</div>
 | 
			
		||||
						<div class="flex flex-col text-left flex-auto w-0">
 | 
			
		||||
							<div class="text-white text-base font-medium truncate">{{ playQueueStore.list[oriIndex].song.name }}</div>
 | 
			
		||||
							<div class="text-white/75 text-sm truncate">
 | 
			
		||||
								{{ artistsOrganize(playQueueStore.list[oriIndex].song.artists ?? []) }} —
 | 
			
		||||
								{{ playQueueStore.list[oriIndex].album?.name ?? '未知专辑' }}
 | 
			
		||||
							</div>
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
				</button>
 | 
			
		||||
				<PlayQueueItem v-for="(oriIndex, shuffledIndex) in playQueueStore.shuffleList"
 | 
			
		||||
					:queueItem="playQueueStore.list[oriIndex]" :isCurrent="playQueueStore.currentIndex === shuffledIndex"
 | 
			
		||||
					:key="playQueueStore.list[oriIndex].song.cid" :index="shuffledIndex" />
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="flex-auto h-0 overflow-y-auto px-4 flex flex-col gap-2" v-else>
 | 
			
		||||
				<button v-for="(track, index) in playQueueStore.list" class="p-4 w-full rounded-md hover:bg-white/5 first:mt-2"
 | 
			
		||||
					:key="track.song.cid" @click="() => {
 | 
			
		||||
						if (playQueueStore.currentIndex === index) { return }
 | 
			
		||||
						playQueueStore.currentIndex = index
 | 
			
		||||
						playQueueStore.isPlaying = true
 | 
			
		||||
					}">
 | 
			
		||||
					<div class="flex gap-2">
 | 
			
		||||
						<div class="relative w-12 h-12 rounded-md shadow-xl overflow-hidden">
 | 
			
		||||
							<img :src="track.album?.coverUrl" />
 | 
			
		||||
							<div class="w-full h-full absolute top-0 left-0 bg-neutral-900/75 flex justify-center items-center"
 | 
			
		||||
								v-if="index === playQueueStore.currentIndex">
 | 
			
		||||
								<div style="height: 1rem;" class="flex justify-center items-center gap-[.125rem]">
 | 
			
		||||
									<div class="bg-white w-[.125rem] rounded-full" v-for="(bar, index) in playQueueStore.visualizer"
 | 
			
		||||
										:key="index" :style="{
 | 
			
		||||
											height: `${Math.max(10, bar)}%`
 | 
			
		||||
										}" />
 | 
			
		||||
								</div>
 | 
			
		||||
							</div>
 | 
			
		||||
						</div>
 | 
			
		||||
						<div class="flex flex-col text-left flex-auto w-0">
 | 
			
		||||
							<div class="text-white text-base font-medium truncate">{{ track.song.name }}</div>
 | 
			
		||||
							<div class="text-white/75 text-sm truncate">{{ artistsOrganize(track.song.artists ?? []) }} —
 | 
			
		||||
								{{ track.album?.name ?? '未知专辑' }}
 | 
			
		||||
							</div>
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
				</button>
 | 
			
		||||
				<PlayQueueItem :queueItem="track" :isCurrent="playQueueStore.currentIndex === index"
 | 
			
		||||
					v-for="(track, index) in playQueueStore.list" :index="index" :key="track.song.cid" />
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</dialog>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue
	
	Block a user