From a9adb6be36895a6b54fe7a6b7a779031f795948c Mon Sep 17 00:00:00 2001 From: alger Date: Wed, 22 Oct 2025 21:50:20 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=94=A8=E6=88=B7=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=94=B6=E8=97=8F=E4=B8=93=E8=BE=91=E5=B1=95?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/i18n/lang/en-US/user.ts | 5 + src/i18n/lang/ja-JP/user.ts | 5 + src/i18n/lang/ko-KR/user.ts | 5 + src/i18n/lang/zh-CN/user.ts | 5 + src/i18n/lang/zh-Hant/user.ts | 5 + src/renderer/api/music.ts | 12 ++ src/renderer/api/user.ts | 12 ++ src/renderer/store/modules/user.ts | 46 ++++- src/renderer/views/music/MusicListPage.vue | 79 ++++++-- src/renderer/views/user/index.vue | 212 +++++++++++++++++---- 10 files changed, 328 insertions(+), 58 deletions(-) diff --git a/src/i18n/lang/en-US/user.ts b/src/i18n/lang/en-US/user.ts index e07970f..6cf529f 100644 --- a/src/i18n/lang/en-US/user.ts +++ b/src/i18n/lang/en-US/user.ts @@ -10,6 +10,11 @@ export default { trackCount: '{count} tracks', playCount: 'Played {count} times' }, + tabs: { + created: 'Created', + favorite: 'Favorite', + album: 'Album' + }, ranking: { title: 'Listening History', playCount: '{count} times' diff --git a/src/i18n/lang/ja-JP/user.ts b/src/i18n/lang/ja-JP/user.ts index f7fcd91..c464078 100644 --- a/src/i18n/lang/ja-JP/user.ts +++ b/src/i18n/lang/ja-JP/user.ts @@ -10,6 +10,11 @@ export default { trackCount: '{count}曲', playCount: '{count}回再生' }, + tabs: { + created: '作成', + favorite: 'お気に入り', + album: 'アルバム' + }, ranking: { title: '聴取ランキング', playCount: '{count}回' diff --git a/src/i18n/lang/ko-KR/user.ts b/src/i18n/lang/ko-KR/user.ts index bd8fb47..26f07cb 100644 --- a/src/i18n/lang/ko-KR/user.ts +++ b/src/i18n/lang/ko-KR/user.ts @@ -10,6 +10,11 @@ export default { trackCount: '{count}곡', playCount: '{count}회 재생' }, + tabs: { + created: '생성', + favorite: '즐겨찾기', + album: '앨범' + }, ranking: { title: '음악 청취 순위', playCount: '{count}회' diff --git a/src/i18n/lang/zh-CN/user.ts b/src/i18n/lang/zh-CN/user.ts index 778187a..c8b18df 100644 --- a/src/i18n/lang/zh-CN/user.ts +++ b/src/i18n/lang/zh-CN/user.ts @@ -10,6 +10,11 @@ export default { trackCount: '{count}首', playCount: '播放{count}次' }, + tabs: { + created: '创建', + favorite: '收藏', + album: '专辑' + }, ranking: { title: '听歌排行', playCount: '{count}次' diff --git a/src/i18n/lang/zh-Hant/user.ts b/src/i18n/lang/zh-Hant/user.ts index 61aeaac..a858e0e 100644 --- a/src/i18n/lang/zh-Hant/user.ts +++ b/src/i18n/lang/zh-Hant/user.ts @@ -10,6 +10,11 @@ export default { trackCount: '{count}首', playCount: '播放{count}次' }, + tabs: { + created: '建立', + favorite: '收藏', + album: '專輯' + }, ranking: { title: '聽歌排行', playCount: '{count}次' diff --git a/src/renderer/api/music.ts b/src/renderer/api/music.ts index 4a928a5..34eb53f 100644 --- a/src/renderer/api/music.ts +++ b/src/renderer/api/music.ts @@ -176,3 +176,15 @@ export function subscribePlaylist(params: { t: number; id: number }) { params }); } + +/** + * 收藏/取消收藏专辑 + * @param params t: 1 收藏, 2 取消收藏; id: 专辑id + */ +export function subscribeAlbum(params: { t: number; id: number }) { + return request({ + url: '/album/sub', + method: 'post', + params + }); +} diff --git a/src/renderer/api/user.ts b/src/renderer/api/user.ts index efe5094..89896e3 100644 --- a/src/renderer/api/user.ts +++ b/src/renderer/api/user.ts @@ -72,3 +72,15 @@ export const getUserPlaylists = (params: { uid: string | number }) => { params }); }; + +// 获取已收藏专辑列表 +export const getUserAlbumSublist = (params?: { limit?: number; offset?: number }) => { + return request({ + url: '/album/sublist', + method: 'get', + params: { + limit: params?.limit || 25, + offset: params?.offset || 0 + } + }); +}; diff --git a/src/renderer/store/modules/user.ts b/src/renderer/store/modules/user.ts index 65731f1..23b3bad 100644 --- a/src/renderer/store/modules/user.ts +++ b/src/renderer/store/modules/user.ts @@ -3,6 +3,7 @@ import { ref } from 'vue'; import { logout } from '@/api/login'; import { getLikedList } from '@/api/music'; +import { getUserAlbumSublist } from '@/api/user'; import { clearLoginStatus } from '@/utils/auth'; interface UserData { @@ -27,6 +28,8 @@ export const useUserStore = defineStore('user', () => { ); const searchValue = ref(''); const searchType = ref(1); + // 收藏的专辑 ID 列表 + const collectedAlbumIds = ref>(new Set()); // 方法 const setUser = (userData: UserData) => { @@ -69,6 +72,39 @@ export const useUserStore = defineStore('user', () => { searchType.value = type; }; + // 初始化收藏的专辑列表 + const initializeCollectedAlbums = async () => { + if (!user.value || !localStorage.getItem('token')) { + collectedAlbumIds.value.clear(); + return; + } + + try { + const { data } = await getUserAlbumSublist({ limit: 1000, offset: 0 }); + const albumIds = (data?.data || []).map((album: any) => album.id); + collectedAlbumIds.value = new Set(albumIds); + console.log(`已加载 ${albumIds.length} 个收藏专辑`); + } catch (error) { + console.error('获取收藏专辑列表失败:', error); + collectedAlbumIds.value.clear(); + } + }; + + // 添加收藏专辑 + const addCollectedAlbum = (albumId: number) => { + collectedAlbumIds.value.add(albumId); + }; + + // 移除收藏专辑 + const removeCollectedAlbum = (albumId: number) => { + collectedAlbumIds.value.delete(albumId); + }; + + // 检查专辑是否已收藏 + const isAlbumCollected = (albumId: number) => { + return collectedAlbumIds.value.has(albumId); + }; + // 初始化 const initializeUser = async () => { const savedUser = getLocalStorageItem('user', null); @@ -77,6 +113,9 @@ export const useUserStore = defineStore('user', () => { // 如果用户已登录,获取收藏列表 if (localStorage.getItem('token')) { try { + // 同时初始化收藏专辑列表 + await initializeCollectedAlbums(); + const { data } = await getLikedList(savedUser.userId); return data?.ids || []; } catch (error) { @@ -94,6 +133,7 @@ export const useUserStore = defineStore('user', () => { loginType, searchValue, searchType, + collectedAlbumIds, // 方法 setUser, @@ -101,6 +141,10 @@ export const useUserStore = defineStore('user', () => { handleLogout, setSearchValue, setSearchType, - initializeUser + initializeUser, + initializeCollectedAlbums, + addCollectedAlbum, + removeCollectedAlbum, + isAlbumCollected }; }); diff --git a/src/renderer/views/music/MusicListPage.vue b/src/renderer/views/music/MusicListPage.vue index 4bb01a3..98682ec 100644 --- a/src/renderer/views/music/MusicListPage.vue +++ b/src/renderer/views/music/MusicListPage.vue @@ -153,7 +153,12 @@ object-fit="cover" /> -
+ +
+ + {{ listInfo.artist.name }} +
+
{{ listInfo.creator.nickname }}
@@ -226,12 +231,16 @@ import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRoute } from 'vue-router'; -import { subscribePlaylist, updatePlaylistTracks } from '@/api/music'; -import { getMusicDetail } from '@/api/music'; +import { + getMusicDetail, + subscribeAlbum, + subscribePlaylist, + updatePlaylistTracks +} from '@/api/music'; import PlayBottom from '@/components/common/PlayBottom.vue'; import SongItem from '@/components/common/SongItem.vue'; import { useDownload } from '@/hooks/useDownload'; -import { useMusicStore, usePlayerStore, useRecommendStore } from '@/store'; +import { useMusicStore, usePlayerStore, useRecommendStore, useUserStore } from '@/store'; import { SongResult } from '@/types/music'; import { getImgUrl, isElectron, isMobile, setAnimationClass } from '@/utils'; import { getLoginErrorMessage, hasPermission } from '@/utils/auth'; @@ -241,11 +250,13 @@ const route = useRoute(); const playerStore = usePlayerStore(); const musicStore = useMusicStore(); const recommendStore = useRecommendStore(); +const userStore = useUserStore(); const message = useMessage(); // 从路由参数或状态管理获取数据 const loading = ref(false); const isDailyRecommend = computed(() => route.query.type === 'dailyRecommend'); +const isAlbum = computed(() => route.query.type === 'album'); const name = computed(() => { if (isDailyRecommend.value) { return t('comp.recommendSinger.songlist'); // 日推的标题 @@ -333,7 +344,7 @@ onMounted(() => { }); const getCoverImgUrl = computed(() => { - const coverImgUrl = listInfo.value?.coverImgUrl; + const coverImgUrl = listInfo.value?.coverImgUrl || listInfo.value?.picUrl; if (coverImgUrl) { return coverImgUrl; } @@ -801,19 +812,29 @@ const toggleLayout = () => { localStorage.setItem('musicListLayout', isCompactLayout.value ? 'compact' : 'normal'); }; -// 初始化歌单收藏状态 +// 初始化收藏状态(支持歌单和专辑) const checkCollectionStatus = () => { - // 只有歌单类型才能收藏 - if (route.query.type === 'playlist' && listInfo.value?.id) { + const type = route.query.type as string; + + // 歌单类型的收藏检查 + if (type === 'playlist' && listInfo.value?.id) { canCollect.value = true; - // 检查是否已收藏 isCollected.value = listInfo.value.subscribed || false; - } else { + } + // 专辑类型的收藏检查 - 使用 store 判断 + else if (type === 'album' && listInfo.value?.id) { + canCollect.value = true; + // 从 userStore 中判断是否已收藏 + isCollected.value = userStore.isAlbumCollected(listInfo.value.id); + } + // 其他类型不支持收藏 + else { canCollect.value = false; + isCollected.value = false; } }; -// 切换收藏状态 +// 切换收藏状态(支持歌单和专辑) const toggleCollect = async () => { if (!listInfo.value?.id) return; @@ -823,13 +844,23 @@ const toggleCollect = async () => { return; } + const type = route.query.type as string; + try { loadingList.value = true; const tVal = isCollected.value ? 2 : 1; // 1:收藏, 2:取消收藏 - const response = await subscribePlaylist({ - t: tVal, - id: listInfo.value.id - }); + + // 根据类型调用不同的API + const response = + type === 'album' + ? await subscribeAlbum({ + t: tVal, + id: listInfo.value.id + }) + : await subscribePlaylist({ + t: tVal, + id: listInfo.value.id + }); // 假设API返回格式是 { data: { code: number, msg?: string } } const res = response.data; @@ -840,13 +871,25 @@ const toggleCollect = async () => { ? 'comp.musicList.collectSuccess' : 'comp.musicList.cancelCollectSuccess'; message.success(t(msgKey)); - // 更新歌单信息 - listInfo.value.subscribed = isCollected.value; + + // 更新收藏状态 + if (type === 'album') { + // 专辑:更新 store 中的收藏状态 + if (isCollected.value) { + userStore.addCollectedAlbum(listInfo.value.id); + } else { + userStore.removeCollectedAlbum(listInfo.value.id); + } + (listInfo.value as any).isSub = isCollected.value; + } else { + // 歌单:更新 listInfo 的状态 + listInfo.value.subscribed = isCollected.value; + } } else { throw new Error(res.msg || t('comp.musicList.operationFailed')); } } catch (error) { - console.error('收藏歌单失败:', error); + console.error(`收藏${type === 'album' ? '专辑' : '歌单'}失败:`, error); message.error(t('comp.musicList.operationFailed')); } finally { loadingList.value = false; diff --git a/src/renderer/views/user/index.vue b/src/renderer/views/user/index.vue index 1bbfb4a..f1786fa 100644 --- a/src/renderer/views/user/index.vue +++ b/src/renderer/views/user/index.vue @@ -32,41 +32,50 @@
{{ userDetail.profile.signature }}
-
-
{{ t('user.playlist.created') }}
-
- {{ t('comp.playlist.import.button') }} -
+
+ + + +
-
- -
-
- {{ item.name }} -
- {{ t('user.playlist.mine') }} +
+ +
+ +
+
+ {{ item.name }} +
+
+ {{ getItemDescription(item) }} +
+
+
-
-
@@ -114,7 +123,7 @@ import { useI18n } from 'vue-i18n'; import { useRouter } from 'vue-router'; import { getListDetail } from '@/api/list'; -import { getUserDetail, getUserPlaylist, getUserRecord } from '@/api/user'; +import { getUserAlbumSublist, getUserDetail, getUserPlaylist, getUserRecord } from '@/api/user'; import { navigateToMusicList } from '@/components/common/MusicListNavigator'; import PlayBottom from '@/components/common/PlayBottom.vue'; import SongItem from '@/components/common/SongItem.vue'; @@ -143,8 +152,70 @@ const list = ref(); const listLoading = ref(false); const message = useMessage(); +// Tab 相关 +const tabs = [ + { key: 'created', label: 'user.tabs.created' }, + { key: 'favorite', label: 'user.tabs.favorite' }, + { key: 'album', label: 'user.tabs.album' } +]; +const currentTab = ref('created'); +const albumList = ref([]); + const user = computed(() => userStore.user); +// 创建的歌单(当前用户创建的) +const createdPlaylists = computed(() => { + if (!user.value) return []; + return playList.value.filter((item) => item.creator?.userId === user.value!.userId); +}); + +// 收藏的歌单(非当前用户创建的) +const favoritePlaylists = computed(() => { + if (!user.value) return []; + return playList.value.filter((item) => item.creator?.userId !== user.value!.userId); +}); + +// 当前显示的列表(根据 tab 切换) +const currentList = computed(() => { + switch (currentTab.value) { + case 'created': + return createdPlaylists.value; + case 'favorite': + return favoritePlaylists.value; + case 'album': + return albumList.value; + default: + return []; + } +}); + +// 获取封面图片 URL +const getCoverUrl = (item: any) => { + return item.coverImgUrl || item.picUrl || ''; +}; + +// 获取列表项描述 +const getItemDescription = (item: any) => { + if (currentTab.value === 'album') { + // 专辑:显示艺术家和歌曲数量 + const artist = item.artist?.name || ''; + const size = item.size ? ` · ${item.size}首` : ''; + return `${artist}${size}`; + } else { + // 歌单:显示曲目数和播放量 + return `${t('user.playlist.trackCount', { count: item.trackCount })},${t('user.playlist.playCount', { count: item.playCount })}`; + } +}; + +// 统一处理列表项点击 +const handleItemClick = (item: any) => { + if (currentTab.value === 'album') { + openAlbum(item); + } else { + openPlaylist(item); + } +}; + const goToImportPlaylist = () => { router.push('/playlist/import'); }; @@ -231,6 +302,23 @@ const loadData = async () => { } }; +// 加载专辑列表 +const loadAlbumList = async () => { + try { + infoLoading.value = true; + const res = await getUserAlbumSublist({ limit: 100, offset: 0 }); + if (!mounted.value) return; + albumList.value = res.data.data || []; + } catch (error: any) { + console.error('加载专辑列表失败:', error); + message.error('加载专辑列表失败'); + } finally { + if (mounted.value) { + infoLoading.value = false; + } + } +}; + // 监听路由变化 watch( () => router.currentRoute.value.path, @@ -255,6 +343,18 @@ watch( } ); +// 监听 tab 切换 +watch(currentTab, async (newTab) => { + if (newTab === 'album') { + // 刷新收藏专辑列表到 store + await userStore.initializeCollectedAlbums(); + // 如果本地列表为空,则加载 + if (albumList.value.length === 0) { + loadAlbumList(); + } + } +}); + // 页面挂载时检查登录状态 onMounted(() => { checkLoginStatus() && loadData(); @@ -279,6 +379,38 @@ const openPlaylist = (item: any) => { }); }; +// 打开专辑 +const openAlbum = async (item: any) => { + // 使用专辑 API 获取专辑详情 + try { + listLoading.value = true; + const { getAlbumDetail } = await import('@/api/music'); + const res = await getAlbumDetail(item.id.toString()); + + if (res.data?.album && res.data?.songs) { + const albumData = res.data.album; + const songs = res.data.songs.map((item) => ({ + ...item, + picUrl: albumData.picUrl + })); + + navigateToMusicList(router, { + id: item.id, + type: 'album', + name: albumData.name, + songList: songs, + listInfo: albumData, + canRemove: false // 专辑不支持移除歌曲 + }); + } + } catch (error) { + console.error('加载专辑失败:', error); + message.error('加载专辑失败'); + } finally { + listLoading.value = false; + } +}; + const handlePlay = () => { const tracks = recordList.value || []; playerStore.setPlayList(tracks); @@ -321,13 +453,7 @@ const currentLoginType = computed(() => userStore.loginType); .title { @apply text-lg font-bold flex items-center justify-between; @apply text-gray-900 dark:text-white; - .import-btn { - @apply bg-light-100 font-normal rounded-lg px-2 py-1 text-opacity-70 text-sm hover:bg-light-200 hover:text-green-500 dark:bg-dark-200 dark:hover:bg-dark-300 dark:hover:text-green-400; - @apply cursor-pointer; - @apply transition-all duration-200; - } } - .user-name { @apply text-xl font-bold mb-4 flex justify-between; @apply text-white text-opacity-70; @@ -393,14 +519,15 @@ const currentLoginType = computed(() => userStore.loginType); } &-item { - @apply flex items-center px-2 py-1 rounded-xl cursor-pointer; + @apply flex items-center px-2 py-2 rounded-xl cursor-pointer w-full; @apply transition-all duration-200; @apply hover:bg-light-200 dark:hover:bg-dark-200; &-img { - width: 60px; - height: 60px; - @apply rounded-xl; + @apply flex items-center justify-center rounded-xl text-[40px] w-[60px] h-[60px] bg-light-300 dark:bg-dark-300; + .iconfont { + @apply text-[40px]; + } } &-info { @@ -442,4 +569,11 @@ const currentLoginType = computed(() => userStore.loginType); @apply flex justify-center items-center h-full w-full; } } + +:deep(.n-tabs-rail) { + @apply rounded-xl overflow-hidden !important; + .n-tabs-capsule { + @apply rounded-xl !important; + } +}