diff --git a/src/web/api/favorites.ts b/src/web/api/favorites.ts index 08fc292..0282c6b 100644 --- a/src/web/api/favorites.ts +++ b/src/web/api/favorites.ts @@ -15,7 +15,7 @@ export function createFavoritesRouter(database: BotDatabase, logger: Logger): Ro // POST /api/favorites — 添加收藏 router.post("/", (req, res) => { const userId = req.user!.id; - const { platform, playlistId, name, coverUrl, songCount } = req.body; + const { platform, playlistId, name, coverUrl, songCount } = req.body ?? {}; if (!platform || !playlistId || !name) { res.status(400).json({ error: "platform, playlistId, name are required" }); return; @@ -64,11 +64,11 @@ export function createFavoritesRouter(database: BotDatabase, logger: Logger): Ro router.get("/check", (req, res) => { const userId = req.user!.id; const { platform, playlistId } = req.query; - if (!platform || !playlistId) { + if (typeof platform !== "string" || typeof playlistId !== "string") { res.status(400).json({ error: "platform and playlistId required" }); return; } - const favorited = database.isFavorited(userId, playlistId as string, platform as string); + const favorited = database.isFavorited(userId, playlistId, platform); res.json({ favorited }); }); diff --git a/web/src/App.vue b/web/src/App.vue index 3781225..5b3e1b0 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -143,6 +143,10 @@ function cycleMobileMode() { onMounted(async () => { playerStore.loadTheme(); connect(); + // Hydrate favorites once per session so deep-links / hard refreshes onto + // Search or Playlist render hearts correctly without first visiting Home. + // (fire-and-forget; fetchFavorites swallows the 401 when not yet logged in.) + playerStore.fetchFavorites(); syncTimer = setInterval(() => playerStore.syncElapsed(), 3000); mobileRaf = requestAnimationFrame(updateMobileProgress); // Reconcile the dedicated-link scope only after the bot list is known: the diff --git a/web/src/stores/player.ts b/web/src/stores/player.ts index 316e03f..909b8bd 100644 --- a/web/src/stores/player.ts +++ b/web/src/stores/player.ts @@ -473,15 +473,33 @@ export const usePlayerStore = defineStore('player', { }, async addFavorite(playlist: { platform: string; playlistId: string; name: string; coverUrl: string; songCount: number }) { - await axios.post('/api/favorites', playlist); - await this.fetchFavorites(); - this.notify('已收藏', 'info'); + try { + await axios.post('/api/favorites', playlist); + await this.fetchFavorites(); + this.notify('已收藏', 'info'); + } catch (err: any) { + // 409 = already favorited (e.g. stale heart); just resync so the UI converges. + if (err?.response?.status === 409) { + await this.fetchFavorites(); + return; + } + this.notify('收藏失败', 'error'); + } }, async removeFavorite(id: number) { - await axios.delete(`/api/favorites/${id}`); - await this.fetchFavorites(); - this.notify('已取消收藏', 'info'); + try { + await axios.delete(`/api/favorites/${id}`); + await this.fetchFavorites(); + this.notify('已取消收藏', 'info'); + } catch (err: any) { + // 404 = already gone; resync. Otherwise report failure. + if (err?.response?.status === 404) { + await this.fetchFavorites(); + return; + } + this.notify('取消收藏失败', 'error'); + } }, isFavorited(playlistId: string, platform: string): boolean { @@ -505,6 +523,10 @@ export const usePlayerStore = defineStore('player', { this.authStatus.netease = newAuth.netease; this.authStatus.qq = newAuth.qq; + // Favorites are user-local and cheap; always refresh them, even on a + // home-data cache hit, so hearts stay correct across tabs/sessions. + this.fetchFavorites(); + // Cache hit only if auth is unchanged AND within TTL. if ( !authChanged && @@ -575,9 +597,6 @@ export const usePlayerStore = defineStore('player', { if (authOk) { this.lastFetchTime = Date.now(); } - - // Fetch favorites in parallel — not cached by TTL; always fresh - this.fetchFavorites(); }, }, });