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