feat: 重构首页Hero、导航菜单与页面布局统一

HomeHero:
- 重建每日推荐(左)+私人FM(右)双栏布局
- FM播放/暂停切换、不喜欢/下一首、背景流动动画、均衡器特效
- 修复FM数据获取(res.data.data双层结构)
- 歌单预加载改为hover懒加载避免502

导航优化:
- SearchBar顶部菜单: 首页/歌单/专辑/排行榜/MV/本地音乐
- 侧边栏隐藏MV和本地音乐(hideInSidebar)
- 修复搜索类型切换时失焦收起(@mousedown.prevent)

页面统一:
- 新建StickyTabPage通用布局组件(标题+吸顶tabs+内容slot)
- 歌单/专辑/MV/播客页面统一使用StickyTabPage重构
- CategorySelector第一项添加ml-0.5防scale裁切

播客优化:
- RadioCard简化去除订阅按钮、容忍radio为undefined
- 去除最近播放section、loadDashboard包含loadSubscribedRadios

i18n: 新碟上架→专辑(5语言)、新增fmTrash/fmNext(5语言)
This commit is contained in:
alger
2026-03-16 23:22:35 +08:00
parent 68b3700f3f
commit a3f91c45f0
17 changed files with 1184 additions and 1130 deletions
@@ -34,11 +34,13 @@
:cover="item.picUrl"
:title="item.name"
:subtitle="item.copywriter"
:tracks="playlistTracksMap[item.id] || []"
:tracks="isElectron ? playlistTracksMap[item.id] || [] : []"
:show-hover-tracks="isElectron"
:play-count="item.playCount"
:animation-delay="calculateAnimationDelay(index, 0.04)"
@click="handlePlaylistClick(item)"
@play="playPlaylist(item)"
@mouseenter="isElectron && loadTracksOnHover(item.id)"
/>
</div>
@@ -110,10 +112,6 @@ const fetchPlaylists = async () => {
const { data } = await getPersonalizedPlaylist(props.limit || displayCount.value + 5);
if (data.code === 200) {
playlists.value = data.result || [];
// Preload tracks for displayed playlists (Electron only)
if (isElectron) {
preloadAllTracks();
}
}
} catch (error) {
console.error('Failed to fetch playlists:', error);
@@ -122,29 +120,19 @@ const fetchPlaylists = async () => {
}
};
const preloadAllTracks = async () => {
const playlistsToLoad = displayPlaylists.value;
// Load tracks in parallel with concurrency limit
const batchSize = 4;
for (let i = 0; i < playlistsToLoad.length; i += batchSize) {
const batch = playlistsToLoad.slice(i, i + batchSize);
await Promise.all(
batch.map(async (item) => {
if (playlistTracksMap[item.id]) return;
try {
const { data } = await getListDetail(item.id);
if (data.playlist?.tracks) {
playlistTracksMap[item.id] = data.playlist.tracks.slice(0, 3).map((s: any) => ({
id: s.id,
name: s.name
}));
}
} catch (error) {
console.debug('Failed to load tracks for playlist:', item.id, error);
}
})
);
/** Lazy load tracks for a single playlist on hover */
const loadTracksOnHover = async (id: number) => {
if (playlistTracksMap[id]) return;
try {
const { data } = await getListDetail(id);
if (data.playlist?.tracks) {
playlistTracksMap[id] = data.playlist.tracks.slice(0, 3).map((s: any) => ({
id: s.id,
name: s.name
}));
}
} catch {
// silent — user can retry by hovering again
}
};