feat(播放队列): 添加播放队列功能并集成 Pinia 状态管理

新增播放队列功能,使用 Pini a进行状态管理。包括创建播放队列存储、在专辑详情页添加播放功能、以及在播放组件中监听播放状态的变化。同时,更新了依赖以支持 Pinia 的集成。
This commit is contained in:
Astrian Zheng 2025-05-24 16:33:50 +10:00
parent 202e7951f3
commit 94a153ae11
Signed by: Astrian
SSH Key Fingerprint: SHA256:rVnhx3DAKjujCwWE13aDl7uV6+9U1MvydLkNRXJrBiA
7 changed files with 194 additions and 3 deletions

136
package-lock.json generated
View File

@ -10,6 +10,7 @@
"dependencies": { "dependencies": {
"@tailwindcss/vite": "^4.1.7", "@tailwindcss/vite": "^4.1.7",
"axios": "^1.9.0", "axios": "^1.9.0",
"pinia": "^3.0.2",
"tailwindcss": "^4.1.7", "tailwindcss": "^4.1.7",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-router": "^4.5.1" "vue-router": "^4.5.1"
@ -1354,6 +1355,30 @@
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@vue/devtools-kit": {
"version": "7.7.6",
"resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.6.tgz",
"integrity": "sha512-geu7ds7tem2Y7Wz+WgbnbZ6T5eadOvozHZ23Atk/8tksHMFOFylKi1xgGlQlVn0wlkEf4hu+vd5ctj1G4kFtwA==",
"license": "MIT",
"dependencies": {
"@vue/devtools-shared": "^7.7.6",
"birpc": "^2.3.0",
"hookable": "^5.5.3",
"mitt": "^3.0.1",
"perfect-debounce": "^1.0.0",
"speakingurl": "^14.0.1",
"superjson": "^2.2.2"
}
},
"node_modules/@vue/devtools-shared": {
"version": "7.7.6",
"resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.6.tgz",
"integrity": "sha512-yFEgJZ/WblEsojQQceuyK6FzpFDx4kqrz2ohInxNj5/DnhoX023upTv4OD6lNPLAA5LLkbwPVb10o/7b+Y4FVA==",
"license": "MIT",
"dependencies": {
"rfdc": "^1.4.1"
}
},
"node_modules/@vue/language-core": { "node_modules/@vue/language-core": {
"version": "2.2.10", "version": "2.2.10",
"resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.10.tgz", "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.10.tgz",
@ -1460,6 +1485,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/birpc": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/birpc/-/birpc-2.3.0.tgz",
"integrity": "sha512-ijbtkn/F3Pvzb6jHypHRyve2QApOCZDR25D/VnkY2G/lBNcXCTsnsCxgY4k4PkVB7zfwzYbY3O9Lcqe3xufS5g==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
@ -1504,6 +1538,21 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/copy-anything": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz",
"integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==",
"license": "MIT",
"dependencies": {
"is-what": "^4.1.8"
},
"engines": {
"node": ">=12.13"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/csstype": { "node_modules/csstype": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
@ -1841,6 +1890,24 @@
"he": "bin/he" "he": "bin/he"
} }
}, },
"node_modules/hookable": {
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
"license": "MIT"
},
"node_modules/is-what": {
"version": "4.1.16",
"resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz",
"integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==",
"license": "MIT",
"engines": {
"node": ">=12.13"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/jiti": { "node_modules/jiti": {
"version": "2.4.2", "version": "2.4.2",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz",
@ -2154,6 +2221,12 @@
"node": ">= 18" "node": ">= 18"
} }
}, },
"node_modules/mitt": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
"license": "MIT"
},
"node_modules/mkdirp": { "node_modules/mkdirp": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
@ -2201,6 +2274,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/perfect-debounce": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
"license": "MIT"
},
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@ -2219,6 +2298,36 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/pinia": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.2.tgz",
"integrity": "sha512-sH2JK3wNY809JOeiiURUR0wehJ9/gd9qFN2Y828jCbxEzKEmEt0pzCXwqiSTfuRsK9vQsOflSdnbdBOGrhtn+g==",
"license": "MIT",
"dependencies": {
"@vue/devtools-api": "^7.7.2"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"typescript": ">=4.4.4",
"vue": "^2.7.0 || ^3.5.11"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/pinia/node_modules/@vue/devtools-api": {
"version": "7.7.6",
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.6.tgz",
"integrity": "sha512-b2Xx0KvXZObePpXPYHvBRRJLDQn5nhKjXh7vUhMEtWxz1AYNFOVIsh5+HLP8xDGL7sy+Q7hXeUxPHB/KgbtsPw==",
"license": "MIT",
"dependencies": {
"@vue/devtools-kit": "^7.7.6"
}
},
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.3", "version": "8.5.3",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
@ -2253,6 +2362,12 @@
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/rfdc": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
"license": "MIT"
},
"node_modules/rollup": { "node_modules/rollup": {
"version": "4.41.0", "version": "4.41.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.0.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.0.tgz",
@ -2301,6 +2416,27 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/speakingurl": {
"version": "14.0.1",
"resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz",
"integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/superjson": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.2.tgz",
"integrity": "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==",
"license": "MIT",
"dependencies": {
"copy-anything": "^3.0.2"
},
"engines": {
"node": ">=16"
}
},
"node_modules/tailwindcss": { "node_modules/tailwindcss": {
"version": "4.1.7", "version": "4.1.7",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.7.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.7.tgz",

View File

@ -15,6 +15,7 @@
"dependencies": { "dependencies": {
"@tailwindcss/vite": "^4.1.7", "@tailwindcss/vite": "^4.1.7",
"axios": "^1.9.0", "axios": "^1.9.0",
"pinia": "^3.0.2",
"tailwindcss": "^4.1.7", "tailwindcss": "^4.1.7",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-router": "^4.5.1" "vue-router": "^4.5.1"

View File

@ -1,3 +1,14 @@
<script setup lang="ts">
import { usePlayQueueStore } from '../stores/usePlayQueueStore'
import { watch } from 'vue'
const playQueue = usePlayQueueStore()
watch(() => playQueue.isPlaying, (newValue) => {
console.log(newValue)
})
</script>
<template> <template>
<div class="bg-[#070909a7] backdrop-blur-3xl h-24 shadow-[0px_-1px_1rem_0px_rgba(0,0,0,0.1)] border-t border-t-[#1c1c1c] sticky bottom-0 w-full"> <div class="bg-[#070909a7] backdrop-blur-3xl h-24 shadow-[0px_-1px_1rem_0px_rgba(0,0,0,0.1)] border-t border-t-[#1c1c1c] sticky bottom-0 w-full">

View File

@ -1,6 +1,8 @@
import { createApp } from 'vue' import { createApp } from 'vue'
import { createWebHashHistory, createRouter } from 'vue-router' import { createWebHashHistory, createRouter } from 'vue-router'
import './style.css' import './style.css'
import { createPinia } from 'pinia'
import App from './App.vue' import App from './App.vue'
import HomePage from './pages/Home.vue' import HomePage from './pages/Home.vue'
import AlbumDetailView from './pages/AlbumDetail.vue' import AlbumDetailView from './pages/AlbumDetail.vue'
@ -15,4 +17,7 @@ const router = createRouter({
routes routes
}) })
; createApp(App).use(router).mount('#app') const pinia = createPinia()
createApp(App).use(router).use(pinia).mount('#app')

View File

@ -2,17 +2,19 @@
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import apis from '../apis' import apis from '../apis'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { usePlayQueueStore } from '../stores/usePlayQueueStore'
const album = ref<Album>() const album = ref<Album>()
const route = useRoute() const route = useRoute()
const albumId = route.params.albumId const albumId = route.params.albumId
const playQueue = usePlayQueueStore()
onMounted(async () => { onMounted(async () => {
try { try {
const res = await apis.getAlbum(albumId as string) const res = await apis.getAlbum(albumId as string)
album.value = res album.value = res
console.log(res)
} catch (error) { } catch (error) {
console.log(error) console.log(error)
} }
@ -24,6 +26,24 @@ function artistsOrganize(list: string[]) {
return artist return artist
}).join(' / ') }).join(' / ')
} }
function playTheAlbum() {
if (playQueue.queueReplaceLock) {
if (!confirm("当前操作会将你的待播列表清空、放入这张专辑所有曲目,并从新待播清单的开头播放。继续吗?")) { return }
playQueue.queueReplaceLock = false
}
let newPlayQueue = []
for (const track of album.value?.songs ?? []) {
newPlayQueue.push({
song: track,
album: album.value
})
}
playQueue.list = newPlayQueue
playQueue.currentIndex = 0
playQueue.isPlaying = true
}
</script> </script>
<template> <template>
@ -46,7 +66,9 @@ function artistsOrganize(list: string[]) {
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<div class="flex gap-2"> <div class="flex gap-2">
<button <button
class="bg-sky-500/20 hover:bg-sky-500/30 active:bg-sky-600/30 active:shadow-inner border border-[#ffffff39] rounded-full w-56 h-10 text-base text-white flex justify-center items-center gap-2"> class="bg-sky-500/20 hover:bg-sky-500/30 active:bg-sky-600/30 active:shadow-inner border border-[#ffffff39] rounded-full w-56 h-10 text-base text-white flex justify-center items-center gap-2"
@click="playTheAlbum"
>
<div class="w-4 h-4"> <div class="w-4 h-4">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path <path

View File

@ -0,0 +1,11 @@
import { defineStore } from "pinia"
import { ref } from "vue"
export const usePlayQueueStore = defineStore('queue', () =>{
const list = ref<QueueItem[]>([])
const currentIndex = ref<number>(0)
const isPlaying = ref<boolean>(false)
const queueReplaceLock = ref<boolean>(false)
return { list, currentIndex, isPlaying, queueReplaceLock }
})

5
src/vite-env.d.ts vendored
View File

@ -32,4 +32,9 @@ interface ApiResponse {
code: number code: number
msg: string msg: string
data: unknown data: unknown
}
interface QueueItem {
song: Song
album?: Album
} }