diff --git a/docs/superpowers/specs/2026-05-06-music-source-tabs-design.md b/docs/superpowers/specs/2026-05-06-music-source-tabs-design.md new file mode 100644 index 0000000..3ac5b05 --- /dev/null +++ b/docs/superpowers/specs/2026-05-06-music-source-tabs-design.md @@ -0,0 +1,189 @@ +# Multi-Source Tabs for Recommend / User Playlists / Daily Songs + +**Date:** 2026-05-06 +**Status:** Spec — pending implementation + +## Problem + +Home 和 Library 页面的"推荐歌单 / 每日推荐 / 我的歌单"这三类内容当前硬编码只走网易云。当用户同时登录了网易云和 QQ 音乐时,无法在 Web UI 上看到 QQ 侧的对应内容、也无法切换查看。 + +## Goal + +在以下 4 个 section 上提供"网易云 / QQ"来源切换 tab,桌面端和移动端均可用: + +- `Home.vue` — 推荐歌单 +- `Home.vue` — 每日推荐 +- `Home.vue` — 我的歌单 +- `Library.vue` — 我的歌单 + +切换为纯前端动作(数据已预先 fetch),无加载闪烁。各 section 的选择独立持久化。 + +## Out of Scope + +- 私人 FM(QQ 无对应概念) +- B 站热门(独立第三来源,不属于网易/QQ 切换语义) +- 最近播放(bot 维度的播放历史,与音乐源无关) +- Library 现有的"我的收藏"段落 —— 当前调用的 `/api/music/user/liked` 端点不存在,是死代码,本次顺手移除 +- 登录状态实时同步(用户在 Settings 登录后需手动刷新 Home/Library 才能看到 QQ tab) +- Tab 排序、隐藏、拖动等高级配置 + +## Non-functional Constraints + +- 桌面端(>768px)和移动端(≤768px)布局均可用,tab 与 section title 同行排布;空间不足时允许 flex-wrap +- Tab 触控区域有效高度 ≥36px +- 现有 5 分钟 home data cache 行为保留 +- 不引入新的后端端点(后端已通过 `?platform=` 参数支持多源) + +## Architecture + +### 数据层(`web/src/stores/player.ts`) + +字段从单平台改为按 platform 切分: + +```ts +// 前 +recommendPlaylists: PlaylistItem[] +userPlaylists: PlaylistItem[] +dailySongs: Song[] + +// 后 +recommendPlaylists: { netease: PlaylistItem[]; qq: PlaylistItem[] } +userPlaylists: { netease: PlaylistItem[]; qq: PlaylistItem[] } +dailySongs: { netease: Song[]; qq: Song[] } + +// 新增 +authStatus: { netease: boolean; qq: boolean } +``` + +`fetchHomeData()` 改写: + +1. 并发调用 `/api/auth/status?platform=netease` 与 `?platform=qq`,写入 `authStatus` +2. 网易云的三类数据照常 fetch(推荐歌单匿名可访问;每日推荐和我的歌单需登录,未登录时 API 自然返回空或失败,`Promise.allSettled` 已隔离) +3. QQ 的三类数据**仅在 QQ 登录时** fetch,未登录则为空数组 +4. B 站热门保持原样 +5. 5 分钟缓存 TTL 不变 + +### UI 组件 + +新增 `web/src/components/SourceTabs.vue`: + +```vue + +``` + +Props: +- `sources: ('netease' | 'qq')[]` — 由父组件根据 auth 状态过滤后传入 +- `modelValue: 'netease' | 'qq'` — v-model 绑定 + +行为: +- `sources.length < 2` 时组件**自身不渲染**(返回空),父组件无需 v-if 包装 +- 文字标签:`{ netease: '网易云', qq: 'QQ' }` +- 视觉:水平排列,激活态用主色(`var(--color-primary)`)下划线 + 加粗,未激活态使用次要文字色 +- 紧贴 section-title 右侧,使用 `display: inline-flex`,移动端 padding/font-size 缩小 + +### 各 section 接入模板 + +```vue +
+

+ 推荐歌单 + +

+
+ + +
{{ pl.name }}
+
+
+
+``` + +每个 section 在 `