feat: add QQ Music API integration, Pinia home cache, and unified search

- Start @sansenjian/qq-music-api server via dynamic import in api-server.ts
- Rewrite QQ Music provider to use real API endpoints (getSearchByKey,
  getMusicPlay, getSongListDetail, getAlbumInfo, getLyric, etc.)
- Cache home page data (recommend playlists, daily songs, user playlists)
  in Pinia store with 5-minute TTL to avoid refetching on every mount
- Add unified /api/music/search/all endpoint that queries both Netease
  and QQ providers in parallel and returns merged results
- Remove platform toggle from Search.vue; search both platforms at once
- Add platform badge (网易云 / QQ) to SongCard component

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
saopig1andClaude Opus 4.6 committed 2026-03-30 02:31:15 +08:00
1 parent ce6fc92780
commit 94a0df8391
8 files changed
+195 -155

No files matched your search

+3 -41
View File
@@ -16,22 +16,6 @@
autofocus
/>
</div>
<div class="platform-toggle">
<button
class="platform-btn"
:class="{ active: platform === 'netease' }"
@click="platform = 'netease'; doSearch()"
>
网易云
</button>
<button
class="platform-btn"
:class="{ active: platform === 'qq' }"
@click="platform = 'qq'; doSearch()"
>
QQ音乐
</button>
</div>
</div>
<div v-if="loading" class="loading">搜索中...</div>
@@ -39,7 +23,7 @@
<div v-else-if="results.length > 0" class="results">
<SongCard
v-for="(song, i) in results"
:key="song.id"
:key="`${song.platform}-${song.id}`"
:song="song"
:index="i + 1"
:active="store.currentSong?.id === song.id"
@@ -66,7 +50,6 @@ const store = usePlayerStore();
const route = useRoute();
const query = ref((route.query.q as string) || '');
const platform = ref<'netease' | 'qq'>('netease');
const results = ref<Array<{ id: string; name: string; artist: string; album: string; duration: number; coverUrl: string; platform: string }>>([]);
const loading = ref(false);
const searched = ref(false);
@@ -76,8 +59,8 @@ async function doSearch() {
loading.value = true;
searched.value = true;
try {
const res = await axios.get('/api/music/search', {
params: { q: query.value, platform: platform.value },
const res = await axios.get('/api/music/search/all', {
params: { q: query.value },
});
results.value = res.data.songs;
} catch {
@@ -137,27 +120,6 @@ onMounted(() => {
}
}
.platform-toggle {
display: flex;
gap: 8px;
}
.platform-btn {
padding: 6px 18px;
border-radius: var(--radius-sm);
font-size: 13px;
font-weight: 600;
background: var(--hover-bg);
opacity: 0.6;
transition: all var(--transition-fast);
&.active {
background: var(--color-primary);
color: white;
opacity: 1;
}
}
.loading {
text-align: center;
padding: 40px;