feat(Library): add Library page and integrate favourites functionality
refactor(useFavourites): switch from chrome.storage.sync to chrome.storage.local and normalize favourites data
This commit is contained in:
parent
377a15cdad
commit
65e3520ecf
|
@ -12,7 +12,7 @@ const router = useRouter()
|
|||
|
||||
<template>
|
||||
<div class="w-screen h-screen overflow-hidden bg-[#191919]">
|
||||
<div class="flex flex-col w-full h-full overflow-y-auto pb-24">
|
||||
<div class="flex flex-col w-full h-full overflow-y-auto">
|
||||
<div class="py-8 px-4 md:px-8 w-screen bg-gradient-to-b from-[#00000080] to-transparent z-20 absolute top-0">
|
||||
<div class="flex justify-between align-center h-[2.625rem] items-center">
|
||||
<ul class="flex gap-4" v-if="(() => {
|
||||
|
|
|
@ -9,11 +9,13 @@ import App from './App.vue'
|
|||
import HomePage from './pages/Home.vue'
|
||||
import AlbumDetailView from './pages/AlbumDetail.vue'
|
||||
import Playroom from './pages/Playroom.vue'
|
||||
import Library from './pages/Library.vue'
|
||||
|
||||
const routes = [
|
||||
{ path: '/', component: HomePage },
|
||||
{ path: '/albums/:albumId', component: AlbumDetailView },
|
||||
{ path: '/playroom', component: Playroom }
|
||||
{ path: '/playroom', component: Playroom },
|
||||
{ path: '/library', component: Library }
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
|
|
97
src/pages/Library.vue
Normal file
97
src/pages/Library.vue
Normal file
|
@ -0,0 +1,97 @@
|
|||
<script setup lang="ts">
|
||||
import StarFilledIcon from '../assets/icons/starfilled.vue'
|
||||
import PlayIcon from '../assets/icons/play.vue'
|
||||
import ShuffleIcon from '../assets/icons/shuffle.vue'
|
||||
|
||||
import { useFavourites } from '../stores/useFavourites'
|
||||
import { ref } from 'vue'
|
||||
import { artistsOrganize } from '../utils'
|
||||
|
||||
const favourites = useFavourites()
|
||||
|
||||
const currentList = ref<'favourites' | number>('favourites')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-screen overflow-y-auto gap-8 select-none">
|
||||
<div class="w-96 sticky top-0 pl-8">
|
||||
<ul class="pt-26 flex flex-col gap-2">
|
||||
<li>
|
||||
<button
|
||||
class="flex gap-2 items-center w-full hover:bg-neutral-600/40 active:bg-neutral-700/50 transition-all rounded-md px-2 py-2"
|
||||
:class="currentList === 'favourites' ? 'bg-neutral-600/20' : ''" @click="currentList = 'favourites'">
|
||||
<div class="w-12 h-12 relative text-white bg-neutral-600 rounded-md shadow-md">
|
||||
<StarFilledIcon :size="6" class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2" />
|
||||
</div>
|
||||
<div class="flex flex-col text-left">
|
||||
<div class="text-white text-xl">我的星标歌曲</div>
|
||||
<div class="text-white/50 text-sm">{{ favourites.favouritesCount }} 首歌曲</div>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<div class="w-full">
|
||||
<div class="text-white/50 text-center">
|
||||
<div class="text-lg">自定义歌单功能尚未就绪</div>
|
||||
<div class="text-md">随时回来看看!</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="flex-1 mt-26">
|
||||
<div class="flex gap-4">
|
||||
<div class="w-72 h-72 rounded-md overflow-hidden shadow-2xl bg-neutral-800/20 relative">
|
||||
<img :src="favourites.favourites[0]?.album?.coverUrl"
|
||||
v-if="favourites.favouritesCount > 0 && favourites.favouritesCount < 4" />
|
||||
<div v-else-if="favourites.favouritesCount >= 4" class="grid grid-cols-2 grid-rows-2">
|
||||
<img :src="favourites.favourites[0]?.album?.coverUrl" class="w-full h-full object-cover" />
|
||||
<img :src="favourites.favourites[1]?.album?.coverUrl" class="w-full h-full object-cover" />
|
||||
<img :src="favourites.favourites[2]?.album?.coverUrl" class="w-full h-full object-cover" />
|
||||
<img :src="favourites.favourites[3]?.album?.coverUrl" class="w-full h-full object-cover" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="absolute top-0 left-0 w-full h-full bg-gradient-to-b from-neutral-800/20 to-sky-800/70 backdrop-grayscale-100">
|
||||
<StarFilledIcon class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-white" :size="32" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col justify-between">
|
||||
<div v-if="currentList === 'favourites'" class="flex flex-col gap-2">
|
||||
<div class="text-white text-4xl font-medium">我的星标歌曲</div>
|
||||
<div class="text-white/50 text-lg">{{ favourites.favouritesCount }} 首歌曲</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="bg-sky-500/20 hover:bg-sky-500/30 active:bg-sky-600/30 active:shadow-inner backdrop-blur-3xl border border-[#ffffff39] rounded-full w-56 h-10 text-base text-white flex justify-center items-center gap-2 transition-all"
|
||||
@click="">
|
||||
<PlayIcon :size="4" />
|
||||
<div>播放歌单</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="text-white w-10 h-10 bg-neutral-800/80 border border-[#ffffff39] backdrop-blur-3xl rounded-full flex justify-center items-center hover:bg-neutral-700/80 transition-all"
|
||||
@click="">
|
||||
<ShuffleIcon :size="4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex flex-col gap-2 mt-4 mr-8">
|
||||
<button v-for="item in favourites.favourites.slice().reverse()" :key="item.song.cid"
|
||||
class="text-left flex items-center p-2 hover:bg-neutral-400/10 odd:bg-neutral-400/5 rounded-md transition-all">
|
||||
<img :src="item.album?.coverUrl" class="w-12 h-12 rounded-md object-cover inline-block mr-2" />
|
||||
<div>
|
||||
<div class="text-white text-base font-medium">{{ item.song.name }}</div>
|
||||
<div class="text-white/50 text-sm">{{ item.album?.name }} - {{ artistsOrganize(item.song.artists ?? []) }}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -21,7 +21,7 @@ export const useFavourites = defineStore('favourites', () => {
|
|||
const detectAvailableAPIs = () => {
|
||||
// 检查原生 chrome API
|
||||
try {
|
||||
if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.sync) {
|
||||
if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.local) {
|
||||
storageType.value = 'chrome'
|
||||
return 'chrome'
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ export const useFavourites = defineStore('favourites', () => {
|
|||
|
||||
// 检查 window.chrome
|
||||
try {
|
||||
if (window.chrome && window.chrome.storage && window.chrome.storage.sync) {
|
||||
if (window.chrome && window.chrome.storage && window.chrome.storage.local) {
|
||||
storageType.value = 'chrome'
|
||||
return 'chrome'
|
||||
}
|
||||
|
@ -64,7 +64,7 @@ export const useFavourites = defineStore('favourites', () => {
|
|||
switch (type) {
|
||||
case 'chrome':
|
||||
return await new Promise((resolve) => {
|
||||
const api = chrome?.storage?.sync || window.chrome?.storage?.sync
|
||||
const api = chrome?.storage?.local || window.chrome?.storage?.local
|
||||
if (api) {
|
||||
api.get({ [key]: defaultValue }, (result) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
|
@ -100,7 +100,7 @@ export const useFavourites = defineStore('favourites', () => {
|
|||
switch (type) {
|
||||
case 'chrome':
|
||||
return await new Promise<void>((resolve, reject) => {
|
||||
const api = chrome?.storage?.sync || window.chrome?.storage?.sync
|
||||
const api = chrome?.storage?.local || window.chrome?.storage?.local
|
||||
if (api) {
|
||||
api.set({ [key]: value }, () => {
|
||||
if (chrome.runtime.lastError) {
|
||||
|
@ -127,17 +127,62 @@ export const useFavourites = defineStore('favourites', () => {
|
|||
}
|
||||
}
|
||||
|
||||
// 数据验证和规范化函数
|
||||
const normalizeFavourites = (data: any[]): QueueItem[] => {
|
||||
if (!Array.isArray(data)) return []
|
||||
|
||||
return data.map(item => {
|
||||
if (!item || !item.song) return null
|
||||
|
||||
// 规范化 Song 对象
|
||||
const song: Song = {
|
||||
cid: item.song.cid || '',
|
||||
name: item.song.name || '',
|
||||
albumCid: item.song.albumCid,
|
||||
sourceUrl: item.song.sourceUrl,
|
||||
lyricUrl: item.song.lyricUrl,
|
||||
mvUrl: item.song.mvUrl,
|
||||
mvCoverUrl: item.song.mvCoverUrl,
|
||||
// 确保 artistes 和 artists 是数组
|
||||
artistes: Array.isArray(item.song.artistes) ? item.song.artistes :
|
||||
typeof item.song.artistes === 'object' ? Object.values(item.song.artistes) :
|
||||
[],
|
||||
artists: Array.isArray(item.song.artists) ? item.song.artists :
|
||||
typeof item.song.artists === 'object' ? Object.values(item.song.artists) :
|
||||
[]
|
||||
}
|
||||
|
||||
// 规范化 Album 对象(如果存在)
|
||||
const album = item.album ? {
|
||||
cid: item.album.cid || '',
|
||||
name: item.album.name || '',
|
||||
intro: item.album.intro,
|
||||
belong: item.album.belong,
|
||||
coverUrl: item.album.coverUrl || '',
|
||||
coverDeUrl: item.album.coverDeUrl,
|
||||
artistes: Array.isArray(item.album.artistes) ? item.album.artistes :
|
||||
typeof item.album.artistes === 'object' ? Object.values(item.album.artistes) :
|
||||
[],
|
||||
songs: item.album.songs
|
||||
} : undefined
|
||||
|
||||
return { song, album }
|
||||
}).filter(Boolean) as QueueItem[]
|
||||
}
|
||||
|
||||
// 获取收藏列表
|
||||
const getFavourites = async () => {
|
||||
const result = await getStoredValue('favourites', defaultFavourites)
|
||||
// 确保返回的是数组
|
||||
return Array.isArray(result) ? result : defaultFavourites
|
||||
// 确保返回的是数组并进行数据规范化
|
||||
const normalizedResult = Array.isArray(result) ? normalizeFavourites(result) : defaultFavourites
|
||||
return normalizedResult
|
||||
}
|
||||
|
||||
// 保存收藏列表
|
||||
const saveFavourites = async () => {
|
||||
// 确保保存的是数组
|
||||
await setStoredValue('favourites', [...favourites.value])
|
||||
// 确保保存的是规范化的数组
|
||||
const normalizedFavourites = normalizeFavourites([...favourites.value])
|
||||
await setStoredValue('favourites', normalizedFavourites)
|
||||
}
|
||||
|
||||
// 检查歌曲是否已收藏
|
||||
|
@ -209,7 +254,7 @@ export const useFavourites = defineStore('favourites', () => {
|
|||
const initializeFavourites = async () => {
|
||||
try {
|
||||
const savedFavourites = await getFavourites()
|
||||
// 确保设置的是有效数组
|
||||
// 确保设置的是有效且规范化的数组
|
||||
favourites.value = Array.isArray(savedFavourites) ? savedFavourites : []
|
||||
isLoaded.value = true
|
||||
} catch (error) {
|
||||
|
|
Loading…
Reference in New Issue
Block a user