From ac55a346bece8740095466c8c466db1ed23eb046 Mon Sep 17 00:00:00 2001 From: saopig1 <4x7sw862st@gmail.com> Date: Wed, 6 May 2026 13:19:49 +0800 Subject: [PATCH] feat(web): per-platform source tabs on Home and Library MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Recommend playlists, daily songs, and user playlists on Home now show a [网易云][QQ] tab when both platforms are logged in. Library 我的歌单 gets the same tab. Selection persists per-section in localStorage and falls back gracefully when the persisted source becomes unavailable (e.g., user logged out). Removes dead 我的收藏 block from Library that referenced a non-existent /api/music/user/liked endpoint. Spec: docs/superpowers/specs/2026-05-06-music-source-tabs-design.md Co-Authored-By: Claude Opus 4.7 (1M context) --- web/src/stores/player.ts | 83 +++++++++++++++++++++++++++-------- web/src/stores/sourceTabs.ts | 36 ++++++++++++++++ web/src/views/Home.vue | 84 ++++++++++++++++++++++++++++++------ web/src/views/Library.vue | 57 ++++++++++++------------ 4 files changed, 199 insertions(+), 61 deletions(-) create mode 100644 web/src/stores/sourceTabs.ts diff --git a/web/src/stores/player.ts b/web/src/stores/player.ts index d9c0655..83a259a 100644 --- a/web/src/stores/player.ts +++ b/web/src/stores/player.ts @@ -11,6 +11,8 @@ export interface Song { platform: 'netease' | 'qq' | 'bilibili' | 'youtube'; } +export type Source = 'netease' | 'qq'; + export interface BotStatus { id: string; name: string; @@ -54,11 +56,12 @@ export const usePlayerStore = defineStore('player', { timings: {} as Record, theme: 'dark' as 'dark' | 'light', - // Home page cache - recommendPlaylists: [] as PlaylistItem[], - dailySongs: [] as Song[], - userPlaylists: [] as PlaylistItem[], + // Home page cache, split by source + recommendPlaylists: { netease: [] as PlaylistItem[], qq: [] as PlaylistItem[] }, + dailySongs: { netease: [] as Song[], qq: [] as Song[] }, + userPlaylists: { netease: [] as PlaylistItem[], qq: [] as PlaylistItem[] }, bilibiliPopular: [] as Song[], + authStatus: { netease: false, qq: false }, lastFetchTime: 0, }), @@ -354,24 +357,70 @@ export const usePlayerStore = defineStore('player', { return; } - const [playlistRes, dailyRes, userRes, biliRes] = await Promise.allSettled([ - axios.get('/api/music/recommend/playlists'), - axios.get('/api/music/recommend/songs'), - axios.get('/api/music/user/playlists'), - axios.get('/api/music/bilibili/popular?limit=12'), + // 1. Fetch auth status for both platforms first. + const [neAuthRes, qqAuthRes] = await Promise.allSettled([ + axios.get('/api/auth/status', { params: { platform: 'netease' } }), + axios.get('/api/auth/status', { params: { platform: 'qq' } }), + ]); + this.authStatus.netease = + neAuthRes.status === 'fulfilled' && !!neAuthRes.value.data?.loggedIn; + this.authStatus.qq = + qqAuthRes.status === 'fulfilled' && !!qqAuthRes.value.data?.loggedIn; + + // 2. NetEase data: recommend playlists work anonymously; daily/user + // playlists need login but Promise.allSettled isolates failures. + const neteasePromises = [ + axios.get('/api/music/recommend/playlists', { params: { platform: 'netease' } }), + axios.get('/api/music/recommend/songs', { params: { platform: 'netease' } }), + axios.get('/api/music/user/playlists', { params: { platform: 'netease' } }), + ]; + + // 3. QQ data: only fetch when QQ is logged in. When not logged in, + // resolve to empty payloads so the same indexed handling works. + const emptyPlaylists = { data: { playlists: [] } }; + const emptySongs = { data: { songs: [] } }; + const qqPromises = this.authStatus.qq + ? [ + axios.get('/api/music/recommend/playlists', { params: { platform: 'qq' } }), + axios.get('/api/music/recommend/songs', { params: { platform: 'qq' } }), + axios.get('/api/music/user/playlists', { params: { platform: 'qq' } }), + ] + : [ + Promise.resolve(emptyPlaylists), + Promise.resolve(emptySongs), + Promise.resolve(emptyPlaylists), + ]; + + const biliPromise = axios.get('/api/music/bilibili/popular?limit=12'); + + const results = await Promise.allSettled([ + ...neteasePromises, + ...qqPromises, + biliPromise, ]); - if (playlistRes.status === 'fulfilled') { - this.recommendPlaylists = playlistRes.value.data.playlists; + const [neRecPL, neDaily, neUserPL, qqRecPL, qqDaily, qqUserPL, bili] = results; + + if (neRecPL.status === 'fulfilled') { + this.recommendPlaylists.netease = neRecPL.value.data.playlists ?? []; } - if (dailyRes.status === 'fulfilled') { - this.dailySongs = dailyRes.value.data.songs; + if (neDaily.status === 'fulfilled') { + this.dailySongs.netease = neDaily.value.data.songs ?? []; } - if (userRes.status === 'fulfilled') { - this.userPlaylists = userRes.value.data.playlists; + if (neUserPL.status === 'fulfilled') { + this.userPlaylists.netease = neUserPL.value.data.playlists ?? []; } - if (biliRes.status === 'fulfilled') { - this.bilibiliPopular = biliRes.value.data.songs; + if (qqRecPL.status === 'fulfilled') { + this.recommendPlaylists.qq = qqRecPL.value.data.playlists ?? []; + } + if (qqDaily.status === 'fulfilled') { + this.dailySongs.qq = qqDaily.value.data.songs ?? []; + } + if (qqUserPL.status === 'fulfilled') { + this.userPlaylists.qq = qqUserPL.value.data.playlists ?? []; + } + if (bili.status === 'fulfilled') { + this.bilibiliPopular = bili.value.data.songs ?? []; } this.lastFetchTime = Date.now(); diff --git a/web/src/stores/sourceTabs.ts b/web/src/stores/sourceTabs.ts new file mode 100644 index 0000000..652a6a8 --- /dev/null +++ b/web/src/stores/sourceTabs.ts @@ -0,0 +1,36 @@ +import type { Source } from './player.js'; + +const STORAGE_KEY = 'source-tabs'; + +export type TabKey = + | 'home.recommend' + | 'home.daily' + | 'home.user' + | 'library.user'; + +function readAll(): Partial> { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) return {}; + const parsed = JSON.parse(raw); + return typeof parsed === 'object' && parsed !== null ? parsed : {}; + } catch { + return {}; + } +} + +export function loadTabSource(key: TabKey, fallback: Source = 'netease'): Source { + const all = readAll(); + const v = all[key]; + return v === 'netease' || v === 'qq' ? v : fallback; +} + +export function saveTabSource(key: TabKey, value: Source): void { + try { + const all = readAll(); + all[key] = value; + localStorage.setItem(STORAGE_KEY, JSON.stringify(all)); + } catch { + // localStorage may be unavailable (private browsing); silently no-op + } +} diff --git a/web/src/views/Home.vue b/web/src/views/Home.vue index 969b48f..3ed7a1a 100644 --- a/web/src/views/Home.vue +++ b/web/src/views/Home.vue @@ -34,11 +34,14 @@ -
-

每日推荐

+
+

+ 每日推荐 + +

-
-

推荐歌单

+
+

+ 推荐歌单 + +

-
+

我的歌单 - {{ store.userPlaylists.length }} + {{ currentUserPlaylists.length }} +

@@ -117,19 +124,68 @@